From 1b1aa4915b2d7eb48a70d346030036bfb8d5d71c Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 3 Oct 2025 21:50:56 +0530 Subject: [PATCH] chore: link to docs, QB flavour for expr and update options (#9246) --- .../AlertCondition/AlertThreshold.tsx | 12 +- .../Footer/__tests__/utils.test.ts | 12 +- .../NotificationSettings.tsx | 4 +- .../ChartPreview/ChartPreview.tsx | 2 - .../CreateAlertV2/__tests__/utils.test.tsx | 2 +- .../src/container/CreateAlertV2/constants.ts | 2 +- .../CreateAlertV2/context/constants.ts | 7 +- .../src/container/CreateAlertV2/utils.tsx | 6 +- .../container/ListAlertRules/ListAlert.tsx | 18 +- .../RoutingPolicies/RoutingPolicies.tsx | 1 + .../RoutingPolicies/RoutingPolicyList.tsx | 17 +- .../__tests__/RoutingPoliciesList.test.tsx | 6 +- .../src/container/RoutingPolicies/types.ts | 1 + go.mod | 2 + go.sum | 4 +- .../rulebasednotification/provider_test.go | 204 ++++++++++++++++++ 16 files changed, 273 insertions(+), 27 deletions(-) diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx b/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx index c67eebf77490..02c1f0929b71 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx +++ b/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx @@ -4,8 +4,10 @@ import '../EvaluationSettings/styles.scss'; import { Button, Select, Tooltip, Typography } from 'antd'; import classNames from 'classnames'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import getRandomColor from 'lib/getRandomColor'; import { Plus } from 'lucide-react'; import { useEffect } from 'react'; +import { v4 } from 'uuid'; import { useCreateAlertState } from '../context'; import { @@ -68,11 +70,15 @@ function AlertThreshold({ const addThreshold = (): void => { let newThreshold; if (thresholdState.thresholds.length === 1) { - newThreshold = INITIAL_WARNING_THRESHOLD; + newThreshold = { ...INITIAL_WARNING_THRESHOLD, id: v4() }; } else if (thresholdState.thresholds.length === 2) { - newThreshold = INITIAL_INFO_THRESHOLD; + newThreshold = { ...INITIAL_INFO_THRESHOLD, id: v4() }; } else { - newThreshold = INITIAL_RANDOM_THRESHOLD; + newThreshold = { + ...INITIAL_RANDOM_THRESHOLD, + id: v4(), + color: getRandomColor(), + }; } setThresholdState({ type: 'SET_THRESHOLDS', diff --git a/frontend/src/container/CreateAlertV2/Footer/__tests__/utils.test.ts b/frontend/src/container/CreateAlertV2/Footer/__tests__/utils.test.ts index 3e8fa9071894..56921627924b 100644 --- a/frontend/src/container/CreateAlertV2/Footer/__tests__/utils.test.ts +++ b/frontend/src/container/CreateAlertV2/Footer/__tests__/utils.test.ts @@ -141,7 +141,7 @@ describe('Footer utils', () => { groupBy: [], renotify: { enabled: false, - interval: '1m', + interval: '30m', alertStates: [], }, usePolicy: false, @@ -154,7 +154,7 @@ describe('Footer utils', () => { ...INITIAL_NOTIFICATION_SETTINGS_STATE, reNotification: { enabled: true, - value: 1, + value: 30, unit: UniversalYAxisUnit.MINUTES, conditions: ['firing'], }, @@ -165,7 +165,7 @@ describe('Footer utils', () => { groupBy: [], renotify: { enabled: true, - interval: '1m', + interval: '30m', alertStates: ['firing'], }, usePolicy: false, @@ -183,7 +183,7 @@ describe('Footer utils', () => { groupBy: [], renotify: { enabled: false, - interval: '1m', + interval: '30m', alertStates: [], }, usePolicy: true, @@ -201,7 +201,7 @@ describe('Footer utils', () => { groupBy: ['test group'], renotify: { enabled: false, - interval: '1m', + interval: '30m', alertStates: [], }, usePolicy: false, @@ -495,7 +495,7 @@ describe('Footer utils', () => { groupBy: [], renotify: { enabled: false, - interval: '1m', + interval: '30m', alertStates: [], }, usePolicy: false, diff --git a/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationSettings.tsx b/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationSettings.tsx index 4b6e0b17e0b8..2ef35c450cb1 100644 --- a/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationSettings.tsx +++ b/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationSettings.tsx @@ -4,8 +4,8 @@ import { Input, Select, Typography } from 'antd'; import { useCreateAlertState } from '../context'; import { - ADVANCED_OPTIONS_TIME_UNIT_OPTIONS as RE_NOTIFICATION_UNIT_OPTIONS, RE_NOTIFICATION_CONDITION_OPTIONS, + RE_NOTIFICATION_TIME_UNIT_OPTIONS, } from '../context/constants'; import AdvancedOptionItem from '../EvaluationSettings/AdvancedOptionItem'; import Stepper from '../Stepper'; @@ -45,7 +45,7 @@ function NotificationSettings(): JSX.Element { value={notificationSettings.reNotification.unit || null} placeholder="Select unit" disabled={!notificationSettings.reNotification.enabled} - options={RE_NOTIFICATION_UNIT_OPTIONS} + options={RE_NOTIFICATION_TIME_UNIT_OPTIONS} onChange={(value): void => { setNotificationSettings({ type: 'SET_RE_NOTIFICATION', diff --git a/frontend/src/container/CreateAlertV2/QuerySection/ChartPreview/ChartPreview.tsx b/frontend/src/container/CreateAlertV2/QuerySection/ChartPreview/ChartPreview.tsx index 9335e3a2c690..05703de36016 100644 --- a/frontend/src/container/CreateAlertV2/QuerySection/ChartPreview/ChartPreview.tsx +++ b/frontend/src/container/CreateAlertV2/QuerySection/ChartPreview/ChartPreview.tsx @@ -51,7 +51,6 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element { yAxisUnit={yAxisUnit || ''} graphType={panelType || PANEL_TYPES.TIME_SERIES} setQueryStatus={setQueryStatus} - showSideLegend additionalThresholds={thresholdState.thresholds} /> ); @@ -66,7 +65,6 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element { yAxisUnit={yAxisUnit || ''} graphType={panelType || PANEL_TYPES.TIME_SERIES} setQueryStatus={setQueryStatus} - showSideLegend additionalThresholds={thresholdState.thresholds} /> ); diff --git a/frontend/src/container/CreateAlertV2/__tests__/utils.test.tsx b/frontend/src/container/CreateAlertV2/__tests__/utils.test.tsx index 5ab1e63a1264..d480707a63e1 100644 --- a/frontend/src/container/CreateAlertV2/__tests__/utils.test.tsx +++ b/frontend/src/container/CreateAlertV2/__tests__/utils.test.tsx @@ -216,7 +216,7 @@ describe('CreateAlertV2 utils', () => { multipleNotifications: ['email'], reNotification: { enabled: false, - value: 1, + value: 30, unit: UniversalYAxisUnit.MINUTES, conditions: [], }, diff --git a/frontend/src/container/CreateAlertV2/constants.ts b/frontend/src/container/CreateAlertV2/constants.ts index 5049c8099b4e..b48af9acada5 100644 --- a/frontend/src/container/CreateAlertV2/constants.ts +++ b/frontend/src/container/CreateAlertV2/constants.ts @@ -22,7 +22,7 @@ const defaultNotificationSettings: PostableAlertRuleV2['notificationSettings'] = groupBy: [], renotify: { enabled: false, - interval: '1m', + interval: '30m', alertStates: [], }, usePolicy: false, diff --git a/frontend/src/container/CreateAlertV2/context/constants.ts b/frontend/src/container/CreateAlertV2/context/constants.ts index dabd9b1508fc..b6a50b06096c 100644 --- a/frontend/src/container/CreateAlertV2/context/constants.ts +++ b/frontend/src/container/CreateAlertV2/context/constants.ts @@ -172,6 +172,11 @@ export const ADVANCED_OPTIONS_TIME_UNIT_OPTIONS = [ { value: UniversalYAxisUnit.HOURS, label: 'Hours' }, ]; +export const RE_NOTIFICATION_TIME_UNIT_OPTIONS = [ + { value: UniversalYAxisUnit.MINUTES, label: 'Minutes' }, + { value: UniversalYAxisUnit.HOURS, label: 'Hours' }, +]; + export const NOTIFICATION_MESSAGE_PLACEHOLDER = 'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})'; @@ -184,7 +189,7 @@ export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = { multipleNotifications: [], reNotification: { enabled: false, - value: 1, + value: 30, unit: UniversalYAxisUnit.MINUTES, conditions: [], }, diff --git a/frontend/src/container/CreateAlertV2/utils.tsx b/frontend/src/container/CreateAlertV2/utils.tsx index d0072681b721..41ae9c7577c4 100644 --- a/frontend/src/container/CreateAlertV2/utils.tsx +++ b/frontend/src/container/CreateAlertV2/utils.tsx @@ -198,10 +198,10 @@ export function getNotificationSettingsStateFromAlertDef( (state) => state as 'firing' | 'nodata', ) || []; const reNotificationValue = alertDef.notificationSettings?.renotify - ? parseGoTime(alertDef.notificationSettings.renotify.interval || '1m').time - : 1; + ? parseGoTime(alertDef.notificationSettings.renotify.interval || '30m').time + : 30; const reNotificationUnit = alertDef.notificationSettings?.renotify - ? parseGoTime(alertDef.notificationSettings.renotify.interval || '1m').unit + ? parseGoTime(alertDef.notificationSettings.renotify.interval || '30m').unit : UniversalYAxisUnit.MINUTES; return { diff --git a/frontend/src/container/ListAlertRules/ListAlert.tsx b/frontend/src/container/ListAlertRules/ListAlert.tsx index dafa333373bf..ea28a58b2f8e 100644 --- a/frontend/src/container/ListAlertRules/ListAlert.tsx +++ b/frontend/src/container/ListAlertRules/ListAlert.tsx @@ -1,6 +1,14 @@ /* eslint-disable react/display-name */ import { PlusOutlined } from '@ant-design/icons'; -import { Button, Dropdown, Flex, Input, MenuProps, Typography } from 'antd'; +import { + Button, + Dropdown, + Flex, + Input, + MenuProps, + Tag, + Typography, +} from 'antd'; import type { ColumnsType } from 'antd/es/table/interface'; import saveAlertApi from 'api/alerts/save'; import logEvent from 'api/common/logEvent'; @@ -118,12 +126,16 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { const newAlertMenuItems: MenuProps['items'] = [ { key: 'new', - label: 'Try the new experience', + label: ( + + Try the new experience Beta + + ), onClick: onClickNewAlertV2Handler, }, { key: 'classic', - label: 'Continue with the current experience', + label: 'Continue with the classic experience', onClick: onClickNewClassicAlertHandler, }, ]; diff --git a/frontend/src/container/RoutingPolicies/RoutingPolicies.tsx b/frontend/src/container/RoutingPolicies/RoutingPolicies.tsx index b5cb3f08d4f5..f340e8e14783 100644 --- a/frontend/src/container/RoutingPolicies/RoutingPolicies.tsx +++ b/frontend/src/container/RoutingPolicies/RoutingPolicies.tsx @@ -88,6 +88,7 @@ function RoutingPolicies(): JSX.Element { isRoutingPoliciesError={isErrorRoutingPolicies} handlePolicyDetailsModalOpen={handlePolicyDetailsModalOpen} handleDeleteModalOpen={handleDeleteModalOpen} + hasSearchTerm={(searchTerm?.length ?? 0) > 0} /> {policyDetailsModalState.isOpen && ( ['columns'] = [ { @@ -25,6 +26,7 @@ function RoutingPolicyList({ }, ]; + /* eslint-disable no-nested-ternary */ const localeEmptyState = useMemo( () => (
@@ -41,12 +43,23 @@ function RoutingPolicyList({ Something went wrong while fetching routing policies. + ) : hasSearchTerm ? ( + No matching routing policies found. ) : ( - No routing policies found. + + No routing policies yet,{' '} + + Learn more here + + )}
), - [isRoutingPoliciesError], + [isRoutingPoliciesError, hasSearchTerm], ); return ( diff --git a/frontend/src/container/RoutingPolicies/__tests__/RoutingPoliciesList.test.tsx b/frontend/src/container/RoutingPolicies/__tests__/RoutingPoliciesList.test.tsx index 62d41fc76cfa..f8adeaa4d9da 100644 --- a/frontend/src/container/RoutingPolicies/__tests__/RoutingPoliciesList.test.tsx +++ b/frontend/src/container/RoutingPolicies/__tests__/RoutingPoliciesList.test.tsx @@ -28,6 +28,7 @@ describe('RoutingPoliciesList', () => { isRoutingPoliciesError={useRoutingPolicesMockData.isErrorRoutingPolicies} handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen} handleDeleteModalOpen={mockHandleDeleteModalOpen} + hasSearchTerm={false} />, ); @@ -51,6 +52,7 @@ describe('RoutingPoliciesList', () => { isRoutingPoliciesError={false} handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen} handleDeleteModalOpen={mockHandleDeleteModalOpen} + hasSearchTerm={false} />, ); // Check for loading spinner by class name @@ -67,6 +69,7 @@ describe('RoutingPoliciesList', () => { isRoutingPoliciesError handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen} handleDeleteModalOpen={mockHandleDeleteModalOpen} + hasSearchTerm={false} />, ); expect( @@ -82,8 +85,9 @@ describe('RoutingPoliciesList', () => { isRoutingPoliciesError={false} handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen} handleDeleteModalOpen={mockHandleDeleteModalOpen} + hasSearchTerm={false} />, ); - expect(screen.getByText('No routing policies found.')).toBeInTheDocument(); + expect(screen.getByText('No routing policies yet,')).toBeInTheDocument(); }); }); diff --git a/frontend/src/container/RoutingPolicies/types.ts b/frontend/src/container/RoutingPolicies/types.ts index 631d7b09858d..8fc5908f7986 100644 --- a/frontend/src/container/RoutingPolicies/types.ts +++ b/frontend/src/container/RoutingPolicies/types.ts @@ -37,6 +37,7 @@ export interface RoutingPolicyListProps { isRoutingPoliciesError: boolean; handlePolicyDetailsModalOpen: HandlePolicyDetailsModalOpen; handleDeleteModalOpen: HandleDeleteModalOpen; + hasSearchTerm: boolean; } export interface RoutingPolicyListItemProps { diff --git a/go.mod b/go.mod index fcd2e0124a9b..0b70e74e0a40 100644 --- a/go.mod +++ b/go.mod @@ -338,3 +338,5 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) + +replace github.com/expr-lang/expr => github.com/SigNoz/expr v1.17.7-beta diff --git a/go.sum b/go.sum index db6a0ae3d544..f6c6a857f389 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,8 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/SigNoz/expr v1.17.7-beta h1:FyZkleM5dTQ0O6muQfwGpoH5A2ohmN/XTasRCO72gAA= +github.com/SigNoz/expr v1.17.7-beta/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc= github.com/SigNoz/signoz-otel-collector v0.129.4 h1:DGDu9y1I1FU+HX4eECPGmfhnXE4ys4yr7LL6znbf6to= @@ -248,8 +250,6 @@ github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= -github.com/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k= -github.com/expr-lang/expr v1.17.5/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= diff --git a/pkg/alertmanager/nfmanager/rulebasednotification/provider_test.go b/pkg/alertmanager/nfmanager/rulebasednotification/provider_test.go index 37ef095ebfb5..8c32f6e42412 100644 --- a/pkg/alertmanager/nfmanager/rulebasednotification/provider_test.go +++ b/pkg/alertmanager/nfmanager/rulebasednotification/provider_test.go @@ -295,6 +295,15 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "simple equality check - match", + expression: `threshold.name = 'auth' AND ruleId = 'rule1'`, + labelSet: model.LabelSet{ + "threshold.name": "auth", + "ruleId": "rule1", + }, + expected: true, + }, { name: "simple equality check - no match", expression: `service == "payment"`, @@ -304,6 +313,15 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: false, }, + { + name: "simple equality check - no match", + expression: `service = "payment"`, + labelSet: model.LabelSet{ + "service": "auth", + "env": "production", + }, + expected: false, + }, { name: "multiple conditions with AND - both match", expression: `service == "auth" && env == "production"`, @@ -313,6 +331,15 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "multiple conditions with AND - both match", + expression: `service = "auth" AND env = "production"`, + labelSet: model.LabelSet{ + "service": "auth", + "env": "production", + }, + expected: true, + }, { name: "multiple conditions with AND - one doesn't match", expression: `service == "auth" && env == "staging"`, @@ -322,6 +349,15 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: false, }, + { + name: "multiple conditions with AND - one doesn't match", + expression: `service = "auth" AND env = "staging"`, + labelSet: model.LabelSet{ + "service": "auth", + "env": "production", + }, + expected: false, + }, { name: "multiple conditions with OR - one matches", expression: `service == "payment" || env == "production"`, @@ -331,6 +367,15 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "multiple conditions with OR - one matches", + expression: `service = "payment" OR env = "production"`, + labelSet: model.LabelSet{ + "service": "auth", + "env": "production", + }, + expected: true, + }, { name: "multiple conditions with OR - none match", expression: `service == "payment" || env == "staging"`, @@ -340,6 +385,15 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: false, }, + { + name: "multiple conditions with OR - none match", + expression: `service = "payment" OR env = "staging"`, + labelSet: model.LabelSet{ + "service": "auth", + "env": "production", + }, + expected: false, + }, { name: "in operator - value in list", expression: `service in ["auth", "payment", "notification"]`, @@ -348,6 +402,14 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "in operator - value in list", + expression: `service IN ["auth", "payment", "notification"]`, + labelSet: model.LabelSet{ + "service": "auth", + }, + expected: true, + }, { name: "in operator - value not in list", expression: `service in ["payment", "notification"]`, @@ -356,6 +418,14 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: false, }, + { + name: "in operator - value not in list", + expression: `service IN ["payment", "notification"]`, + labelSet: model.LabelSet{ + "service": "auth", + }, + expected: false, + }, { name: "contains operator - substring match", expression: `host contains "prod"`, @@ -364,6 +434,14 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "contains operator - substring match", + expression: `host CONTAINS "prod"`, + labelSet: model.LabelSet{ + "host": "prod-server-01", + }, + expected: true, + }, { name: "contains operator - no substring match", expression: `host contains "staging"`, @@ -372,6 +450,14 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: false, }, + { + name: "contains operator - no substring match", + expression: `host CONTAINS "staging"`, + labelSet: model.LabelSet{ + "host": "prod-server-01", + }, + expected: false, + }, { name: "complex expression with parentheses", expression: `(service == "auth" && env == "production") || critical == "true"`, @@ -382,6 +468,16 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "complex expression with parentheses", + expression: `(service = "auth" AND env = "production") OR critical = "true"`, + labelSet: model.LabelSet{ + "service": "payment", + "env": "staging", + "critical": "true", + }, + expected: true, + }, { name: "missing label key", expression: `"missing_key" == "value"`, @@ -390,6 +486,14 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: false, }, + { + name: "missing label key", + expression: `"missing_key" = "value"`, + labelSet: model.LabelSet{ + "service": "auth", + }, + expected: false, + }, { name: "rule-based expression with threshold name and ruleId", expression: `'threshold.name' == "high-cpu" && ruleId == "rule-123"`, @@ -400,6 +504,16 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: false, //no commas }, + { + name: "rule-based expression with threshold name and ruleId", + expression: `'threshold.name' = "high-cpu" AND ruleId == "rule-123"`, + labelSet: model.LabelSet{ + "threshold.name": "high-cpu", + "ruleId": "rule-123", + "service": "auth", + }, + expected: false, //no commas + }, { name: "alertname and ruleId combination", expression: `alertname == "HighCPUUsage" && ruleId == "cpu-alert-001"`, @@ -410,6 +524,16 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "alertname and ruleId combination", + expression: `alertname = "HighCPUUsage" AND ruleId = "cpu-alert-001"`, + labelSet: model.LabelSet{ + "alertname": "HighCPUUsage", + "ruleId": "cpu-alert-001", + "severity": "critical", + }, + expected: true, + }, { name: "kubernetes namespace filtering", expression: `k8s.namespace.name == "auth" && service in ["auth", "payment"]`, @@ -420,6 +544,16 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "kubernetes namespace filtering", + expression: `k8s.namespace.name = "auth" && service IN ["auth", "payment"]`, + labelSet: model.LabelSet{ + "k8s.namespace.name": "auth", + "service": "auth", + "host": "k8s-node-1", + }, + expected: true, + }, { name: "migration expression format from SQL migration", expression: `threshold.name == "HighCPUUsage" && ruleId == "rule-uuid-123"`, @@ -430,6 +564,16 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "migration expression format from SQL migration", + expression: `threshold.name = "HighCPUUsage" && ruleId = "rule-uuid-123"`, + labelSet: model.LabelSet{ + "threshold.name": "HighCPUUsage", + "ruleId": "rule-uuid-123", + "severity": "warning", + }, + expected: true, + }, { name: "case sensitive matching", expression: `service == "Auth"`, // capital A @@ -438,6 +582,14 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: false, }, + { + name: "case sensitive matching", + expression: `service = "Auth"`, // capital A + labelSet: model.LabelSet{ + "service": "auth", // lowercase a + }, + expected: false, + }, { name: "numeric comparison as strings", expression: `port == "8080"`, @@ -446,6 +598,14 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "numeric comparison as strings", + expression: `port = "8080"`, + labelSet: model.LabelSet{ + "port": "8080", + }, + expected: true, + }, { name: "quoted string with special characters", expression: `service == "auth-service-v2"`, @@ -454,6 +614,14 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "quoted string with special characters", + expression: `service = "auth-service-v2"`, + labelSet: model.LabelSet{ + "service": "auth-service-v2", + }, + expected: true, + }, { name: "boolean operators precedence", expression: `service == "auth" && env == "prod" || critical == "true"`, @@ -464,6 +632,16 @@ func TestProvider_EvaluateExpression(t *testing.T) { }, expected: true, }, + { + name: "boolean operators precedence", + expression: `service = "auth" AND env = "prod" OR critical = "true"`, + labelSet: model.LabelSet{ + "service": "payment", + "env": "staging", + "critical": "true", + }, + expected: true, + }, } for _, tt := range tests { @@ -554,6 +732,21 @@ func TestProvider_CreateRoute(t *testing.T) { }, wantErr: false, }, + { + name: "valid route qb format", + orgID: "test-org-123", + route: &alertmanagertypes.RoutePolicy{ + Identifiable: types.Identifiable{ID: valuer.GenerateUUID()}, + Expression: `service = "auth"`, + ExpressionKind: alertmanagertypes.PolicyBasedExpression, + Name: "auth-service-route", + Description: "Route for auth service alerts", + Enabled: true, + OrgID: "test-org-123", + Channels: []string{"slack-channel"}, + }, + wantErr: false, + }, { name: "nil route", orgID: "test-org-123", @@ -582,6 +775,17 @@ func TestProvider_CreateRoute(t *testing.T) { }, wantErr: true, }, + { + name: "invalid route - missing name", + orgID: "test-org-123", + route: &alertmanagertypes.RoutePolicy{ + Expression: `service = "auth"`, + ExpressionKind: alertmanagertypes.PolicyBasedExpression, + Name: "", // empty name + OrgID: "test-org-123", + }, + wantErr: true, + }, } for _, tt := range tests {