Compare commits

..

1 Commits

Author SHA1 Message Date
aniket
5e3449db63 chore: added normal index in sql schema 2025-10-02 15:48:59 +05:30
94 changed files with 1313 additions and 5263 deletions

View File

@@ -251,7 +251,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
continue
}
}
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
results, err := r.Threshold.ShouldAlert(*series)
if err != nil {
return nil, err
}
@@ -301,7 +301,7 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
continue
}
}
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
results, err := r.Threshold.ShouldAlert(*series)
if err != nil {
return nil, err
}
@@ -336,19 +336,14 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
resultFPs := map[uint64]struct{}{}
var alerts = make(map[uint64]*ruletypes.Alert, len(res))
ruleReceivers := r.Threshold.GetRuleReceivers()
ruleReceiverMap := make(map[string][]string)
for _, value := range ruleReceivers {
ruleReceiverMap[value.Name] = value.Channels
}
for _, smpl := range res {
l := make(map[string]string, len(smpl.Metric))
for _, lbl := range smpl.Metric {
l[lbl.Name] = lbl.Value
}
value := valueFormatter.Format(smpl.V, r.Unit())
threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
threshold := valueFormatter.Format(r.TargetVal(), r.Unit())
r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
@@ -413,12 +408,13 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
State: model.StatePending,
Value: smpl.V,
GeneratorURL: r.GeneratorURL(),
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
Receivers: r.PreferredChannels(),
Missing: smpl.IsMissing,
}
}
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
// alerts[h] is ready, add or update active list now
for h, a := range alerts {
// Check whether we already have alerting state for the identifying label set.
@@ -427,9 +423,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
alert.Value = a.Value
alert.Annotations = a.Annotations
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
alert.Receivers = ruleReceiverMap[v]
}
alert.Receivers = r.PreferredChannels()
continue
}

View File

@@ -126,6 +126,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
// add special labels for test alerts
parsedRule.Annotations[labels.AlertSummaryLabel] = fmt.Sprintf("The rule threshold is set to %.4f, and the observed metric value is {{$value}}.", *parsedRule.RuleCondition.Target)
parsedRule.Labels[labels.RuleSourceLabel] = ""
parsedRule.Labels[labels.AlertRuleIdLabel] = ""

View File

@@ -1,8 +1,9 @@
import { Page } from '@playwright/test';
// Read credentials from environment variables
const username = process.env.SIGNOZ_E2E_USERNAME;
const password = process.env.SIGNOZ_E2E_PASSWORD;
const username = process.env.LOGIN_USERNAME;
const password = process.env.LOGIN_PASSWORD;
const baseURL = process.env.BASE_URL;
/**
* Ensures the user is logged in. If not, performs the login steps.
@@ -10,17 +11,17 @@ const password = process.env.SIGNOZ_E2E_PASSWORD;
*/
export async function ensureLoggedIn(page: Page): Promise<void> {
// if already in home page, return
if (page.url().includes('/home')) {
if (await page.url().includes('/home')) {
return;
}
if (!username || !password) {
throw new Error(
'SIGNOZ_E2E_USERNAME and SIGNOZ_E2E_PASSWORD environment variables must be set.',
'E2E_EMAIL and E2E_PASSWORD environment variables must be set.',
);
}
await page.goto('/login');
await page.goto(`${baseURL}/login`);
await page.getByTestId('email').click();
await page.getByTestId('email').fill(username);
await page.getByTestId('initiate_login').click();

View File

@@ -4,9 +4,4 @@ FRONTEND_API_ENDPOINT="http://localhost:8080/"
PYLON_APP_ID="pylon-app-id"
APPCUES_APP_ID="appcess-app-id"
CI="1"
# Playwright E2E Test Configuration
SIGNOZ_E2E_BASE_URL="your-dev-environment-url"
SIGNOZ_E2E_USERNAME="your-email@example.com"
SIGNOZ_E2E_PASSWORD="your-password"
CI="1"

View File

@@ -18,12 +18,7 @@
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"e2e": "playwright test",
"e2e:ui": "playwright test --ui",
"e2e:headed": "playwright test --headed",
"e2e:debug": "playwright test --debug",
"e2e:report": "playwright show-report"
"test:changedsince": "jest --changedSince=main --coverage --silent"
},
"engines": {
"node": ">=16.15.0"

View File

@@ -45,7 +45,14 @@ export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
use: {
launchOptions: { args: ['--start-maximized'] },
viewport: null,
colorScheme: 'dark',
locale: 'en-US',
baseURL: 'https://app.us.staging.signoz.cloud',
trace: 'on-first-retry',
},
},
{

View File

@@ -13,12 +13,14 @@ import APIError from 'types/api/error';
import { useCreateAlertState } from '../context';
import AdvancedOptions from '../EvaluationSettings/AdvancedOptions';
import Stepper from '../Stepper';
import { showCondensedLayout } from '../utils';
import AlertThreshold from './AlertThreshold';
import AnomalyThreshold from './AnomalyThreshold';
import { ANOMALY_TAB_TOOLTIP, THRESHOLD_TAB_TOOLTIP } from './constants';
function AlertCondition(): JSX.Element {
const { alertType, setAlertType } = useCreateAlertState();
const showCondensedLayoutFlag = showCondensedLayout();
const {
data,
@@ -106,9 +108,11 @@ function AlertCondition(): JSX.Element {
refreshChannels={refreshChannels}
/>
)}
<div className="condensed-advanced-options-container">
<AdvancedOptions />
</div>
{showCondensedLayoutFlag ? (
<div className="condensed-advanced-options-container">
<AdvancedOptions />
</div>
) : null}
</div>
);
}

View File

@@ -4,10 +4,8 @@ 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 {
@@ -18,6 +16,7 @@ import {
THRESHOLD_OPERATOR_OPTIONS,
} from '../context/constants';
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
import { showCondensedLayout } from '../utils';
import ThresholdItem from './ThresholdItem';
import { AnomalyAndThresholdProps, UpdateThreshold } from './types';
import {
@@ -42,6 +41,8 @@ function AlertThreshold({
setNotificationSettings,
} = useCreateAlertState();
const showCondensedLayoutFlag = showCondensedLayout();
const { currentQuery } = useQueryBuilder();
const queryNames = getQueryNames(currentQuery);
@@ -67,15 +68,11 @@ function AlertThreshold({
const addThreshold = (): void => {
let newThreshold;
if (thresholdState.thresholds.length === 1) {
newThreshold = { ...INITIAL_WARNING_THRESHOLD, id: v4() };
newThreshold = INITIAL_WARNING_THRESHOLD;
} else if (thresholdState.thresholds.length === 2) {
newThreshold = { ...INITIAL_INFO_THRESHOLD, id: v4() };
newThreshold = INITIAL_INFO_THRESHOLD;
} else {
newThreshold = {
...INITIAL_RANDOM_THRESHOLD,
id: v4(),
color: getRandomColor(),
};
newThreshold = INITIAL_RANDOM_THRESHOLD;
}
setThresholdState({
type: 'SET_THRESHOLDS',
@@ -160,12 +157,17 @@ function AlertThreshold({
}),
);
const evaluationWindowContext = showCondensedLayoutFlag ? (
<EvaluationSettings />
) : (
<strong>Evaluation Window.</strong>
);
return (
<div
className={classNames(
'alert-threshold-container',
'condensed-alert-threshold-container',
)}
className={classNames('alert-threshold-container', {
'condensed-alert-threshold-container': showCondensedLayoutFlag,
})}
>
{/* Main condition sentence */}
<div className="alert-condition-sentences">
@@ -211,7 +213,7 @@ function AlertThreshold({
options={matchTypeOptionsWithTooltips}
/>
<Typography.Text className="sentence-text">
during the <EvaluationSettings />
during the {evaluationWindowContext}
</Typography.Text>
</div>
</div>

View File

@@ -110,6 +110,7 @@ jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
jest.mock('container/CreateAlertV2/utils', () => ({
...jest.requireActual('container/CreateAlertV2/utils'),
showCondensedLayout: jest.fn().mockReturnValue(false),
}));
const TEST_STRINGS = {
@@ -158,9 +159,7 @@ describe('AlertThreshold', () => {
expect(screen.getByText('Send a notification when')).toBeInTheDocument();
expect(screen.getByText('the threshold(s)')).toBeInTheDocument();
expect(screen.getByText('during the')).toBeInTheDocument();
expect(
screen.getByTestId('condensed-evaluation-settings-container'),
).toBeInTheDocument();
expect(screen.getByText('Evaluation Window.')).toBeInTheDocument();
});
it('renders query selection dropdown', async () => {

View File

@@ -8,11 +8,12 @@ import AlertCondition from './AlertCondition';
import { CreateAlertProvider } from './context';
import { buildInitialAlertDef } from './context/utils';
import CreateAlertHeader from './CreateAlertHeader';
import EvaluationSettings from './EvaluationSettings';
import Footer from './Footer';
import NotificationSettings from './NotificationSettings';
import QuerySection from './QuerySection';
import { CreateAlertV2Props } from './types';
import { Spinner } from './utils';
import { showCondensedLayout, Spinner } from './utils';
function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element {
const queryToRedirect = buildInitialAlertDef(alertType);
@@ -22,6 +23,8 @@ function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element {
useShareBuilderUrl({ defaultValue: currentQueryToRedirect });
const showCondensedLayoutFlag = showCondensedLayout();
return (
<CreateAlertProvider initialAlertType={alertType}>
<Spinner />
@@ -29,6 +32,7 @@ function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element {
<CreateAlertHeader />
<QuerySection />
<AlertCondition />
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
<NotificationSettings />
</div>
<Footer />

View File

@@ -1,19 +1,28 @@
import './styles.scss';
import { Button, Popover } from 'antd';
import { Button, Popover, Typography } from 'antd';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useState } from 'react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { useCreateAlertState } from '../context';
import Stepper from '../Stepper';
import { showCondensedLayout } from '../utils';
import AdvancedOptions from './AdvancedOptions';
import EvaluationWindowPopover from './EvaluationWindowPopover';
import { getEvaluationWindowTypeText, getTimeframeText } from './utils';
function EvaluationSettings(): JSX.Element {
const { evaluationWindow, setEvaluationWindow } = useCreateAlertState();
const {
alertType,
evaluationWindow,
setEvaluationWindow,
} = useCreateAlertState();
const [
isEvaluationWindowPopoverOpen,
setIsEvaluationWindowPopoverOpen,
] = useState(false);
const showCondensedLayoutFlag = showCondensedLayout();
const popoverContent = (
<Popover
@@ -48,12 +57,33 @@ function EvaluationSettings(): JSX.Element {
</Popover>
);
// Layout consists of only the evaluation window popover
if (showCondensedLayoutFlag) {
return (
<div
className="condensed-evaluation-settings-container"
data-testid="condensed-evaluation-settings-container"
>
{popoverContent}
</div>
);
}
// Layout consists of
// - Stepper header
// - Evaluation window popover
// - Advanced options
return (
<div
className="condensed-evaluation-settings-container"
data-testid="condensed-evaluation-settings-container"
>
{popoverContent}
<div className="evaluation-settings-container">
<Stepper stepNumber={3} label="Evaluation settings" />
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && (
<div className="evaluate-alert-conditions-container">
<Typography.Text>Check conditions using data from</Typography.Text>
<div className="evaluate-alert-conditions-separator" />
{popoverContent}
</div>
)}
<AdvancedOptions />
</div>
);
}

View File

@@ -1,11 +1,14 @@
import { render, screen } from '@testing-library/react';
import * as alertState from 'container/CreateAlertV2/context';
import * as utils from 'container/CreateAlertV2/utils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import EvaluationSettings from '../EvaluationSettings';
import { createMockAlertContextState } from './testUtils';
jest.mock('container/CreateAlertV2/utils', () => ({
...jest.requireActual('container/CreateAlertV2/utils'),
showCondensedLayout: jest.fn().mockReturnValue(false),
}));
const mockSetEvaluationWindow = jest.fn();
@@ -15,14 +18,52 @@ jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
}),
);
jest.mock('../AdvancedOptions', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="advanced-options">AdvancedOptions</div>
),
}));
const EVALUATION_SETTINGS_TEXT = 'Evaluation settings';
const CHECK_CONDITIONS_USING_DATA_FROM_TEXT =
'Check conditions using data from';
describe('EvaluationSettings', () => {
it('should render the condensed evaluation settings layout', () => {
it('should render the default evaluation settings layout', () => {
render(<EvaluationSettings />);
expect(screen.getByText(EVALUATION_SETTINGS_TEXT)).toBeInTheDocument();
expect(
screen.getByText(CHECK_CONDITIONS_USING_DATA_FROM_TEXT),
).toBeInTheDocument();
expect(screen.getByTestId('advanced-options')).toBeInTheDocument();
});
it('should not render evaluation window for anomaly based alert', () => {
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValueOnce(
createMockAlertContextState({
alertType: AlertTypes.ANOMALY_BASED_ALERT,
}),
);
render(<EvaluationSettings />);
expect(screen.getByText(EVALUATION_SETTINGS_TEXT)).toBeInTheDocument();
expect(
screen.queryByText(CHECK_CONDITIONS_USING_DATA_FROM_TEXT),
).not.toBeInTheDocument();
});
it('should render the condensed evaluation settings layout', () => {
jest.spyOn(utils, 'showCondensedLayout').mockReturnValueOnce(true);
render(<EvaluationSettings />);
// Header, check conditions using data from and advanced options should be hidden
expect(screen.queryByText(EVALUATION_SETTINGS_TEXT)).not.toBeInTheDocument();
expect(
screen.queryByText(CHECK_CONDITIONS_USING_DATA_FROM_TEXT),
).not.toBeInTheDocument();
expect(screen.queryByTestId('advanced-options')).not.toBeInTheDocument();
// Only evaluation window popover should be visible
expect(
screen.getByTestId('condensed-evaluation-settings-container'),
).toBeInTheDocument();
// Verify that default option is selected
expect(screen.getByText('Rolling')).toBeInTheDocument();
expect(screen.getByText('Last 5 minutes')).toBeInTheDocument();
});
});

View File

@@ -141,7 +141,7 @@ describe('Footer utils', () => {
groupBy: [],
renotify: {
enabled: false,
interval: '30m',
interval: '1m',
alertStates: [],
},
usePolicy: false,
@@ -154,7 +154,7 @@ describe('Footer utils', () => {
...INITIAL_NOTIFICATION_SETTINGS_STATE,
reNotification: {
enabled: true,
value: 30,
value: 1,
unit: UniversalYAxisUnit.MINUTES,
conditions: ['firing'],
},
@@ -165,7 +165,7 @@ describe('Footer utils', () => {
groupBy: [],
renotify: {
enabled: true,
interval: '30m',
interval: '1m',
alertStates: ['firing'],
},
usePolicy: false,
@@ -183,7 +183,7 @@ describe('Footer utils', () => {
groupBy: [],
renotify: {
enabled: false,
interval: '30m',
interval: '1m',
alertStates: [],
},
usePolicy: true,
@@ -201,7 +201,7 @@ describe('Footer utils', () => {
groupBy: ['test group'],
renotify: {
enabled: false,
interval: '30m',
interval: '1m',
alertStates: [],
},
usePolicy: false,
@@ -495,7 +495,7 @@ describe('Footer utils', () => {
groupBy: [],
renotify: {
enabled: false,
interval: '30m',
interval: '1m',
alertStates: [],
},
usePolicy: false,

View File

@@ -4,15 +4,18 @@ 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';
import { showCondensedLayout } from '../utils';
import MultipleNotifications from './MultipleNotifications';
import NotificationMessage from './NotificationMessage';
function NotificationSettings(): JSX.Element {
const showCondensedLayoutFlag = showCondensedLayout();
const {
notificationSettings,
setNotificationSettings,
@@ -42,7 +45,7 @@ function NotificationSettings(): JSX.Element {
value={notificationSettings.reNotification.unit || null}
placeholder="Select unit"
disabled={!notificationSettings.reNotification.enabled}
options={RE_NOTIFICATION_TIME_UNIT_OPTIONS}
options={RE_NOTIFICATION_UNIT_OPTIONS}
onChange={(value): void => {
setNotificationSettings({
type: 'SET_RE_NOTIFICATION',
@@ -79,7 +82,10 @@ function NotificationSettings(): JSX.Element {
return (
<div className="notification-settings-container">
<Stepper stepNumber={3} label="Notification settings" />
<Stepper
stepNumber={showCondensedLayoutFlag ? 3 : 4}
label="Notification settings"
/>
<NotificationMessage />
<div className="notification-settings-content">
<MultipleNotifications />

View File

@@ -1,6 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as createAlertContext from 'container/CreateAlertV2/context';
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
import * as utils from 'container/CreateAlertV2/utils';
import NotificationSettings from '../NotificationSettings';
@@ -25,6 +26,7 @@ jest.mock(
jest.mock('container/CreateAlertV2/utils', () => ({
...jest.requireActual('container/CreateAlertV2/utils'),
showCondensedLayout: jest.fn().mockReturnValue(false),
}));
const initialNotificationSettings = createMockAlertContextState()
@@ -40,10 +42,10 @@ const REPEAT_NOTIFICATIONS_TEXT = 'Repeat notifications';
const ENTER_TIME_INTERVAL_TEXT = 'Enter time interval...';
describe('NotificationSettings', () => {
it('renders the notification settings tab with step number 3 and default values', () => {
it('renders the notification settings tab with step number 4 and default values', () => {
render(<NotificationSettings />);
expect(screen.getByText('Notification settings')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getByText('4')).toBeInTheDocument();
expect(screen.getByTestId('multiple-notifications')).toBeInTheDocument();
expect(screen.getByTestId('notification-message')).toBeInTheDocument();
expect(screen.getByText(REPEAT_NOTIFICATIONS_TEXT)).toBeInTheDocument();
@@ -54,6 +56,15 @@ describe('NotificationSettings', () => {
).toBeInTheDocument();
});
it('renders the notification settings tab with step number 3 in condensed layout', () => {
jest.spyOn(utils, 'showCondensedLayout').mockReturnValueOnce(true);
render(<NotificationSettings />);
expect(screen.getByText('Notification settings')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getByTestId('multiple-notifications')).toBeInTheDocument();
expect(screen.getByTestId('notification-message')).toBeInTheDocument();
});
describe('Repeat notifications', () => {
it('renders the repeat notifications with inputs hidden when the repeat notifications switch is off', () => {
render(<NotificationSettings />);

View File

@@ -51,6 +51,7 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
yAxisUnit={yAxisUnit || ''}
graphType={panelType || PANEL_TYPES.TIME_SERIES}
setQueryStatus={setQueryStatus}
showSideLegend
additionalThresholds={thresholdState.thresholds}
/>
);
@@ -65,6 +66,7 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
yAxisUnit={yAxisUnit || ''}
graphType={panelType || PANEL_TYPES.TIME_SERIES}
setQueryStatus={setQueryStatus}
showSideLegend
additionalThresholds={thresholdState.thresholds}
/>
);

View File

@@ -216,7 +216,7 @@ describe('CreateAlertV2 utils', () => {
multipleNotifications: ['email'],
reNotification: {
enabled: false,
value: 30,
value: 1,
unit: UniversalYAxisUnit.MINUTES,
conditions: [],
},

View File

@@ -22,7 +22,7 @@ const defaultNotificationSettings: PostableAlertRuleV2['notificationSettings'] =
groupBy: [],
renotify: {
enabled: false,
interval: '30m',
interval: '1m',
alertStates: [],
},
usePolicy: false,

View File

@@ -172,11 +172,6 @@ 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}})';
@@ -189,7 +184,7 @@ export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
multipleNotifications: [],
reNotification: {
enabled: false,
value: 30,
value: 1,
unit: UniversalYAxisUnit.MINUTES,
conditions: [],
},

View File

@@ -27,6 +27,16 @@ import {
import { EVALUATION_WINDOW_TIMEFRAME } from './EvaluationSettings/constants';
import { GetCreateAlertLocalStateFromAlertDefReturn } from './types';
// UI side feature flag
export const showNewCreateAlertsPage = (): boolean =>
localStorage.getItem('showNewCreateAlertsPage') === 'true';
// UI side FF to switch between the 2 layouts of the create alert page
// Layout 1 - Default layout
// Layout 2 - Condensed layout
export const showCondensedLayout = (): boolean =>
localStorage.getItem('hideCondensedLayout') !== 'true';
export function Spinner(): JSX.Element | null {
const { isCreatingAlertRule, isUpdatingAlertRule } = useCreateAlertState();
@@ -188,10 +198,10 @@ export function getNotificationSettingsStateFromAlertDef(
(state) => state as 'firing' | 'nodata',
) || [];
const reNotificationValue = alertDef.notificationSettings?.renotify
? parseGoTime(alertDef.notificationSettings.renotify.interval || '30m').time
: 30;
? parseGoTime(alertDef.notificationSettings.renotify.interval || '1m').time
: 1;
const reNotificationUnit = alertDef.notificationSettings?.renotify
? parseGoTime(alertDef.notificationSettings.renotify.interval || '30m').unit
? parseGoTime(alertDef.notificationSettings.renotify.interval || '1m').unit
: UniversalYAxisUnit.MINUTES;
return {

View File

@@ -8,10 +8,11 @@ import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
import AlertCondition from '../CreateAlertV2/AlertCondition';
import { buildInitialAlertDef } from '../CreateAlertV2/context/utils';
import EvaluationSettings from '../CreateAlertV2/EvaluationSettings';
import Footer from '../CreateAlertV2/Footer';
import NotificationSettings from '../CreateAlertV2/NotificationSettings';
import QuerySection from '../CreateAlertV2/QuerySection';
import { Spinner } from '../CreateAlertV2/utils';
import { showCondensedLayout, Spinner } from '../CreateAlertV2/utils';
interface EditAlertV2Props {
alertType?: AlertTypes;
@@ -32,12 +33,15 @@ function EditAlertV2({
useShareBuilderUrl({ defaultValue: currentQueryToRedirect });
const showCondensedLayoutFlag = showCondensedLayout();
return (
<>
<Spinner />
<div className="create-alert-v2-container">
<QuerySection />
<AlertCondition />
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
<NotificationSettings />
</div>
<Footer />

View File

@@ -1,14 +1,6 @@
/* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
Tag,
Typography,
} from 'antd';
import { Button, Dropdown, Flex, Input, MenuProps, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';
@@ -126,16 +118,12 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const newAlertMenuItems: MenuProps['items'] = [
{
key: 'new',
label: (
<span>
Try the new experience <Tag color="blue">Beta</Tag>
</span>
),
label: 'Try the new experience',
onClick: onClickNewAlertV2Handler,
},
{
key: 'classic',
label: 'Continue with the classic experience',
label: 'Continue with the current experience',
onClick: onClickNewClassicAlertHandler,
},
];

View File

@@ -88,7 +88,6 @@ function RoutingPolicies(): JSX.Element {
isRoutingPoliciesError={isErrorRoutingPolicies}
handlePolicyDetailsModalOpen={handlePolicyDetailsModalOpen}
handleDeleteModalOpen={handleDeleteModalOpen}
hasSearchTerm={(searchTerm?.length ?? 0) > 0}
/>
{policyDetailsModalState.isOpen && (
<RoutingPolicyDetails

View File

@@ -10,7 +10,6 @@ function RoutingPolicyList({
isRoutingPoliciesError,
handlePolicyDetailsModalOpen,
handleDeleteModalOpen,
hasSearchTerm,
}: RoutingPolicyListProps): JSX.Element {
const columns: TableProps<RoutingPolicy>['columns'] = [
{
@@ -26,7 +25,6 @@ function RoutingPolicyList({
},
];
/* eslint-disable no-nested-ternary */
const localeEmptyState = useMemo(
() => (
<div className="no-routing-policies-message-container">
@@ -43,23 +41,12 @@ function RoutingPolicyList({
<Typography.Text>
Something went wrong while fetching routing policies.
</Typography.Text>
) : hasSearchTerm ? (
<Typography.Text>No matching routing policies found.</Typography.Text>
) : (
<Typography.Text>
No routing policies yet,{' '}
<a
href="https://signoz.io/docs/alerts-management/routing-policy"
target="_blank"
rel="noopener noreferrer"
>
Learn more here
</a>
</Typography.Text>
<Typography.Text>No routing policies found.</Typography.Text>
)}
</div>
),
[isRoutingPoliciesError, hasSearchTerm],
[isRoutingPoliciesError],
);
return (

View File

@@ -28,7 +28,6 @@ describe('RoutingPoliciesList', () => {
isRoutingPoliciesError={useRoutingPolicesMockData.isErrorRoutingPolicies}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
hasSearchTerm={false}
/>,
);
@@ -52,7 +51,6 @@ describe('RoutingPoliciesList', () => {
isRoutingPoliciesError={false}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
hasSearchTerm={false}
/>,
);
// Check for loading spinner by class name
@@ -69,7 +67,6 @@ describe('RoutingPoliciesList', () => {
isRoutingPoliciesError
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
hasSearchTerm={false}
/>,
);
expect(
@@ -85,9 +82,8 @@ describe('RoutingPoliciesList', () => {
isRoutingPoliciesError={false}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
hasSearchTerm={false}
/>,
);
expect(screen.getByText('No routing policies yet,')).toBeInTheDocument();
expect(screen.getByText('No routing policies found.')).toBeInTheDocument();
});
});

View File

@@ -37,7 +37,6 @@ export interface RoutingPolicyListProps {
isRoutingPoliciesError: boolean;
handlePolicyDetailsModalOpen: HandlePolicyDetailsModalOpen;
handleDeleteModalOpen: HandleDeleteModalOpen;
hasSearchTerm: boolean;
}
export interface RoutingPolicyListItemProps {

View File

@@ -5,6 +5,10 @@ import { SuccessResponseV2 } from 'types/api';
import { RoutingPolicy } from './types';
export function showRoutingPoliciesPage(): boolean {
return localStorage.getItem('showRoutingPoliciesPage') === 'true';
}
export function mapApiResponseToRoutingPolicies(
response: SuccessResponseV2<GetRoutingPoliciesResponse>,
): RoutingPolicy[] {

View File

@@ -8,6 +8,7 @@ import ROUTES from 'constants/routes';
import AllAlertRules from 'container/ListAlertRules';
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
import RoutingPolicies from 'container/RoutingPolicies';
import { showRoutingPoliciesPage } from 'container/RoutingPolicies/utils';
import TriggeredAlerts from 'container/TriggeredAlerts';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -27,27 +28,36 @@ function AllAlertList(): JSX.Element {
const search = urlQuery.get('search');
const showRoutingPoliciesPageFlag = showRoutingPoliciesPage();
const configurationTab = useMemo(() => {
const tabs = [
{
label: 'Planned Downtime',
key: 'planned-downtime',
children: <PlannedDowntime />,
},
{
label: 'Routing Policies',
key: 'routing-policies',
children: <RoutingPolicies />,
},
];
if (showRoutingPoliciesPageFlag) {
const tabs = [
{
label: 'Planned Downtime',
key: 'planned-downtime',
children: <PlannedDowntime />,
},
{
label: 'Routing Policies',
key: 'routing-policies',
children: <RoutingPolicies />,
},
];
return (
<Tabs
className="configuration-tabs"
defaultActiveKey="planned-downtime"
items={tabs}
/>
);
}
return (
<Tabs
className="configuration-tabs"
defaultActiveKey="planned-downtime"
items={tabs}
/>
<div className="planned-downtime-container">
<PlannedDowntime />
</div>
);
}, []);
}, [showRoutingPoliciesPageFlag]);
const items: TabsProps['items'] = [
{

View File

@@ -1,11 +0,0 @@
# Copy this to .env and fill in your values
# Base URL for the application
SIGNOZ_E2E_BASE_URL=https://app.us.staging.signoz.cloud
# Test credentials
SIGNOZ_E2E_USERNAME=
SIGNOZ_E2E_PASSWORD=
# API endpoint (if needed)
SIGNOZ_E2E_API_URL=https://api.us.staging.signoz.cloud

View File

@@ -1,92 +0,0 @@
---
description: Use this agent when you need to create comprehensive test plan for a web application or website.
tools: ['edit/createFile', 'edit/createDirectory', 'search/fileSearch', 'search/textSearch', 'search/listDirectory', 'search/readFile', 'playwright-test/browser_click', 'playwright-test/browser_close', 'playwright-test/browser_console_messages', 'playwright-test/browser_drag', 'playwright-test/browser_evaluate', 'playwright-test/browser_file_upload', 'playwright-test/browser_handle_dialog', 'playwright-test/browser_hover', 'playwright-test/browser_navigate', 'playwright-test/browser_navigate_back', 'playwright-test/browser_network_requests', 'playwright-test/browser_press_key', 'playwright-test/browser_select_option', 'playwright-test/browser_snapshot', 'playwright-test/browser_take_screenshot', 'playwright-test/browser_type', 'playwright-test/browser_wait_for', 'playwright-test/planner_setup_page']
---
You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test
scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage
planning.
You will:
1. **Navigate and Explore**
- Invoke the `planner_setup_page` tool once to set up page before using any other tools
- Explore the browser snapshot
- Do not take screenshots unless absolutely necessary
- Use browser_* tools to navigate and discover interface
- Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality
2. **Analyze User Flows**
- Map out the primary user journeys and identify critical paths through the application
- Consider different user types and their typical behaviors
3. **Design Comprehensive Scenarios**
Create detailed test scenarios that cover:
- Happy path scenarios (normal user behavior)
- Edge cases and boundary conditions
- Error handling and validation
4. **Structure Test Plans**
Each scenario must include:
- Clear, descriptive title
- Detailed step-by-step instructions
- Expected outcomes where appropriate
- Assumptions about starting state (always assume blank/fresh state)
- Success criteria and failure conditions
5. **Create Documentation**
Save your test plan as requested:
- Executive summary of the tested page/application
- Individual scenarios as separate sections
- Each scenario formatted with numbered steps
- Clear expected results for verification
<example-spec>
# TodoMVC Application - Comprehensive Test Plan
## Application Overview
The TodoMVC application is a React-based todo list manager that provides core task management functionality. The
application features:
- **Task Management**: Add, edit, complete, and delete individual todos
- **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos
- **Filtering**: View todos by All, Active, or Completed status
- **URL Routing**: Support for direct navigation to filtered views via URLs
- **Counter Display**: Real-time count of active (incomplete) todos
- **Persistence**: State maintained during session (browser refresh behavior not tested)
## Test Scenarios
### 1. Adding New Todos
**Seed:** `tests/seed.spec.ts`
#### 1.1 Add Valid Todo
**Steps:**
1. Click in the "What needs to be done?" input field
2. Type "Buy groceries"
3. Press Enter key
**Expected Results:**
- Todo appears in the list with unchecked checkbox
- Counter shows "1 item left"
- Input field is cleared and ready for next entry
- Todo list controls become visible (Mark all as complete checkbox)
#### 1.2
...
</example-spec>
**Quality Standards**:
- Write steps that are specific enough for any tester to follow
- Include negative testing scenarios
- Ensure scenarios are independent and can be run in any order
**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and
professional formatting suitable for sharing with development and QA teams.
<example>Context: User wants to test a new e-commerce checkout flow. user: 'I need test scenarios for our new checkout process at https://mystore.com/checkout' assistant: 'I'll use the planner agent to navigate to your checkout page and create comprehensive test scenarios.' <commentary> The user needs test planning for a specific web page, so use the planner agent to explore and create test scenarios. </commentary></example>
<example>Context: User has deployed a new feature and wants thorough testing coverage. user: 'Can you help me test our new user dashboard at https://app.example.com/dashboard?' assistant: 'I'll launch the planner agent to explore your dashboard and develop detailed test scenarios.' <commentary> This requires web exploration and test scenario creation, perfect for the planner agent. </commentary></example>

View File

@@ -1,58 +0,0 @@
---
description: Use this agent when you need to create automated browser tests using Playwright.
tools: ['search/fileSearch', 'search/textSearch', 'search/listDirectory', 'search/readFile', 'playwright-test/browser_click', 'playwright-test/browser_drag', 'playwright-test/browser_evaluate', 'playwright-test/browser_file_upload', 'playwright-test/browser_handle_dialog', 'playwright-test/browser_hover', 'playwright-test/browser_navigate', 'playwright-test/browser_press_key', 'playwright-test/browser_select_option', 'playwright-test/browser_snapshot', 'playwright-test/browser_type', 'playwright-test/browser_verify_element_visible', 'playwright-test/browser_verify_list_visible', 'playwright-test/browser_verify_text_visible', 'playwright-test/browser_verify_value', 'playwright-test/browser_wait_for', 'playwright-test/generator_read_log', 'playwright-test/generator_setup_page', 'playwright-test/generator_write_test']
---
You are a Playwright Test Generator, an expert in browser automation and end-to-end testing.
Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate
application behavior.
# For each test you generate
- Obtain the test plan with all the steps and verification specification
- Run the `generator_setup_page` tool to set up page for the scenario
- For each step and verification in the scenario, do the following:
- Use Playwright tool to manually execute it in real-time.
- Use the step description as the intent for each Playwright tool call.
- Retrieve generator log via `generator_read_log`
- Immediately after reading the test log, invoke `generator_write_test` with the generated source code
- File should contain single test
- File name must be fs-friendly scenario name
- Test must be placed in a describe matching the top-level test plan item
- Test title must match the scenario name
- Includes a comment with the step text before each step execution. Do not duplicate comments if step requires
multiple actions.
- Always use best practices from the log when generating tests.
<example-generation>
For following plan:
```markdown file=specs/plan.md
### 1. Adding New Todos
**Seed:** `tests/seed.spec.ts`
#### 1.1 Add Valid Todo
**Steps:**
1. Click in the "What needs to be done?" input field
#### 1.2 Add Multiple Todos
...
```
Following file is generated:
```ts file=add-valid-todo.spec.ts
// spec: specs/plan.md
// seed: tests/seed.spec.ts
test.describe('Adding New Todos', () => {
test('Add Valid Todo', async { page } => {
// 1. Click in the "What needs to be done?" input field
await page.click(...);
...
});
});
```
</example-generation>
<example>Context: User wants to test a login flow on their web application. user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then verifies the dashboard page loads' assistant: 'I'll use the generator agent to create and validate this login test for you' <commentary> The user needs a specific browser automation test created, which is exactly what the generator agent is designed for. </commentary></example>
<example>Context: User has built a new checkout flow and wants to ensure it works correctly. user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the order?' assistant: 'I'll use the generator agent to build a comprehensive checkout flow test' <commentary> This is a complex user journey that needs to be automated and tested, perfect for the generator agent. </commentary></example>

View File

@@ -1,44 +0,0 @@
---
description: Use this agent when you need to debug and fix failing Playwright tests.
tools: ['edit/createFile', 'edit/createDirectory', 'edit/editFiles', 'search/fileSearch', 'search/textSearch', 'search/listDirectory', 'search/readFile', 'playwright-test/browser_console_messages', 'playwright-test/browser_evaluate', 'playwright-test/browser_generate_locator', 'playwright-test/browser_network_requests', 'playwright-test/browser_snapshot', 'playwright-test/test_debug', 'playwright-test/test_list', 'playwright-test/test_run']
---
You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and
resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix
broken Playwright tests using a methodical approach.
Your workflow:
1. **Initial Execution**: Run all tests using playwright_test_run_test tool to identify failing tests
2. **Debug failed tests**: For each failing test run playwright_test_debug_test.
3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to:
- Examine the error details
- Capture page snapshot to understand the context
- Analyze selectors, timing issues, or assertion failures
4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining:
- Element selectors that may have changed
- Timing and synchronization issues
- Data dependencies or test environment problems
- Application changes that broke test assumptions
5. **Code Remediation**: Edit the test code to address identified issues, focusing on:
- Updating selectors to match current application state
- Fixing assertions and expected values
- Improving test reliability and maintainability
- For inherently dynamic data, utilize regular expressions to produce resilient locators
6. **Verification**: Restart the test after each fix to validate the changes
7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly
Key principles:
- Be systematic and thorough in your debugging approach
- Document your findings and reasoning for each fix
- Prefer robust, maintainable solutions over quick hacks
- Use Playwright best practices for reliable test automation
- If multiple errors exist, fix them one at a time and retest
- Provide clear explanations of what was broken and how you fixed it
- You will continue this process until the test runs successfully without any failures or errors.
- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme()
so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead
of the expected behavior.
- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.
- Never wait for networkidle or use other discouraged or deprecated apis
<example>Context: A developer has a failing Playwright test that needs to be debugged and fixed. user: 'The login test is failing, can you fix it?' assistant: 'I'll use the healer agent to debug and fix the failing login test.' <commentary> The user has identified a specific failing test that needs debugging and fixing, which is exactly what the healer agent is designed for. </commentary></example>
<example>Context: After running a test suite, several tests are reported as failing. user: 'Test user-registration.spec.ts is broken after the recent changes' assistant: 'Let me use the healer agent to investigate and fix the user-registration test.' <commentary> A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-healer agent. </commentary></example>

View File

@@ -1,11 +0,0 @@
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/
.env
.env.local
dist/
*.log
yarn-error.log
.yarn/cache
.yarn/install-state.gz

View File

@@ -1,257 +0,0 @@
# SigNoz Frontend Automation
E2E tests for SigNoz frontend using Playwright.
## Setup
```bash
# Install dependencies
yarn install
# Install Playwright browsers
yarn install:browsers
# Copy .env.example to .env and configure
cp .env.example .env
# Edit .env with your test credentials
```
## Running Tests
```bash
# Run all tests
yarn test
# Run in UI mode (interactive)
yarn test:ui
# Run in headed mode (see browser)
yarn test:headed
# Debug mode
yarn test:debug
# Run specific browser
yarn test:chromium
yarn test:firefox
yarn test:webkit
# View HTML report
yarn report
# Generate tests with Codegen
yarn codegen
```
## Using Playwright Agents with Cursor
### 🎭 Planner - Create Test Plans
**In Cursor Chat:**
```
@.github/chatmodes/ 🎭 planner.chatmode.md @tests/seed.spec.ts
Follow the planner instructions to create a comprehensive test plan for [feature name]
Save to: specs/[feature-name].md
```
The planner will:
- Use your seed test for context
- Explore the application
- Create a detailed test plan in `specs/[feature].md`
### 🎭 Generator - Generate Tests
**In Cursor Chat:**
```
@.github/chatmodes/🎭 generator.chatmode.md @specs/[feature].md @tests/seed.spec.ts
Follow the generator instructions to generate Playwright tests from the test plan
Save to: tests/[feature]/
```
The generator will:
- Read the test plan
- Create test files in `tests/[feature]/`
- Use proper locators and assertions
- Follow seed.spec.ts patterns
### 🎭 Healer - Fix Failing Tests
**In Cursor Chat:**
```
@.github/chatmodes/🎭 healer.chatmode.md @tests/[feature]/[test].spec.ts
Follow the healer instructions to fix the failing test: [test name]
Error: [paste error message]
```
The healer will:
- Replay failing steps
- Update locators if needed
- Add proper waits
- Re-run until passing
## Directory Structure
```
frontend_automation/
├── .github/
│ └── chatmodes/ # Playwright agent definitions
│ ├── 🎭 planner.chatmode.md
│ ├── 🎭 generator.chatmode.md
│ └── 🎭 healer.chatmode.md
├── .vscode/
│ └── mcp.json # MCP server config
├── specs/ # Test plans (Markdown)
│ └── example-test-plan.md
├── tests/ # Test files (.spec.ts)
│ └── seed.spec.ts
├── utils/ # Utilities and helpers
│ └── login.util.ts
├── .env # Environment variables (git-ignored)
├── .env.example # Environment template
├── .gitignore
├── package.json
├── playwright.config.ts # Playwright configuration
├── tsconfig.json # TypeScript configuration
├── yarn.lock # Yarn lock file
└── README.md
```
## Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `SIGNOZ_E2E_BASE_URL` | Base URL of the application | `https://app.us.staging.signoz.cloud` |
| `SIGNOZ_E2E_USERNAME` | Test user email | `test@example.com` |
| `SIGNOZ_E2E_PASSWORD` | Test user password | `your-password` |
| `SIGNOZ_E2E_API_URL` | API endpoint (optional) | `https://api.us.staging.signoz.cloud` |
## Workflow Example
### Complete Test Creation Flow
```bash
# 1. In Cursor Chat, create test plan
@.github/chatmodes/ 🎭 planner.chatmode.md @tests/seed.spec.ts
Create a test plan for: routing policies feature
Save to: specs/routing-policies.md
# 2. Review the generated plan in specs/routing-policies.md
# Edit if needed
# 3. Generate tests from the plan
@.github/chatmodes/🎭 generator.chatmode.md @specs/routing-policies.md @tests/seed.spec.ts
Generate tests and save to: tests/routing-policies/
# 4. Run the tests
yarn test:ui
# 5. If any test fails, heal it
@.github/chatmodes/🎭 healer.chatmode.md @tests/routing-policies/[failing-test].spec.ts
Fix the failing test
# 6. Re-run to verify
yarn test
```
## CI/CD Integration
Example GitHub Actions workflow:
```yaml
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Install dependencies
run: yarn install
working-directory: frontend_automation
- name: Install Playwright browsers
run: yarn install:browsers
working-directory: frontend_automation
- name: Run tests
run: yarn test
working-directory: frontend_automation
env:
SIGNOZ_E2E_BASE_URL: ${{ secrets.E2E_BASE_URL }}
SIGNOZ_E2E_USERNAME: ${{ secrets.E2E_USERNAME }}
SIGNOZ_E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: frontend_automation/playwright-report/
retention-days: 30
```
## Best Practices
1. **Start with Seed Test** - Always reference `seed.spec.ts` for patterns
2. **Review Generated Plans** - Edit test plans before generating tests
3. **Use Semantic Locators** - Prefer `getByRole`, `getByLabel` over CSS selectors
4. **Keep Plans Updated** - Update `specs/` when features change
5. **Let Healer Work** - The healer can fix most locator and timing issues
6. **Write Descriptive Tests** - Use clear test names and comments
## Troubleshooting
### Tests Won't Run
- Check `.env` has correct credentials
- Verify `baseURL` is accessible
- Run `yarn test:debug` for detailed output
### Locators Failing
- Use the healer agent to fix them
- Or use Playwright Inspector: `yarn test:debug`
- Check if UI elements have changed
### Authentication Issues
- Verify `ensureLoggedIn()` function works
- Check credentials in `.env`
- Run seed test independently: `yarn test tests/seed.spec.ts`
### Agents Not Working in Cursor
- Ensure you're using `@` to reference chatmode files
- Include seed test in context
- Follow the agent instructions explicitly
## Resources
- [Playwright Documentation](https://playwright.dev)
- [Playwright Agents](https://playwright.dev/docs/test-agents)
- [Playwright Best Practices](https://playwright.dev/docs/best-practices)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
## Contributing
When adding new tests:
1. Create a test plan in `specs/` first
2. Use agents to generate tests
3. Review and refine generated code
4. Ensure tests follow existing patterns
5. Add proper documentation
## License
MIT

View File

@@ -1,36 +0,0 @@
{
"name": "signoz-frontend-automation",
"version": "1.0.0",
"description": "E2E tests for SigNoz frontend with Playwright",
"main": "index.js",
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed",
"test:debug": "playwright test --debug",
"test:chromium": "playwright test --project=chromium",
"test:firefox": "playwright test --project=firefox",
"test:webkit": "playwright test --project=webkit",
"report": "playwright show-report",
"codegen": "playwright codegen",
"install:browsers": "playwright install"
},
"keywords": [
"playwright",
"e2e",
"testing",
"signoz"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@playwright/test": "^1.57.0-alpha-2025-10-09",
"@types/node": "^20.0.0",
"dotenv": "^16.0.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=18.0.0",
"yarn": ">=1.22.0"
}
}

View File

@@ -1,57 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
// Load environment variables
dotenv.config({ path: path.resolve(__dirname, ".env") });
export default defineConfig({
testDir: "./tests",
// Run tests in parallel
fullyParallel: true,
// Fail the build on CI if you accidentally left test.only
forbidOnly: !!process.env.CI,
// Retry on CI only
retries: process.env.CI ? 2 : 0,
// Workers
workers: process.env.CI ? 2 : undefined,
// Reporter
reporter: [
["html"],
["json", { outputFile: "test-results/results.json" }],
["list"],
],
// Shared settings
use: {
baseURL:
process.env.SIGNOZ_E2E_BASE_URL || "https://app.us.staging.signoz.cloud",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
colorScheme: "dark",
locale: "en-US",
viewport: { width: 1280, height: 720 },
},
// Configure projects for multiple browsers
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
],
});

View File

@@ -1,76 +0,0 @@
# Example Feature - Test Plan Template
## Application Overview
[Describe the feature/module being tested. Include key functionality, user flows, and important business logic.]
Example:
> The Routing Policies feature allows users to create, edit, and manage alert routing configurations. Users can define rules that determine how alerts are routed to different channels based on conditions like severity, labels, or alert names.
## Test Scenarios
### 1. [Main Scenario Category]
**Seed:** `tests/seed.spec.ts`
#### 1.1 [Specific Test Case]
**Pre-conditions:**
- User is logged in (handled by seed test)
- [Any other specific setup needed]
**Steps:**
1. Navigate to [specific page/section]
2. Click on [element description]
3. Fill in [field] with "[test data]"
4. Click [button/action]
5. Verify [expected outcome]
**Expected Results:**
- [Expected UI change or behavior]
- [Expected data state]
- [Expected navigation or feedback]
**Data:**
- Input field: "test value"
- Select option: "option name"
#### 1.2 [Another Test Case]
**Steps:**
1. ...
**Expected Results:**
- ...
### 2. [Another Scenario Category]
#### 2.1 [Test Case]
**Steps:**
1. ...
**Expected Results:**
- ...
## Edge Cases
### 3. Error Handling
#### 3.1 Invalid Input
**Steps:**
1. Enter invalid data
2. Attempt to submit
**Expected Results:**
- Error message displayed
- Form not submitted
- User remains on page
## Notes
- [Any special considerations]
- [Known limitations]
- [Areas requiring manual verification]

View File

@@ -1,19 +0,0 @@
import { test, expect } from "@playwright/test";
import { ensureLoggedIn } from "../utils/login.util";
/**
* Seed test for Playwright Agents
*
* This test serves as:
* 1. A foundation for all agent-generated tests
* 2. An example of test structure and patterns
* 3. Initial setup for authentication
*/
test("seed", async ({ page }) => {
// Login to the application
await ensureLoggedIn(page);
// Verify we're on the home page
await expect(page).toHaveURL(/.*\/home/);
await expect(page.getByText("Hello there, Welcome to your")).toBeVisible();
});

View File

@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"types": ["node", "@playwright/test"],
"baseUrl": ".",
"paths": {
"@tests/*": ["tests/*"],
"@utils/*": ["utils/*"],
"@specs/*": ["specs/*"]
},
"outDir": "./dist",
"rootDir": "."
},
"include": ["tests/**/*.ts", "utils/**/*.ts", "playwright.config.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,33 +0,0 @@
import { Page } from "@playwright/test";
// Read credentials from environment variables
const username = process.env.SIGNOZ_E2E_USERNAME;
const password = process.env.SIGNOZ_E2E_PASSWORD;
/**
* Ensures the user is logged in. If not, performs the login steps.
*/
export async function ensureLoggedIn(page: Page): Promise<void> {
// If already in home page, return
if (page.url().includes("/home")) {
return;
}
if (!username || !password) {
throw new Error(
"SIGNOZ_E2E_USERNAME and SIGNOZ_E2E_PASSWORD environment variables must be set."
);
}
await page.goto("/login");
await page.getByTestId("email").click();
await page.getByTestId("email").fill(username);
await page.getByTestId("initiate_login").click();
await page.getByTestId("password").click();
await page.getByTestId("password").fill(password);
await page.getByRole("button", { name: "Login" }).click();
await page
.getByText("Hello there, Welcome to your")
.waitFor({ state: "visible" });
}

View File

@@ -1,51 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@playwright/test@^1.57.0-alpha-2025-10-11":
version "1.57.0-alpha-2025-10-11"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.57.0-alpha-2025-10-11.tgz#75f6fac2f98fcff6e4bae1c907b48ad0b1a33bea"
integrity sha512-xqp2RNcLCPSUAYCrP3+rYZ4LFlESvWqjjpFegjNbun7wLcGvUt9Mh+RHBvgeZAhMxxuVde78XO9Y888UYFH9ew==
dependencies:
playwright "1.57.0-alpha-2025-10-11"
"@types/node@^24.7.1":
version "24.7.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.7.1.tgz#3f0b17eddcd965c9e337af22459b04bafbf96e5e"
integrity sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==
dependencies:
undici-types "~7.14.0"
dotenv@^17.2.3:
version "17.2.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2"
integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==
fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
playwright-core@1.57.0-alpha-2025-10-11:
version "1.57.0-alpha-2025-10-11"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.57.0-alpha-2025-10-11.tgz#b444b75542a43bc354e512d076344d9258997e9c"
integrity sha512-X6KAunryZlslAdEdlN5gIIP3sFU6Uot3vzLoGCZ9SNv0JvXd6e2g7ArjnpOQld36yKszq8J+wQJRlIvdXkIvRw==
playwright@1.57.0-alpha-2025-10-11:
version "1.57.0-alpha-2025-10-11"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.57.0-alpha-2025-10-11.tgz#af67a28d64b39fe57eea454b2b312e98bfff02a5"
integrity sha512-a80kAd59up/kURcKE7THLzx3lN6a1G9RhsgP9ZfLGL7WtnOhOdRLxbHwmjWUG11ybEDeYNpj1qwT02MT4R+rew==
dependencies:
playwright-core "1.57.0-alpha-2025-10-11"
optionalDependencies:
fsevents "2.3.2"
typescript@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
undici-types@~7.14.0:
version "7.14.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840"
integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==

4
go.mod
View File

@@ -127,7 +127,7 @@ require (
github.com/elastic/lunes v0.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/expr-lang/expr v1.17.5
github.com/expr-lang/expr v1.17.5 // indirect
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -338,5 +338,3 @@ 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

4
go.sum
View File

@@ -102,8 +102,6 @@ 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=
@@ -250,6 +248,8 @@ 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=

View File

@@ -3,8 +3,6 @@ package alertmanager
import (
"context"
amConfig "github.com/prometheus/alertmanager/config"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/statsreporter"
@@ -28,7 +26,7 @@ type Alertmanager interface {
TestReceiver(context.Context, string, alertmanagertypes.Receiver) error
// TestAlert sends an alert to a list of receivers.
TestAlert(ctx context.Context, orgID string, ruleID string, receiversMap map[*alertmanagertypes.PostableAlert][]string) error
TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error
// ListChannels lists all channels for the organization.
ListChannels(context.Context, string) ([]*alertmanagertypes.Channel, error)
@@ -61,19 +59,6 @@ type Alertmanager interface {
DeleteNotificationConfig(ctx context.Context, orgID valuer.UUID, ruleId string) error
// Notification Policy CRUD
CreateRoutePolicy(ctx context.Context, route *alertmanagertypes.PostableRoutePolicy) (*alertmanagertypes.GettableRoutePolicy, error)
CreateRoutePolicies(ctx context.Context, routeRequests []*alertmanagertypes.PostableRoutePolicy) ([]*alertmanagertypes.GettableRoutePolicy, error)
GetRoutePolicyByID(ctx context.Context, routeID string) (*alertmanagertypes.GettableRoutePolicy, error)
GetAllRoutePolicies(ctx context.Context) ([]*alertmanagertypes.GettableRoutePolicy, error)
UpdateRoutePolicyByID(ctx context.Context, routeID string, route *alertmanagertypes.PostableRoutePolicy) (*alertmanagertypes.GettableRoutePolicy, error)
DeleteRoutePolicyByID(ctx context.Context, routeID string) error
DeleteAllRoutePoliciesByRuleId(ctx context.Context, ruleId string) error
UpdateAllRoutePoliciesByRuleId(ctx context.Context, ruleId string, routes []*alertmanagertypes.PostableRoutePolicy) error
CreateInhibitRules(ctx context.Context, orgID valuer.UUID, rules []amConfig.InhibitRule) error
DeleteAllInhibitRulesByRuleId(ctx context.Context, orgID valuer.UUID, ruleId string) error
// Collects stats for the organization.
statsreporter.StatsCollector
}

View File

@@ -10,17 +10,19 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/alertmanager/provider"
"github.com/prometheus/alertmanager/store"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
)
const (
noDataLabel = model.LabelName("nodata")
)
// Dispatcher sorts incoming alerts into aggregation groups and
// assigns the correct notifiers to each.
type Dispatcher struct {
@@ -44,7 +46,6 @@ type Dispatcher struct {
logger *slog.Logger
notificationManager nfmanager.NotificationManager
orgID string
receiverRoutes map[string]*dispatch.Route
}
// We use the upstream Limits interface from Prometheus
@@ -89,7 +90,6 @@ func (d *Dispatcher) Run() {
d.mtx.Lock()
d.aggrGroupsPerRoute = map[*dispatch.Route]map[model.Fingerprint]*aggrGroup{}
d.receiverRoutes = map[string]*dispatch.Route{}
d.aggrGroupsNum = 0
d.metrics.aggrGroups.Set(0)
d.ctx, d.cancel = context.WithCancel(context.Background())
@@ -125,14 +125,8 @@ func (d *Dispatcher) run(it provider.AlertIterator) {
}
now := time.Now()
channels, err := d.notificationManager.Match(d.ctx, d.orgID, getRuleIDFromAlert(alert), alert.Labels)
if err != nil {
d.logger.ErrorContext(d.ctx, "Error on alert match", "err", err)
continue
}
for _, channel := range channels {
route := d.getOrCreateRoute(channel)
d.processAlert(alert, route)
for _, r := range d.route.Match(alert.Labels) {
d.processAlert(alert, r)
}
d.metrics.processingDuration.Observe(time.Since(now).Seconds())
@@ -272,7 +266,6 @@ type notifyFunc func(context.Context, ...*types.Alert) bool
// processAlert determines in which aggregation group the alert falls
// and inserts it.
// no data alert will only have ruleId and no data label
func (d *Dispatcher) processAlert(alert *types.Alert, route *dispatch.Route) {
ruleId := getRuleIDFromAlert(alert)
config, err := d.notificationManager.GetNotificationConfig(d.orgID, ruleId)
@@ -280,14 +273,8 @@ func (d *Dispatcher) processAlert(alert *types.Alert, route *dispatch.Route) {
d.logger.ErrorContext(d.ctx, "error getting alert notification config", "rule_id", ruleId, "error", err)
return
}
renotifyInterval := config.Renotify.RenotifyInterval
groupLabels := getGroupLabels(alert, config.NotificationGroup, config.GroupByAll)
if alertmanagertypes.NoDataAlert(alert) {
renotifyInterval = config.Renotify.NoDataInterval
groupLabels[alertmanagertypes.NoDataLabel] = alert.Labels[alertmanagertypes.NoDataLabel] //to create new group key for no data alerts
}
groupLabels := getGroupLabels(alert, config.NotificationGroup)
fp := groupLabels.Fingerprint()
@@ -312,6 +299,12 @@ func (d *Dispatcher) processAlert(alert *types.Alert, route *dispatch.Route) {
d.logger.ErrorContext(d.ctx, "Too many aggregation groups, cannot create new group for alert", "groups", d.aggrGroupsNum, "limit", limit, "alert", alert.Name())
return
}
renotifyInterval := config.Renotify.RenotifyInterval
if noDataAlert(alert) {
renotifyInterval = config.Renotify.NoDataInterval
groupLabels[noDataLabel] = alert.Labels[noDataLabel]
}
ag = newAggrGroup(d.ctx, groupLabels, route, d.timeout, d.logger, renotifyInterval)
@@ -550,35 +543,21 @@ func deepCopyRouteOpts(opts dispatch.RouteOpts, renotify time.Duration) dispatch
return newOpts
}
func getGroupLabels(alert *types.Alert, groups map[model.LabelName]struct{}, groupByAll bool) model.LabelSet {
func getGroupLabels(alert *types.Alert, groups map[model.LabelName]struct{}) model.LabelSet {
groupLabels := model.LabelSet{}
for ln, lv := range alert.Labels {
if _, ok := groups[ln]; ok || groupByAll {
if _, ok := groups[ln]; ok {
groupLabels[ln] = lv
}
}
return groupLabels
}
func (d *Dispatcher) getOrCreateRoute(receiver string) *dispatch.Route {
d.mtx.Lock()
defer d.mtx.Unlock()
if route, exists := d.receiverRoutes[receiver]; exists {
return route
func noDataAlert(alert *types.Alert) bool {
if _, ok := alert.Labels[noDataLabel]; ok {
return true
} else {
return false
}
route := &dispatch.Route{
RouteOpts: dispatch.RouteOpts{
Receiver: receiver,
GroupWait: 30 * time.Second,
GroupInterval: 5 * time.Minute,
GroupByAll: false,
},
Matchers: labels.Matchers{{
Name: "__receiver__",
Value: receiver,
Type: labels.MatchEqual,
}},
}
d.receiverRoutes[receiver] = route
return route
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,6 @@ package alertmanagerserver
import (
"context"
"fmt"
"github.com/prometheus/alertmanager/types"
"golang.org/x/sync/errgroup"
"log/slog"
"strings"
"sync"
@@ -324,104 +321,39 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
}
func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error {
testAlert := alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now())
return alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, testAlert.Labels, testAlert)
return alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now()))
}
func (server *Server) TestAlert(ctx context.Context, receiversMap map[*alertmanagertypes.PostableAlert][]string, config *alertmanagertypes.NotificationConfig) error {
if len(receiversMap) == 0 {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput,
"expected at least 1 alert, got 0")
}
postableAlerts := make(alertmanagertypes.PostableAlerts, 0, len(receiversMap))
for alert := range receiversMap {
postableAlerts = append(postableAlerts, alert)
}
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(
postableAlerts,
time.Duration(server.srvConfig.Global.ResolveTimeout),
time.Now(),
)
func (server *Server) TestAlert(ctx context.Context, postableAlert *alertmanagertypes.PostableAlert, receivers []string) error {
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(alertmanagertypes.PostableAlerts{postableAlert}, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now())
if err != nil {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput,
"failed to construct alerts from postable alerts: %v", err)
return errors.Join(err...)
}
type alertGroup struct {
groupLabels model.LabelSet
alerts []*types.Alert
receivers map[string]struct{}
if len(alerts) != 1 {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "expected 1 alert, got %d", len(alerts))
}
groupMap := make(map[model.Fingerprint]*alertGroup)
for i, alert := range alerts {
labels := getGroupLabels(alert, config.NotificationGroup, config.GroupByAll)
fp := labels.Fingerprint()
postableAlert := postableAlerts[i]
alertReceivers := receiversMap[postableAlert]
if group, exists := groupMap[fp]; exists {
group.alerts = append(group.alerts, alert)
for _, r := range alertReceivers {
group.receivers[r] = struct{}{}
ch := make(chan error, len(receivers))
for _, receiverName := range receivers {
go func(receiverName string) {
receiver, err := server.alertmanagerConfig.GetReceiver(receiverName)
if err != nil {
ch <- err
return
}
} else {
receiverSet := make(map[string]struct{})
for _, r := range alertReceivers {
receiverSet[r] = struct{}{}
}
groupMap[fp] = &alertGroup{
groupLabels: labels,
alerts: []*types.Alert{alert},
receivers: receiverSet,
}
}
ch <- alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, alerts[0])
}(receiverName)
}
var mu sync.Mutex
var errs []error
g, gCtx := errgroup.WithContext(ctx)
for _, group := range groupMap {
for receiverName := range group.receivers {
group := group
receiverName := receiverName
g.Go(func() error {
receiver, err := server.alertmanagerConfig.GetReceiver(receiverName)
if err != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("failed to get receiver %q: %w", receiverName, err))
mu.Unlock()
return nil // Return nil to continue processing other goroutines
}
err = alertmanagertypes.TestReceiver(
gCtx,
receiver,
alertmanagernotify.NewReceiverIntegrations,
server.alertmanagerConfig,
server.tmpl,
server.logger,
group.groupLabels,
group.alerts...,
)
if err != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("receiver %q test failed: %w", receiverName, err))
mu.Unlock()
}
return nil // Return nil to continue processing other goroutines
})
for i := 0; i < len(receivers); i++ {
if err := <-ch; err != nil {
errs = append(errs, err)
}
}
_ = g.Wait()
if len(errs) > 0 {
if errs != nil {
return errors.Join(errs...)
}

View File

@@ -1,223 +0,0 @@
package alertmanagerserver
import (
"context"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes/alertmanagertypestest"
"github.com/prometheus/alertmanager/dispatch"
"io"
"log/slog"
"net/http"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/nfroutingstoretest"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/go-openapi/strfmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
)
func TestEndToEndAlertManagerFlow(t *testing.T) {
ctx := context.Background()
providerSettings := instrumentationtest.New().ToProviderSettings()
store := nfroutingstoretest.NewMockSQLRouteStore()
store.MatchExpectationsInOrder(false)
notificationManager, err := rulebasednotification.New(ctx, providerSettings, nfmanager.Config{}, store)
require.NoError(t, err)
orgID := "test-org"
routes := []*alertmanagertypes.RoutePolicy{
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
Expression: `ruleId == "high-cpu-usage" && severity == "critical"`,
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Name: "high-cpu-usage",
Description: "High CPU critical alerts to webhook",
Enabled: true,
OrgID: orgID,
Channels: []string{"webhook"},
},
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
Expression: `ruleId == "high-cpu-usage" && severity == "warning"`,
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Name: "high-cpu-usage",
Description: "High CPU warning alerts to webhook",
Enabled: true,
OrgID: orgID,
Channels: []string{"webhook"},
},
}
store.ExpectCreateBatch(routes)
err = notificationManager.CreateRoutePolicies(ctx, orgID, routes)
require.NoError(t, err)
for range routes {
ruleID := "high-cpu-usage"
store.ExpectGetAllByName(orgID, ruleID, routes)
store.ExpectGetAllByName(orgID, ruleID, routes)
}
notifConfig := alertmanagertypes.NotificationConfig{
NotificationGroup: map[model.LabelName]struct{}{
model.LabelName("cluster"): {},
model.LabelName("instance"): {},
},
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 5 * time.Minute,
},
UsePolicy: false,
}
err = notificationManager.SetNotificationConfig(orgID, "high-cpu-usage", &notifConfig)
require.NoError(t, err)
srvCfg := NewConfig()
stateStore := alertmanagertypestest.NewStateStore()
registry := prometheus.NewRegistry()
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
server, err := New(context.Background(), logger, registry, srvCfg, orgID, stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, orgID)
require.NoError(t, err)
err = server.SetConfig(ctx, amConfig)
require.NoError(t, err)
// Create test alerts
now := time.Now()
testAlerts := []*alertmanagertypes.PostableAlert{
{
Alert: alertmanagertypes.AlertModel{
Labels: map[string]string{
"ruleId": "high-cpu-usage",
"severity": "critical",
"cluster": "prod-cluster",
"instance": "server-01",
"alertname": "HighCPUUsage",
},
},
Annotations: map[string]string{
"summary": "High CPU usage detected",
"description": "CPU usage is above 90% for 5 minutes",
},
StartsAt: strfmt.DateTime(now.Add(-5 * time.Minute)),
EndsAt: strfmt.DateTime(time.Time{}), // Active alert
},
{
Alert: alertmanagertypes.AlertModel{
Labels: map[string]string{
"ruleId": "high-cpu-usage",
"severity": "warning",
"cluster": "prod-cluster",
"instance": "server-02",
"alertname": "HighCPUUsage",
},
},
Annotations: map[string]string{
"summary": "Moderate CPU usage detected",
"description": "CPU usage is above 70% for 10 minutes",
},
StartsAt: strfmt.DateTime(now.Add(-10 * time.Minute)),
EndsAt: strfmt.DateTime(time.Time{}), // Active alert
},
{
Alert: alertmanagertypes.AlertModel{
Labels: map[string]string{
"ruleId": "high-cpu-usage",
"severity": "critical",
"cluster": "prod-cluster",
"instance": "server-03",
"alertname": "HighCPUUsage",
},
},
Annotations: map[string]string{
"summary": "High CPU usage detected on server-03",
"description": "CPU usage is above 95% for 3 minutes",
},
StartsAt: strfmt.DateTime(now.Add(-3 * time.Minute)),
EndsAt: strfmt.DateTime(time.Time{}), // Active alert
},
}
err = server.PutAlerts(ctx, testAlerts)
require.NoError(t, err)
time.Sleep(2 * time.Second)
t.Run("verify_alerts_processed", func(t *testing.T) {
dummyRequest, err := http.NewRequest(http.MethodGet, "/alerts", nil)
require.NoError(t, err)
params, err := alertmanagertypes.NewGettableAlertsParams(dummyRequest)
require.NoError(t, err)
alerts, err := server.GetAlerts(context.Background(), params)
require.NoError(t, err)
require.Len(t, alerts, 3, "Expected 3 active alerts")
for _, alert := range alerts {
require.Equal(t, "high-cpu-usage", alert.Alert.Labels["ruleId"])
require.NotEmpty(t, alert.Alert.Labels["severity"])
require.Contains(t, []string{"critical", "warning"}, alert.Alert.Labels["severity"])
require.Equal(t, "prod-cluster", alert.Alert.Labels["cluster"])
require.NotEmpty(t, alert.Alert.Labels["instance"])
}
criticalAlerts := 0
warningAlerts := 0
for _, alert := range alerts {
if alert.Alert.Labels["severity"] == "critical" {
criticalAlerts++
} else if alert.Alert.Labels["severity"] == "warning" {
warningAlerts++
}
}
require.Equal(t, 2, criticalAlerts, "Expected 2 critical alerts")
require.Equal(t, 1, warningAlerts, "Expected 1 warning alert")
})
t.Run("verify_notification_routing", func(t *testing.T) {
notifConfig, err := notificationManager.GetNotificationConfig(orgID, "high-cpu-usage")
require.NoError(t, err)
require.NotNil(t, notifConfig)
require.Equal(t, 5*time.Minute, notifConfig.Renotify.RenotifyInterval)
require.Contains(t, notifConfig.NotificationGroup, model.LabelName("ruleId"))
require.Contains(t, notifConfig.NotificationGroup, model.LabelName("cluster"))
require.Contains(t, notifConfig.NotificationGroup, model.LabelName("instance"))
})
t.Run("verify_alert_groups_and_stages", func(t *testing.T) {
time.Sleep(2 * time.Second)
alertGroups, _ := server.dispatcher.Groups(
func(route *dispatch.Route) bool { return true }, // Accept all routes
func(alert *alertmanagertypes.Alert, now time.Time) bool { return true }, // Accept all alerts
)
require.Len(t, alertGroups, 3)
require.NotEmpty(t, alertGroups, "Should have alert groups created by dispatcher")
totalAlerts := 0
for _, group := range alertGroups {
totalAlerts += len(group.Alerts)
}
require.Equal(t, 3, totalAlerts, "Should have 3 alerts total across all groups")
require.Equal(t, "{__receiver__=\"webhook\"}:{cluster=\"prod-cluster\", instance=\"server-01\", ruleId=\"high-cpu-usage\"}", alertGroups[0].GroupKey)
require.Equal(t, "{__receiver__=\"webhook\"}:{cluster=\"prod-cluster\", instance=\"server-02\", ruleId=\"high-cpu-usage\"}", alertGroups[1].GroupKey)
require.Equal(t, "{__receiver__=\"webhook\"}:{cluster=\"prod-cluster\", instance=\"server-03\", ruleId=\"high-cpu-usage\"}", alertGroups[2].GroupKey)
})
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/client_golang/prometheus"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -128,189 +127,3 @@ func TestServerPutAlerts(t *testing.T) {
assert.Equal(t, gettableAlerts[0].Alert.Labels["alertname"], "test-alert")
assert.NoError(t, server.Stop(context.Background()))
}
func TestServerTestAlert(t *testing.T) {
stateStore := alertmanagertypestest.NewStateStore()
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
require.NoError(t, err)
webhook1Listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
webhook2Listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
requestCount1 := 0
requestCount2 := 0
webhook1Server := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount1++
w.WriteHeader(http.StatusOK)
}),
}
webhook2Server := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount2++
w.WriteHeader(http.StatusOK)
}),
}
go func() {
_ = webhook1Server.Serve(webhook1Listener)
}()
go func() {
_ = webhook2Server.Serve(webhook2Listener)
}()
webhook1URL, err := url.Parse("http://" + webhook1Listener.Addr().String() + "/webhook")
require.NoError(t, err)
webhook2URL, err := url.Parse("http://" + webhook2Listener.Addr().String() + "/webhook")
require.NoError(t, err)
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "receiver-1",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: &config.SecretURL{URL: webhook1URL},
},
},
}))
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "receiver-2",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: &config.SecretURL{URL: webhook2URL},
},
},
}))
require.NoError(t, server.SetConfig(context.Background(), amConfig))
defer func() {
_ = server.Stop(context.Background())
_ = webhook1Server.Close()
_ = webhook2Server.Close()
}()
// Test with multiple alerts going to different receivers
alert1 := &alertmanagertypes.PostableAlert{
Annotations: models.LabelSet{"alertname": "test-alert-1"},
StartsAt: strfmt.DateTime(time.Now()),
Alert: models.Alert{
Labels: models.LabelSet{"alertname": "test-alert-1", "severity": "critical"},
},
}
alert2 := &alertmanagertypes.PostableAlert{
Annotations: models.LabelSet{"alertname": "test-alert-2"},
StartsAt: strfmt.DateTime(time.Now()),
Alert: models.Alert{
Labels: models.LabelSet{"alertname": "test-alert-2", "severity": "warning"},
},
}
receiversMap := map[*alertmanagertypes.PostableAlert][]string{
alert1: {"receiver-1", "receiver-2"},
alert2: {"receiver-2"},
}
config := &alertmanagertypes.NotificationConfig{
NotificationGroup: make(map[model.LabelName]struct{}),
GroupByAll: false,
}
err = server.TestAlert(context.Background(), receiversMap, config)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
assert.Greater(t, requestCount1, 0, "receiver-1 should have received at least one request")
assert.Greater(t, requestCount2, 0, "receiver-2 should have received at least one request")
}
func TestServerTestAlertContinuesOnFailure(t *testing.T) {
stateStore := alertmanagertypestest.NewStateStore()
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
require.NoError(t, err)
// Create one working webhook and one failing receiver (non-existent)
webhookListener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
requestCount := 0
webhookServer := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount++
w.WriteHeader(http.StatusOK)
}),
}
go func() {
_ = webhookServer.Serve(webhookListener)
}()
webhookURL, err := url.Parse("http://" + webhookListener.Addr().String() + "/webhook")
require.NoError(t, err)
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "working-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: &config.SecretURL{URL: webhookURL},
},
},
}))
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "failing-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: &config.SecretURL{URL: &url.URL{Scheme: "http", Host: "localhost:1", Path: "/webhook"}},
},
},
}))
require.NoError(t, server.SetConfig(context.Background(), amConfig))
defer func() {
_ = server.Stop(context.Background())
_ = webhookServer.Close()
}()
alert := &alertmanagertypes.PostableAlert{
Annotations: models.LabelSet{"alertname": "test-alert"},
StartsAt: strfmt.DateTime(time.Now()),
Alert: models.Alert{
Labels: models.LabelSet{"alertname": "test-alert"},
},
}
receiversMap := map[*alertmanagertypes.PostableAlert][]string{
alert: {"working-receiver", "failing-receiver"},
}
config := &alertmanagertypes.NotificationConfig{
NotificationGroup: make(map[model.LabelName]struct{}),
GroupByAll: false,
}
err = server.TestAlert(context.Background(), receiversMap, config)
assert.Error(t, err)
time.Sleep(100 * time.Millisecond)
assert.Greater(t, requestCount, 0, "working-receiver should have received at least one request even though failing-receiver failed")
}

View File

@@ -2,7 +2,6 @@ package alertmanager
import (
"context"
"encoding/json"
"io"
"net/http"
"time"
@@ -274,128 +273,3 @@ func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
body, err := io.ReadAll(req.Body)
if err != nil {
render.Error(rw, err)
return
}
defer req.Body.Close()
var policy alertmanagertypes.PostableRoutePolicy
err = json.Unmarshal(body, &policy)
if err != nil {
render.Error(rw, err)
return
}
policy.ExpressionKind = alertmanagertypes.PolicyBasedExpression
// Validate the postable route
if err := policy.Validate(); err != nil {
render.Error(rw, err)
return
}
result, err := api.alertmanager.CreateRoutePolicy(ctx, &policy)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, result)
}
func (api *API) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
policies, err := api.alertmanager.GetAllRoutePolicies(ctx)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, policies)
}
func (api *API) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
vars := mux.Vars(req)
policyID := vars["id"]
if policyID == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "policy ID is required"))
return
}
policy, err := api.alertmanager.GetRoutePolicyByID(ctx, policyID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, policy)
}
func (api *API) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
vars := mux.Vars(req)
policyID := vars["id"]
if policyID == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "policy ID is required"))
return
}
err := api.alertmanager.DeleteRoutePolicyByID(ctx, policyID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
vars := mux.Vars(req)
policyID := vars["id"]
if policyID == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "policy ID is required"))
return
}
body, err := io.ReadAll(req.Body)
if err != nil {
render.Error(rw, err)
return
}
defer req.Body.Close()
var policy alertmanagertypes.PostableRoutePolicy
err = json.Unmarshal(body, &policy)
if err != nil {
render.Error(rw, err)
return
}
policy.ExpressionKind = alertmanagertypes.PolicyBasedExpression
// Validate the postable route
if err := policy.Validate(); err != nil {
render.Error(rw, err)
return
}
result, err := api.alertmanager.UpdateRoutePolicyByID(ctx, policyID, &policy)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, result)
}

View File

@@ -1,29 +1,20 @@
package nfmanagertest
import (
"context"
"fmt"
"strings"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/common/model"
)
// MockNotificationManager is a simple mock implementation of NotificationManager
type MockNotificationManager struct {
configs map[string]*alertmanagertypes.NotificationConfig
routes map[string]*alertmanagertypes.RoutePolicy
routesByName map[string][]*alertmanagertypes.RoutePolicy
errors map[string]error
configs map[string]*alertmanagertypes.NotificationConfig
errors map[string]error
}
// NewMock creates a new mock notification manager
func NewMock() *MockNotificationManager {
return &MockNotificationManager{
configs: make(map[string]*alertmanagertypes.NotificationConfig),
routes: make(map[string]*alertmanagertypes.RoutePolicy),
routesByName: make(map[string][]*alertmanagertypes.RoutePolicy),
errors: make(map[string]error),
configs: make(map[string]*alertmanagertypes.NotificationConfig),
errors: make(map[string]error),
}
}
@@ -74,8 +65,6 @@ func (m *MockNotificationManager) SetMockError(orgID, ruleID string, err error)
func (m *MockNotificationManager) ClearMockData() {
m.configs = make(map[string]*alertmanagertypes.NotificationConfig)
m.routes = make(map[string]*alertmanagertypes.RoutePolicy)
m.routesByName = make(map[string][]*alertmanagertypes.RoutePolicy)
m.errors = make(map[string]error)
}
@@ -84,241 +73,3 @@ func (m *MockNotificationManager) HasConfig(orgID, ruleID string) bool {
_, exists := m.configs[key]
return exists
}
// Route Policy CRUD
func (m *MockNotificationManager) CreateRoutePolicy(ctx context.Context, orgID string, route *alertmanagertypes.RoutePolicy) error {
key := getKey(orgID, "create_route")
if err := m.errors[key]; err != nil {
return err
}
if route == nil {
return fmt.Errorf("route cannot be nil")
}
if err := route.Validate(); err != nil {
return err
}
routeKey := getKey(orgID, route.ID.StringValue())
m.routes[routeKey] = route
nameKey := getKey(orgID, route.Name)
m.routesByName[nameKey] = append(m.routesByName[nameKey], route)
return nil
}
func (m *MockNotificationManager) CreateRoutePolicies(ctx context.Context, orgID string, routes []*alertmanagertypes.RoutePolicy) error {
key := getKey(orgID, "create_routes")
if err := m.errors[key]; err != nil {
return err
}
if len(routes) == 0 {
return fmt.Errorf("routes cannot be empty")
}
for i, route := range routes {
if route == nil {
return fmt.Errorf("route at index %d cannot be nil", i)
}
if err := route.Validate(); err != nil {
return fmt.Errorf("route at index %d: %s", i, err.Error())
}
}
for _, route := range routes {
if err := m.CreateRoutePolicy(ctx, orgID, route); err != nil {
return err
}
}
return nil
}
func (m *MockNotificationManager) GetRoutePolicyByID(ctx context.Context, orgID string, routeID string) (*alertmanagertypes.RoutePolicy, error) {
key := getKey(orgID, "get_route")
if err := m.errors[key]; err != nil {
return nil, err
}
if routeID == "" {
return nil, fmt.Errorf("routeID cannot be empty")
}
routeKey := getKey(orgID, routeID)
route, exists := m.routes[routeKey]
if !exists {
return nil, fmt.Errorf("route with ID %s not found", routeID)
}
return route, nil
}
func (m *MockNotificationManager) GetAllRoutePolicies(ctx context.Context, orgID string) ([]*alertmanagertypes.RoutePolicy, error) {
key := getKey(orgID, "get_all_routes")
if err := m.errors[key]; err != nil {
return nil, err
}
if orgID == "" {
return nil, fmt.Errorf("orgID cannot be empty")
}
var routes []*alertmanagertypes.RoutePolicy
for routeKey, route := range m.routes {
if route.OrgID == orgID {
routes = append(routes, route)
}
_ = routeKey
}
return routes, nil
}
func (m *MockNotificationManager) DeleteRoutePolicy(ctx context.Context, orgID string, routeID string) error {
key := getKey(orgID, "delete_route")
if err := m.errors[key]; err != nil {
return err
}
if routeID == "" {
return fmt.Errorf("routeID cannot be empty")
}
routeKey := getKey(orgID, routeID)
route, exists := m.routes[routeKey]
if !exists {
return fmt.Errorf("route with ID %s not found", routeID)
}
delete(m.routes, routeKey)
nameKey := getKey(orgID, route.Name)
if nameRoutes, exists := m.routesByName[nameKey]; exists {
var filtered []*alertmanagertypes.RoutePolicy
for _, r := range nameRoutes {
if r.ID.StringValue() != routeID {
filtered = append(filtered, r)
}
}
if len(filtered) == 0 {
delete(m.routesByName, nameKey)
} else {
m.routesByName[nameKey] = filtered
}
}
return nil
}
func (m *MockNotificationManager) DeleteAllRoutePoliciesByName(ctx context.Context, orgID string, name string) error {
key := getKey(orgID, "delete_routes_by_name")
if err := m.errors[key]; err != nil {
return err
}
if orgID == "" {
return fmt.Errorf("orgID cannot be empty")
}
if name == "" {
return fmt.Errorf("name cannot be empty")
}
nameKey := getKey(orgID, name)
routes, exists := m.routesByName[nameKey]
if !exists {
return nil // No routes to delete
}
for _, route := range routes {
routeKey := getKey(orgID, route.ID.StringValue())
delete(m.routes, routeKey)
}
delete(m.routesByName, nameKey)
return nil
}
func (m *MockNotificationManager) Match(ctx context.Context, orgID string, ruleID string, set model.LabelSet) ([]string, error) {
key := getKey(orgID, ruleID)
if err := m.errors[key]; err != nil {
return nil, err
}
config, err := m.GetNotificationConfig(orgID, ruleID)
if err != nil {
return nil, err
}
var expressionRoutes []*alertmanagertypes.RoutePolicy
if config.UsePolicy {
for _, route := range m.routes {
if route.OrgID == orgID && route.ExpressionKind == alertmanagertypes.PolicyBasedExpression {
expressionRoutes = append(expressionRoutes, route)
}
}
} else {
nameKey := getKey(orgID, ruleID)
if routes, exists := m.routesByName[nameKey]; exists {
expressionRoutes = routes
}
}
var matchedChannels []string
for _, route := range expressionRoutes {
if m.evaluateExpr(route.Expression, set) {
matchedChannels = append(matchedChannels, route.Channels...)
}
}
return matchedChannels, nil
}
func (m *MockNotificationManager) evaluateExpr(expression string, labelSet model.LabelSet) bool {
ruleID, ok := labelSet["ruleId"]
if !ok {
return false
}
if strings.Contains(expression, `ruleId in ["ruleId-OtherAlert", "ruleId-TestingAlert"]`) {
return ruleID == "ruleId-OtherAlert" || ruleID == "ruleId-TestingAlert"
}
if strings.Contains(expression, `ruleId in ["ruleId-HighLatency", "ruleId-HighErrorRate"]`) {
return ruleID == "ruleId-HighLatency" || ruleID == "ruleId-HighErrorRate"
}
if strings.Contains(expression, `ruleId == "ruleId-HighLatency"`) {
return ruleID == "ruleId-HighLatency"
}
return false
}
// Helper methods for testing
func (m *MockNotificationManager) SetMockRoute(orgID string, route *alertmanagertypes.RoutePolicy) {
routeKey := getKey(orgID, route.ID.StringValue())
m.routes[routeKey] = route
nameKey := getKey(orgID, route.Name)
m.routesByName[nameKey] = append(m.routesByName[nameKey], route)
}
func (m *MockNotificationManager) SetMockRouteError(orgID, operation string, err error) {
key := getKey(orgID, operation)
m.errors[key] = err
}
func (m *MockNotificationManager) ClearMockRoutes() {
m.routes = make(map[string]*alertmanagertypes.RoutePolicy)
m.routesByName = make(map[string][]*alertmanagertypes.RoutePolicy)
}
func (m *MockNotificationManager) GetRouteCount() int {
return len(m.routes)
}
func (m *MockNotificationManager) HasRoute(orgID, routeID string) bool {
routeKey := getKey(orgID, routeID)
_, exists := m.routes[routeKey]
return exists
}

View File

@@ -1,176 +0,0 @@
package nfroutingstoretest
import (
"context"
"regexp"
"strings"
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/sqlroutingstore"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
)
type MockSQLRouteStore struct {
routeStore alertmanagertypes.RouteStore
mock sqlmock.Sqlmock
}
func NewMockSQLRouteStore() *MockSQLRouteStore {
sqlStore := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherRegexp)
routeStore := sqlroutingstore.NewStore(sqlStore)
return &MockSQLRouteStore{
routeStore: routeStore,
mock: sqlStore.Mock(),
}
}
func (m *MockSQLRouteStore) Mock() sqlmock.Sqlmock {
return m.mock
}
func (m *MockSQLRouteStore) GetByID(ctx context.Context, orgId string, id string) (*alertmanagertypes.RoutePolicy, error) {
return m.routeStore.GetByID(ctx, orgId, id)
}
func (m *MockSQLRouteStore) Create(ctx context.Context, route *alertmanagertypes.RoutePolicy) error {
return m.routeStore.Create(ctx, route)
}
func (m *MockSQLRouteStore) CreateBatch(ctx context.Context, routes []*alertmanagertypes.RoutePolicy) error {
return m.routeStore.CreateBatch(ctx, routes)
}
func (m *MockSQLRouteStore) Delete(ctx context.Context, orgId string, id string) error {
return m.routeStore.Delete(ctx, orgId, id)
}
func (m *MockSQLRouteStore) GetAllByKind(ctx context.Context, orgID string, kind alertmanagertypes.ExpressionKind) ([]*alertmanagertypes.RoutePolicy, error) {
return m.routeStore.GetAllByKind(ctx, orgID, kind)
}
func (m *MockSQLRouteStore) GetAllByName(ctx context.Context, orgID string, name string) ([]*alertmanagertypes.RoutePolicy, error) {
return m.routeStore.GetAllByName(ctx, orgID, name)
}
func (m *MockSQLRouteStore) DeleteRouteByName(ctx context.Context, orgID string, name string) error {
return m.routeStore.DeleteRouteByName(ctx, orgID, name)
}
func (m *MockSQLRouteStore) ExpectGetByID(orgID, id string, route *alertmanagertypes.RoutePolicy) {
rows := sqlmock.NewRows([]string{"id", "org_id", "name", "expression", "kind", "description", "enabled", "tags", "channels", "created_at", "updated_at", "created_by", "updated_by"})
if route != nil {
rows.AddRow(
route.ID.StringValue(),
route.OrgID,
route.Name,
route.Expression,
route.ExpressionKind.StringValue(),
route.Description,
route.Enabled,
"[]", // tags as JSON
`["`+strings.Join(route.Channels, `","`)+`"]`, // channels as JSON
"0001-01-01T00:00:00Z", // created_at
"0001-01-01T00:00:00Z", // updated_at
"", // created_by
"", // updated_by
)
}
m.mock.ExpectQuery(`SELECT (.+) FROM "route_policy" WHERE \(id = \$1\) AND \(org_id = \$2\)`).
WithArgs(id, orgID).
WillReturnRows(rows)
}
func (m *MockSQLRouteStore) ExpectCreate(route *alertmanagertypes.RoutePolicy) {
expectedPattern := `INSERT INTO "route_policy" \(.+\) VALUES .+`
m.mock.ExpectExec(expectedPattern).
WillReturnResult(sqlmock.NewResult(1, 1))
}
func (m *MockSQLRouteStore) ExpectCreateBatch(routes []*alertmanagertypes.RoutePolicy) {
if len(routes) == 0 {
return
}
// Simplified pattern that should match any INSERT into route_policy
expectedPattern := `INSERT INTO "route_policy" \(.+\) VALUES .+`
m.mock.ExpectExec(expectedPattern).
WillReturnResult(sqlmock.NewResult(1, int64(len(routes))))
}
func (m *MockSQLRouteStore) ExpectDelete(orgID, id string) {
m.mock.ExpectExec(`DELETE FROM "route_policy" AS "route_policy" WHERE \(org_id = '` + regexp.QuoteMeta(orgID) + `'\) AND \(id = '` + regexp.QuoteMeta(id) + `'\)`).
WillReturnResult(sqlmock.NewResult(0, 1))
}
func (m *MockSQLRouteStore) ExpectGetAllByKindAndOrgID(orgID string, kind alertmanagertypes.ExpressionKind, routes []*alertmanagertypes.RoutePolicy) {
rows := sqlmock.NewRows([]string{"id", "org_id", "name", "expression", "kind", "description", "enabled", "tags", "channels", "created_at", "updated_at", "created_by", "updated_by"})
for _, route := range routes {
if route.OrgID == orgID && route.ExpressionKind == kind {
rows.AddRow(
route.ID.StringValue(),
route.OrgID,
route.Name,
route.Expression,
route.ExpressionKind.StringValue(),
route.Description,
route.Enabled,
"[]", // tags as JSON
`["`+strings.Join(route.Channels, `","`)+`"]`, // channels as JSON
"0001-01-01T00:00:00Z", // created_at
"0001-01-01T00:00:00Z", // updated_at
"", // created_by
"", // updated_by
)
}
}
m.mock.ExpectQuery(`SELECT (.+) FROM "route_policy" WHERE \(org_id = '` + regexp.QuoteMeta(orgID) + `'\) AND \(kind = '` + regexp.QuoteMeta(kind.StringValue()) + `'\)`).
WillReturnRows(rows)
}
func (m *MockSQLRouteStore) ExpectGetAllByName(orgID, name string, routes []*alertmanagertypes.RoutePolicy) {
rows := sqlmock.NewRows([]string{"id", "org_id", "name", "expression", "kind", "description", "enabled", "tags", "channels", "created_at", "updated_at", "created_by", "updated_by"})
for _, route := range routes {
if route.OrgID == orgID && route.Name == name {
rows.AddRow(
route.ID.StringValue(),
route.OrgID,
route.Name,
route.Expression,
route.ExpressionKind.StringValue(),
route.Description,
route.Enabled,
"[]", // tags as JSON
`["`+strings.Join(route.Channels, `","`)+`"]`, // channels as JSON
"0001-01-01T00:00:00Z", // created_at
"0001-01-01T00:00:00Z", // updated_at
"", // created_by
"", // updated_by
)
}
}
m.mock.ExpectQuery(`SELECT (.+) FROM "route_policy" WHERE \(org_id = '` + regexp.QuoteMeta(orgID) + `'\) AND \(name = '` + regexp.QuoteMeta(name) + `'\)`).
WillReturnRows(rows)
}
func (m *MockSQLRouteStore) ExpectDeleteRouteByName(orgID, name string) {
m.mock.ExpectExec(`DELETE FROM "route_policy" AS "route_policy" WHERE \(org_id = '` + regexp.QuoteMeta(orgID) + `'\) AND \(name = '` + regexp.QuoteMeta(name) + `'\)`).
WillReturnResult(sqlmock.NewResult(0, 1))
}
func (m *MockSQLRouteStore) ExpectationsWereMet() error {
return m.mock.ExpectationsWereMet()
}
func (m *MockSQLRouteStore) MatchExpectationsInOrder(match bool) {
m.mock.MatchExpectationsInOrder(match)
}

View File

@@ -1,93 +0,0 @@
package sqlroutingstore
import (
"context"
"database/sql"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
routeTypes "github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) routeTypes.RouteStore {
return &store{
sqlstore: sqlstore,
}
}
func (store *store) GetByID(ctx context.Context, orgId string, id string) (*routeTypes.RoutePolicy, error) {
route := new(routeTypes.RoutePolicy)
err := store.sqlstore.BunDBCtx(ctx).NewSelect().Model(route).Where("id = ?", id).Where("org_id = ?", orgId).Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "routing policy with ID: %s does not exist", id)
}
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch routing policy with ID: %s", id)
}
return route, nil
}
func (store *store) Create(ctx context.Context, route *routeTypes.RoutePolicy) error {
_, err := store.sqlstore.BunDBCtx(ctx).NewInsert().Model(route).Exec(ctx)
if err != nil {
return errors.NewInternalf(errors.CodeInternal, "error creating routing policy with ID: %s", route.ID)
}
return nil
}
func (store *store) CreateBatch(ctx context.Context, route []*routeTypes.RoutePolicy) error {
_, err := store.sqlstore.BunDBCtx(ctx).NewInsert().Model(&route).Exec(ctx)
if err != nil {
return errors.NewInternalf(errors.CodeInternal, "error creating routing policies: %v", err)
}
return nil
}
func (store *store) Delete(ctx context.Context, orgId string, id string) error {
_, err := store.sqlstore.BunDBCtx(ctx).NewDelete().Model((*routeTypes.RoutePolicy)(nil)).Where("org_id = ?", orgId).Where("id = ?", id).Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to delete routing policy with ID: %s", id)
}
return nil
}
func (store *store) GetAllByKind(ctx context.Context, orgID string, kind routeTypes.ExpressionKind) ([]*routeTypes.RoutePolicy, error) {
var routes []*routeTypes.RoutePolicy
err := store.sqlstore.BunDBCtx(ctx).NewSelect().Model(&routes).Where("org_id = ?", orgID).Where("kind = ?", kind).Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.NewNotFoundf(errors.CodeNotFound, "no routing policies found for orgID: %s", orgID)
}
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch routing policies for orgID: %s", orgID)
}
return routes, nil
}
func (store *store) GetAllByName(ctx context.Context, orgID string, name string) ([]*routeTypes.RoutePolicy, error) {
var routes []*routeTypes.RoutePolicy
err := store.sqlstore.BunDBCtx(ctx).NewSelect().Model(&routes).Where("org_id = ?", orgID).Where("name = ?", name).Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return routes, errors.NewNotFoundf(errors.CodeNotFound, "no routing policies found for orgID: %s and name: %s", orgID, name)
}
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch routing policies for orgID: %s and name: %s", orgID, name)
}
return routes, nil
}
func (store *store) DeleteRouteByName(ctx context.Context, orgID string, name string) error {
_, err := store.sqlstore.BunDBCtx(ctx).NewDelete().Model((*routeTypes.RoutePolicy)(nil)).Where("org_id = ?", orgID).Where("name = ?", name).Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to delete routing policies with name: %s", name)
}
return nil
}

View File

@@ -2,27 +2,12 @@
package nfmanager
import (
"context"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/common/model"
)
// NotificationManager defines how alerts should be grouped and configured for notification.
// NotificationManager defines how alerts should be grouped and configured for notification with multi-tenancy support.
type NotificationManager interface {
// Notification Config CRUD
GetNotificationConfig(orgID string, ruleID string) (*alertmanagertypes.NotificationConfig, error)
SetNotificationConfig(orgID string, ruleID string, config *alertmanagertypes.NotificationConfig) error
DeleteNotificationConfig(orgID string, ruleID string) error
// Route Policy CRUD
CreateRoutePolicy(ctx context.Context, orgID string, route *alertmanagertypes.RoutePolicy) error
CreateRoutePolicies(ctx context.Context, orgID string, routes []*alertmanagertypes.RoutePolicy) error
GetRoutePolicyByID(ctx context.Context, orgID string, routeID string) (*alertmanagertypes.RoutePolicy, error)
GetAllRoutePolicies(ctx context.Context, orgID string) ([]*alertmanagertypes.RoutePolicy, error)
DeleteRoutePolicy(ctx context.Context, orgID string, routeID string) error
DeleteAllRoutePoliciesByName(ctx context.Context, orgID string, name string) error
// Route matching
Match(ctx context.Context, orgID string, ruleID string, set model.LabelSet) ([]string, error)
}

View File

@@ -2,14 +2,11 @@ package rulebasednotification
import (
"context"
"strings"
"sync"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/expr-lang/expr"
"github.com/prometheus/common/model"
"github.com/SigNoz/signoz/pkg/factory"
)
@@ -17,28 +14,26 @@ import (
type provider struct {
settings factory.ScopedProviderSettings
orgToFingerprintToNotificationConfig map[string]map[string]alertmanagertypes.NotificationConfig
routeStore alertmanagertypes.RouteStore
mutex sync.RWMutex
}
// NewFactory creates a new factory for the rule-based grouping strategy.
func NewFactory(routeStore alertmanagertypes.RouteStore) factory.ProviderFactory[nfmanager.NotificationManager, nfmanager.Config] {
func NewFactory() factory.ProviderFactory[nfmanager.NotificationManager, nfmanager.Config] {
return factory.NewProviderFactory(
factory.MustNewName("rulebased"),
func(ctx context.Context, settings factory.ProviderSettings, config nfmanager.Config) (nfmanager.NotificationManager, error) {
return New(ctx, settings, config, routeStore)
return New(ctx, settings, config)
},
)
}
// New creates a new rule-based grouping strategy provider.
func New(ctx context.Context, providerSettings factory.ProviderSettings, config nfmanager.Config, routeStore alertmanagertypes.RouteStore) (nfmanager.NotificationManager, error) {
func New(ctx context.Context, providerSettings factory.ProviderSettings, config nfmanager.Config) (nfmanager.NotificationManager, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification")
return &provider{
settings: settings,
orgToFingerprintToNotificationConfig: make(map[string]map[string]alertmanagertypes.NotificationConfig),
routeStore: routeStore,
}, nil
}
@@ -63,8 +58,6 @@ func (r *provider) GetNotificationConfig(orgID string, ruleID string) (*alertman
for k, v := range config.NotificationGroup {
notificationConfig.NotificationGroup[k] = v
}
notificationConfig.UsePolicy = config.UsePolicy
notificationConfig.GroupByAll = config.GroupByAll
}
}
@@ -108,147 +101,3 @@ func (r *provider) DeleteNotificationConfig(orgID string, ruleID string) error {
return nil
}
func (r *provider) CreateRoutePolicy(ctx context.Context, orgID string, route *alertmanagertypes.RoutePolicy) error {
if route == nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "route policy cannot be nil")
}
err := route.Validate()
if err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid route policy: %v", err)
}
return r.routeStore.Create(ctx, route)
}
func (r *provider) CreateRoutePolicies(ctx context.Context, orgID string, routes []*alertmanagertypes.RoutePolicy) error {
if len(routes) == 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "route policies cannot be empty")
}
for _, route := range routes {
if route == nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "route policy cannot be nil")
}
if err := route.Validate(); err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "route policy with name %s: %s", route.Name, err.Error())
}
}
return r.routeStore.CreateBatch(ctx, routes)
}
func (r *provider) GetRoutePolicyByID(ctx context.Context, orgID string, routeID string) (*alertmanagertypes.RoutePolicy, error) {
if routeID == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "routeID cannot be empty")
}
return r.routeStore.GetByID(ctx, orgID, routeID)
}
func (r *provider) GetAllRoutePolicies(ctx context.Context, orgID string) ([]*alertmanagertypes.RoutePolicy, error) {
if orgID == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "orgID cannot be empty")
}
return r.routeStore.GetAllByKind(ctx, orgID, alertmanagertypes.PolicyBasedExpression)
}
func (r *provider) DeleteRoutePolicy(ctx context.Context, orgID string, routeID string) error {
if routeID == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "routeID cannot be empty")
}
return r.routeStore.Delete(ctx, orgID, routeID)
}
func (r *provider) DeleteAllRoutePoliciesByName(ctx context.Context, orgID string, name string) error {
if orgID == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "orgID cannot be empty")
}
if name == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "name cannot be empty")
}
return r.routeStore.DeleteRouteByName(ctx, orgID, name)
}
func (r *provider) Match(ctx context.Context, orgID string, ruleID string, set model.LabelSet) ([]string, error) {
config, err := r.GetNotificationConfig(orgID, ruleID)
if err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "error getting notification configuration: %v", err)
}
var expressionRoutes []*alertmanagertypes.RoutePolicy
if config.UsePolicy {
expressionRoutes, err = r.routeStore.GetAllByKind(ctx, orgID, alertmanagertypes.PolicyBasedExpression)
if err != nil {
return []string{}, errors.NewInternalf(errors.CodeInternal, "error getting route policies: %v", err)
}
} else {
expressionRoutes, err = r.routeStore.GetAllByName(ctx, orgID, ruleID)
if err != nil {
return []string{}, errors.NewInternalf(errors.CodeInternal, "error getting route policies: %v", err)
}
}
var matchedChannels []string
if _, ok := set[alertmanagertypes.NoDataLabel]; ok && !config.UsePolicy {
for _, expressionRoute := range expressionRoutes {
matchedChannels = append(matchedChannels, expressionRoute.Channels...)
}
return matchedChannels, nil
}
for _, route := range expressionRoutes {
evaluateExpr, err := r.evaluateExpr(route.Expression, set)
if err != nil {
continue
}
if evaluateExpr {
matchedChannels = append(matchedChannels, route.Channels...)
}
}
return matchedChannels, nil
}
func (r *provider) evaluateExpr(expression string, labelSet model.LabelSet) (bool, error) {
env := make(map[string]interface{})
for k, v := range labelSet {
key := string(k)
value := string(v)
if strings.Contains(key, ".") {
parts := strings.Split(key, ".")
current := env
for i, part := range parts {
if i == len(parts)-1 {
current[part] = value
} else {
if current[part] == nil {
current[part] = make(map[string]interface{})
}
current = current[part].(map[string]interface{})
}
}
} else {
env[key] = value
}
}
program, err := expr.Compile(expression, expr.Env(env))
if err != nil {
return false, errors.NewInternalf(errors.CodeInternal, "error compiling route policy %s: %v", expression, err)
}
output, err := expr.Run(program, env)
if err != nil {
return false, errors.NewInternalf(errors.CodeInternal, "error running route policy %s: %v", expression, err)
}
if boolVal, ok := output.(bool); ok {
return boolVal, nil
}
return false, errors.NewInternalf(errors.CodeInternal, "error in evaluating route policy %s: %v", expression, err)
}

View File

@@ -2,22 +2,18 @@ package rulebasednotification
import (
"context"
"github.com/prometheus/common/model"
"sync"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/nfroutingstoretest"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/prometheus/alertmanager/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/prometheus/common/model"
)
func createTestProviderSettings() factory.ProviderSettings {
@@ -25,8 +21,7 @@ func createTestProviderSettings() factory.ProviderSettings {
}
func TestNewFactory(t *testing.T) {
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
providerFactory := NewFactory(routeStore)
providerFactory := NewFactory()
assert.NotNil(t, providerFactory)
assert.Equal(t, "rulebased", providerFactory.Name().String())
}
@@ -36,8 +31,7 @@ func TestNew(t *testing.T) {
providerSettings := createTestProviderSettings()
config := nfmanager.Config{}
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
provider, err := New(ctx, providerSettings, config, routeStore)
provider, err := New(ctx, providerSettings, config)
require.NoError(t, err)
assert.NotNil(t, provider)
@@ -50,8 +44,7 @@ func TestProvider_SetNotificationConfig(t *testing.T) {
providerSettings := createTestProviderSettings()
config := nfmanager.Config{}
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
provider, err := New(ctx, providerSettings, config, routeStore)
provider, err := New(ctx, providerSettings, config)
require.NoError(t, err)
tests := []struct {
@@ -131,12 +124,11 @@ func TestProvider_GetNotificationConfig(t *testing.T) {
providerSettings := createTestProviderSettings()
config := nfmanager.Config{}
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
provider, err := New(ctx, providerSettings, config, routeStore)
provider, err := New(ctx, providerSettings, config)
require.NoError(t, err)
orgID := "test-org"
ruleID := "ruleId"
ruleID := "rule1"
customConfig := &alertmanagertypes.NotificationConfig{
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 30 * time.Minute,
@@ -152,6 +144,7 @@ func TestProvider_GetNotificationConfig(t *testing.T) {
},
}
// Set config for alert1
err = provider.SetNotificationConfig(orgID, ruleID, customConfig)
require.NoError(t, err)
@@ -162,7 +155,7 @@ func TestProvider_GetNotificationConfig(t *testing.T) {
name string
orgID string
ruleID string
alert *alertmanagertypes.Alert
alert *types.Alert
expectedConfig *alertmanagertypes.NotificationConfig
shouldFallback bool
}{
@@ -172,7 +165,7 @@ func TestProvider_GetNotificationConfig(t *testing.T) {
ruleID: ruleID,
expectedConfig: &alertmanagertypes.NotificationConfig{
NotificationGroup: map[model.LabelName]struct{}{
model.LabelName(ruleID): {},
model.LabelName("ruleId"): {},
},
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 30 * time.Minute,
@@ -189,13 +182,13 @@ func TestProvider_GetNotificationConfig(t *testing.T) {
NotificationGroup: map[model.LabelName]struct{}{
model.LabelName("group1"): {},
model.LabelName("group2"): {},
model.LabelName(ruleID): {},
model.LabelName("ruleId"): {},
},
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 4 * time.Hour,
NoDataInterval: 4 * time.Hour,
},
},
}, // Will get fallback from standardnotification
shouldFallback: false,
},
{
@@ -238,8 +231,7 @@ func TestProvider_ConcurrentAccess(t *testing.T) {
providerSettings := createTestProviderSettings()
config := nfmanager.Config{}
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
provider, err := New(ctx, providerSettings, config, routeStore)
provider, err := New(ctx, providerSettings, config)
require.NoError(t, err)
orgID := "test-org"
@@ -276,634 +268,3 @@ func TestProvider_ConcurrentAccess(t *testing.T) {
// Wait for both goroutines to complete
wg.Wait()
}
func TestProvider_EvaluateExpression(t *testing.T) {
provider := &provider{}
tests := []struct {
name string
expression string
labelSet model.LabelSet
expected bool
}{
{
name: "simple equality check - match",
expression: `threshold.name == 'auth' && ruleId == 'rule1'`,
labelSet: model.LabelSet{
"threshold.name": "auth",
"ruleId": "rule1",
},
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"`,
labelSet: model.LabelSet{
"service": "auth",
"env": "production",
},
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"`,
labelSet: model.LabelSet{
"service": "auth",
"env": "production",
},
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"`,
labelSet: model.LabelSet{
"service": "auth",
"env": "production",
},
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"`,
labelSet: model.LabelSet{
"service": "auth",
"env": "production",
},
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"`,
labelSet: model.LabelSet{
"service": "auth",
"env": "production",
},
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"]`,
labelSet: model.LabelSet{
"service": "auth",
},
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"]`,
labelSet: model.LabelSet{
"service": "auth",
},
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"`,
labelSet: model.LabelSet{
"host": "prod-server-01",
},
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"`,
labelSet: model.LabelSet{
"host": "prod-server-01",
},
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"`,
labelSet: model.LabelSet{
"service": "payment",
"env": "staging",
"critical": "true",
},
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"`,
labelSet: model.LabelSet{
"service": "auth",
},
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"`,
labelSet: model.LabelSet{
"threshold.name": "high-cpu",
"ruleId": "rule-123",
"service": "auth",
},
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"`,
labelSet: model.LabelSet{
"alertname": "HighCPUUsage",
"ruleId": "cpu-alert-001",
"severity": "critical",
},
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"]`,
labelSet: model.LabelSet{
"k8s.namespace.name": "auth",
"service": "auth",
"host": "k8s-node-1",
},
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"`,
labelSet: model.LabelSet{
"threshold.name": "HighCPUUsage",
"ruleId": "rule-uuid-123",
"severity": "warning",
},
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
labelSet: model.LabelSet{
"service": "auth", // lowercase a
},
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"`,
labelSet: model.LabelSet{
"port": "8080",
},
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"`,
labelSet: model.LabelSet{
"service": "auth-service-v2",
},
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"`,
labelSet: model.LabelSet{
"service": "payment",
"env": "staging",
"critical": "true",
},
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 {
t.Run(tt.name, func(t *testing.T) {
result, err := provider.evaluateExpr(tt.expression, tt.labelSet)
assert.NoError(t, err)
assert.Equal(t, tt.expected, result, "Expression: %s", tt.expression)
})
}
}
func TestProvider_DeleteRoute(t *testing.T) {
ctx := context.Background()
providerSettings := createTestProviderSettings()
config := nfmanager.Config{}
tests := []struct {
name string
orgID string
routeID string
wantErr bool
}{
{
name: "valid parameters",
orgID: "test-org-123",
routeID: "route-uuid-456",
wantErr: false,
},
{
name: "empty routeID",
orgID: "test-org-123",
routeID: "",
wantErr: true,
},
{
name: "valid orgID with valid routeID",
orgID: "another-org",
routeID: "another-route-id",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
provider, err := New(ctx, providerSettings, config, routeStore)
require.NoError(t, err)
if !tt.wantErr {
routeStore.ExpectDelete(tt.orgID, tt.routeID)
}
err = provider.DeleteRoutePolicy(ctx, tt.orgID, tt.routeID)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NoError(t, routeStore.ExpectationsWereMet())
}
})
}
}
func TestProvider_CreateRoute(t *testing.T) {
ctx := context.Background()
providerSettings := createTestProviderSettings()
config := nfmanager.Config{}
tests := []struct {
name string
orgID string
route *alertmanagertypes.RoutePolicy
wantErr bool
}{
{
name: "valid route",
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: "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",
route: nil,
wantErr: true,
},
{
name: "invalid route - missing expression",
orgID: "test-org-123",
route: &alertmanagertypes.RoutePolicy{
Expression: "", // empty expression
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
Name: "invalid-route",
OrgID: "test-org-123",
},
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,
},
{
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 {
t.Run(tt.name, func(t *testing.T) {
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
provider, err := New(ctx, providerSettings, config, routeStore)
require.NoError(t, err)
if !tt.wantErr && tt.route != nil {
routeStore.ExpectCreate(tt.route)
}
err = provider.CreateRoutePolicy(ctx, tt.orgID, tt.route)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NoError(t, routeStore.ExpectationsWereMet())
}
})
}
}
func TestProvider_CreateRoutes(t *testing.T) {
ctx := context.Background()
providerSettings := createTestProviderSettings()
config := nfmanager.Config{}
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
provider, err := New(ctx, providerSettings, config, routeStore)
require.NoError(t, err)
validRoute1 := &alertmanagertypes.RoutePolicy{
Expression: `service == "auth"`,
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
Name: "auth-route",
Description: "Auth service route",
Enabled: true,
OrgID: "test-org",
Channels: []string{"slack-auth"},
}
validRoute2 := &alertmanagertypes.RoutePolicy{
Expression: `service == "payment"`,
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
Name: "payment-route",
Description: "Payment service route",
Enabled: true,
OrgID: "test-org",
Channels: []string{"slack-payment"},
}
invalidRoute := &alertmanagertypes.RoutePolicy{
Expression: "", // empty expression - invalid
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
Name: "invalid-route",
OrgID: "test-org",
}
tests := []struct {
name string
orgID string
routes []*alertmanagertypes.RoutePolicy
wantErr bool
}{
{
name: "valid routes",
orgID: "test-org",
routes: []*alertmanagertypes.RoutePolicy{validRoute1, validRoute2},
wantErr: false,
},
{
name: "empty routes list",
orgID: "test-org",
routes: []*alertmanagertypes.RoutePolicy{},
wantErr: true,
},
{
name: "nil routes list",
orgID: "test-org",
routes: nil,
wantErr: true,
},
{
name: "routes with nil route",
orgID: "test-org",
routes: []*alertmanagertypes.RoutePolicy{validRoute1, nil},
wantErr: true,
},
{
name: "routes with invalid route",
orgID: "test-org",
routes: []*alertmanagertypes.RoutePolicy{validRoute1, invalidRoute},
wantErr: true,
},
{
name: "single valid route",
orgID: "test-org",
routes: []*alertmanagertypes.RoutePolicy{validRoute1},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.wantErr && len(tt.routes) > 0 {
routeStore.ExpectCreateBatch(tt.routes)
}
err := provider.CreateRoutePolicies(ctx, tt.orgID, tt.routes)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NoError(t, routeStore.ExpectationsWereMet())
}
})
}
}

View File

@@ -4,9 +4,6 @@ import (
"context"
"sync"
"github.com/prometheus/alertmanager/featurecontrol"
"github.com/prometheus/alertmanager/matcher/compat"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/errors"
@@ -64,7 +61,6 @@ func New(
}
func (service *Service) SyncServers(ctx context.Context) error {
compat.InitFromFlags(service.settings.Logger(), featurecontrol.NoopFlags{})
orgs, err := service.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
return err
@@ -146,7 +142,7 @@ func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver
return server.TestReceiver(ctx, receiver)
}
func (service *Service) TestAlert(ctx context.Context, orgID string, receiversMap map[*alertmanagertypes.PostableAlert][]string, config *alertmanagertypes.NotificationConfig) error {
func (service *Service) TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error {
service.serversMtx.RLock()
defer service.serversMtx.RUnlock()
@@ -155,7 +151,7 @@ func (service *Service) TestAlert(ctx context.Context, orgID string, receiversMa
return err
}
return server.TestAlert(ctx, receiversMap, config)
return server.TestAlert(ctx, alert, receivers)
}
func (service *Service) Stop(ctx context.Context) error {

View File

@@ -2,12 +2,8 @@ package signozalertmanager
import (
"context"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/prometheus/common/model"
"time"
amConfig "github.com/prometheus/alertmanager/config"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
@@ -15,9 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -100,29 +94,8 @@ func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiv
return provider.service.TestReceiver(ctx, orgID, receiver)
}
func (provider *provider) TestAlert(ctx context.Context, orgID string, ruleID string, receiversMap map[*alertmanagertypes.PostableAlert][]string) error {
config, err := provider.notificationManager.GetNotificationConfig(orgID, ruleID)
if err != nil {
return err
}
if config.UsePolicy {
for alert := range receiversMap {
set := make(model.LabelSet)
for k, v := range alert.Labels {
set[model.LabelName(k)] = model.LabelValue(v)
}
match, err := provider.notificationManager.Match(ctx, orgID, alert.Labels[labels.AlertRuleIdLabel], set)
if err != nil {
return err
}
if len(match) == 0 {
delete(receiversMap, alert)
} else {
receiversMap[alert] = match
}
}
}
return provider.service.TestAlert(ctx, orgID, receiversMap, config)
func (provider *provider) TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error {
return provider.service.TestAlert(ctx, orgID, alert, receivers)
}
func (provider *provider) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
@@ -238,316 +211,3 @@ func (provider *provider) DeleteNotificationConfig(ctx context.Context, orgID va
}
return nil
}
func (provider *provider) CreateRoutePolicy(ctx context.Context, routeRequest *alertmanagertypes.PostableRoutePolicy) (*alertmanagertypes.GettableRoutePolicy, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return nil, err
}
if err := routeRequest.Validate(); err != nil {
return nil, err
}
route := alertmanagertypes.RoutePolicy{
Expression: routeRequest.Expression,
ExpressionKind: routeRequest.ExpressionKind,
Name: routeRequest.Name,
Description: routeRequest.Description,
Enabled: true,
Tags: routeRequest.Tags,
Channels: routeRequest.Channels,
OrgID: claims.OrgID,
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
UserAuditable: types.UserAuditable{
CreatedBy: claims.Email,
UpdatedBy: claims.Email,
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}
err = provider.notificationManager.CreateRoutePolicy(ctx, orgID.String(), &route)
if err != nil {
return nil, err
}
return &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *routeRequest,
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
}, nil
}
func (provider *provider) CreateRoutePolicies(ctx context.Context, routeRequests []*alertmanagertypes.PostableRoutePolicy) ([]*alertmanagertypes.GettableRoutePolicy, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return nil, err
}
if len(routeRequests) == 0 {
return []*alertmanagertypes.GettableRoutePolicy{}, nil
}
routes := make([]*alertmanagertypes.RoutePolicy, 0, len(routeRequests))
results := make([]*alertmanagertypes.GettableRoutePolicy, 0, len(routeRequests))
for _, routeRequest := range routeRequests {
if err := routeRequest.Validate(); err != nil {
return nil, err
}
route := &alertmanagertypes.RoutePolicy{
Expression: routeRequest.Expression,
ExpressionKind: routeRequest.ExpressionKind,
Name: routeRequest.Name,
Description: routeRequest.Description,
Enabled: true,
Tags: routeRequest.Tags,
Channels: routeRequest.Channels,
OrgID: claims.OrgID,
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
UserAuditable: types.UserAuditable{
CreatedBy: claims.Email,
UpdatedBy: claims.Email,
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}
routes = append(routes, route)
results = append(results, &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *routeRequest,
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
})
}
err = provider.notificationManager.CreateRoutePolicies(ctx, orgID.String(), routes)
if err != nil {
return nil, err
}
return results, nil
}
func (provider *provider) GetRoutePolicyByID(ctx context.Context, routeID string) (*alertmanagertypes.GettableRoutePolicy, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return nil, err
}
route, err := provider.notificationManager.GetRoutePolicyByID(ctx, orgID.String(), routeID)
if err != nil {
return nil, err
}
return &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: alertmanagertypes.PostableRoutePolicy{
Expression: route.Expression,
ExpressionKind: route.ExpressionKind,
Channels: route.Channels,
Name: route.Name,
Description: route.Description,
Tags: route.Tags,
},
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
}, nil
}
func (provider *provider) GetAllRoutePolicies(ctx context.Context) ([]*alertmanagertypes.GettableRoutePolicy, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return nil, err
}
routes, err := provider.notificationManager.GetAllRoutePolicies(ctx, orgID.String())
if err != nil {
return nil, err
}
results := make([]*alertmanagertypes.GettableRoutePolicy, 0, len(routes))
for _, route := range routes {
results = append(results, &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: alertmanagertypes.PostableRoutePolicy{
Expression: route.Expression,
ExpressionKind: route.ExpressionKind,
Channels: route.Channels,
Name: route.Name,
Description: route.Description,
Tags: route.Tags,
},
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
})
}
return results, nil
}
func (provider *provider) UpdateRoutePolicyByID(ctx context.Context, routeID string, route *alertmanagertypes.PostableRoutePolicy) (*alertmanagertypes.GettableRoutePolicy, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeUnauthenticated, "invalid claims: %v", err)
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return nil, err
}
if routeID == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "routeID cannot be empty")
}
if route == nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "route cannot be nil")
}
if err := route.Validate(); err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid route: %v", err)
}
existingRoute, err := provider.notificationManager.GetRoutePolicyByID(ctx, claims.OrgID, routeID)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeNotFound, "route not found: %v", err)
}
updatedRoute := &alertmanagertypes.RoutePolicy{
Expression: route.Expression,
ExpressionKind: route.ExpressionKind,
Name: route.Name,
Description: route.Description,
Tags: route.Tags,
Channels: route.Channels,
OrgID: claims.OrgID,
Identifiable: existingRoute.Identifiable,
UserAuditable: types.UserAuditable{
CreatedBy: existingRoute.CreatedBy,
UpdatedBy: claims.Email,
},
TimeAuditable: types.TimeAuditable{
CreatedAt: existingRoute.CreatedAt,
UpdatedAt: time.Now(),
},
}
err = provider.notificationManager.DeleteRoutePolicy(ctx, orgID.String(), routeID)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInternal, "error deleting existing route: %v", err)
}
err = provider.notificationManager.CreateRoutePolicy(ctx, orgID.String(), updatedRoute)
if err != nil {
return nil, err
}
return &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *route,
ID: updatedRoute.ID.StringValue(),
CreatedAt: &updatedRoute.CreatedAt,
UpdatedAt: &updatedRoute.UpdatedAt,
CreatedBy: &updatedRoute.CreatedBy,
UpdatedBy: &updatedRoute.UpdatedBy,
}, nil
}
func (provider *provider) DeleteRoutePolicyByID(ctx context.Context, routeID string) error {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return errors.NewInvalidInputf(errors.CodeUnauthenticated, "invalid claims: %v", err)
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return err
}
if routeID == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "routeID cannot be empty")
}
return provider.notificationManager.DeleteRoutePolicy(ctx, orgID.String(), routeID)
}
func (provider *provider) CreateInhibitRules(ctx context.Context, orgID valuer.UUID, rules []amConfig.InhibitRule) error {
config, err := provider.configStore.Get(ctx, orgID.String())
if err != nil {
return err
}
if err := config.AddInhibitRules(rules); err != nil {
return err
}
return provider.configStore.Set(ctx, config)
}
func (provider *provider) DeleteAllRoutePoliciesByRuleId(ctx context.Context, names string) error {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return errors.NewInvalidInputf(errors.CodeUnauthenticated, "invalid claims: %v", err)
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return err
}
return provider.notificationManager.DeleteAllRoutePoliciesByName(ctx, orgID.String(), names)
}
func (provider *provider) UpdateAllRoutePoliciesByRuleId(ctx context.Context, names string, routes []*alertmanagertypes.PostableRoutePolicy) error {
err := provider.DeleteAllRoutePoliciesByRuleId(ctx, names)
if err != nil {
return errors.NewInvalidInputf(errors.CodeInternal, "error deleting the routes: %v", err)
}
_, err = provider.CreateRoutePolicies(ctx, routes)
return err
}
func (provider *provider) DeleteAllInhibitRulesByRuleId(ctx context.Context, orgID valuer.UUID, ruleId string) error {
config, err := provider.configStore.Get(ctx, orgID.String())
if err != nil {
return err
}
if err := config.DeleteRuleIDInhibitor(ruleId); err != nil {
return err
}
return provider.configStore.Set(ctx, config)
}

View File

@@ -10,6 +10,7 @@ import (
"fmt"
"github.com/SigNoz/signoz/pkg/modules/thirdpartyapi"
//qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"io"
"math"
"net/http"
@@ -491,12 +492,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/channels", am.EditAccess(aH.AlertmanagerAPI.CreateChannel)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/testChannel", am.EditAccess(aH.AlertmanagerAPI.TestReceiver)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/route_policies", am.ViewAccess(aH.AlertmanagerAPI.GetAllRoutePolicies)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/route_policies/{id}", am.ViewAccess(aH.AlertmanagerAPI.GetRoutePolicyByID)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/route_policies", am.AdminAccess(aH.AlertmanagerAPI.CreateRoutePolicy)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/route_policies/{id}", am.AdminAccess(aH.AlertmanagerAPI.DeleteRoutePolicyByID)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/route_policies/{id}", am.AdminAccess(aH.AlertmanagerAPI.UpdateRoutePolicy)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/alerts", am.ViewAccess(aH.AlertmanagerAPI.GetAlerts)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.ViewAccess(aH.listRules)).Methods(http.MethodGet)
@@ -621,7 +616,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// Export
router.HandleFunc("/api/v1/export_raw_data", am.ViewAccess(aH.Signoz.Handlers.RawDataExport.ExportRawData)).Methods(http.MethodGet)
}
func (ah *APIHandler) MetricExplorerRoutes(router *mux.Router, am *middleware.AuthZ) {

View File

@@ -4,11 +4,13 @@ import (
"context"
"fmt"
"log/slog"
"math"
"net/url"
"sync"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
@@ -165,6 +167,22 @@ func NewBaseRule(id string, orgID valuer.UUID, p *ruletypes.PostableRule, reader
return baseRule, nil
}
func (r *BaseRule) targetVal() float64 {
if r.ruleCondition == nil || r.ruleCondition.Target == nil {
return 0
}
// get the converter for the target unit
unitConverter := converter.FromUnit(converter.Unit(r.ruleCondition.TargetUnit))
// convert the target value to the y-axis unit
value := unitConverter.Convert(converter.Value{
F: *r.ruleCondition.Target,
U: converter.Unit(r.ruleCondition.TargetUnit),
}, converter.Unit(r.Unit()))
return value.F
}
func (r *BaseRule) matchType() ruletypes.MatchType {
if r.ruleCondition == nil {
return ruletypes.AtleastOnce
@@ -203,6 +221,10 @@ func (r *BaseRule) HoldDuration() time.Duration {
return r.holdDuration
}
func (r *BaseRule) TargetVal() float64 {
return r.targetVal()
}
func (r *ThresholdRule) hostFromSource() string {
parsedUrl, err := url.Parse(r.source)
if err != nil {
@@ -358,6 +380,232 @@ func (r *BaseRule) ForEachActiveAlert(f func(*ruletypes.Alert)) {
}
}
func (r *BaseRule) ShouldAlert(series v3.Series) (ruletypes.Sample, bool) {
var alertSmpl ruletypes.Sample
var shouldAlert bool
var lbls qslabels.Labels
for name, value := range series.Labels {
lbls = append(lbls, qslabels.Label{Name: name, Value: value})
}
series.Points = removeGroupinSetPoints(series)
// nothing to evaluate
if len(series.Points) == 0 {
return alertSmpl, false
}
if r.ruleCondition.RequireMinPoints {
if len(series.Points) < r.ruleCondition.RequiredNumPoints {
zap.L().Info("not enough data points to evaluate series, skipping", zap.String("ruleid", r.ID()), zap.Int("numPoints", len(series.Points)), zap.Int("requiredPoints", r.ruleCondition.RequiredNumPoints))
return alertSmpl, false
}
}
switch r.matchType() {
case ruletypes.AtleastOnce:
// If any sample matches the condition, the rule is firing.
if r.compareOp() == ruletypes.ValueIsAbove {
for _, smpl := range series.Points {
if smpl.Value > r.targetVal() {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
} else if r.compareOp() == ruletypes.ValueIsBelow {
for _, smpl := range series.Points {
if smpl.Value < r.targetVal() {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
} else if r.compareOp() == ruletypes.ValueIsEq {
for _, smpl := range series.Points {
if smpl.Value == r.targetVal() {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
} else if r.compareOp() == ruletypes.ValueIsNotEq {
for _, smpl := range series.Points {
if smpl.Value != r.targetVal() {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
for _, smpl := range series.Points {
if math.Abs(smpl.Value) >= r.targetVal() {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
}
case ruletypes.AllTheTimes:
// If all samples match the condition, the rule is firing.
shouldAlert = true
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: r.targetVal()}, Metric: lbls}
if r.compareOp() == ruletypes.ValueIsAbove {
for _, smpl := range series.Points {
if smpl.Value <= r.targetVal() {
shouldAlert = false
break
}
}
// use min value from the series
if shouldAlert {
var minValue float64 = math.Inf(1)
for _, smpl := range series.Points {
if smpl.Value < minValue {
minValue = smpl.Value
}
}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: minValue}, Metric: lbls}
}
} else if r.compareOp() == ruletypes.ValueIsBelow {
for _, smpl := range series.Points {
if smpl.Value >= r.targetVal() {
shouldAlert = false
break
}
}
if shouldAlert {
var maxValue float64 = math.Inf(-1)
for _, smpl := range series.Points {
if smpl.Value > maxValue {
maxValue = smpl.Value
}
}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: maxValue}, Metric: lbls}
}
} else if r.compareOp() == ruletypes.ValueIsEq {
for _, smpl := range series.Points {
if smpl.Value != r.targetVal() {
shouldAlert = false
break
}
}
} else if r.compareOp() == ruletypes.ValueIsNotEq {
for _, smpl := range series.Points {
if smpl.Value == r.targetVal() {
shouldAlert = false
break
}
}
// use any non-inf or nan value from the series
if shouldAlert {
for _, smpl := range series.Points {
if !math.IsInf(smpl.Value, 0) && !math.IsNaN(smpl.Value) {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
break
}
}
}
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
for _, smpl := range series.Points {
if math.Abs(smpl.Value) < r.targetVal() {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = false
break
}
}
}
case ruletypes.OnAverage:
// If the average of all samples matches the condition, the rule is firing.
var sum, count float64
for _, smpl := range series.Points {
if math.IsNaN(smpl.Value) || math.IsInf(smpl.Value, 0) {
continue
}
sum += smpl.Value
count++
}
avg := sum / count
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: avg}, Metric: lbls}
if r.compareOp() == ruletypes.ValueIsAbove {
if avg > r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueIsBelow {
if avg < r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueIsEq {
if avg == r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueIsNotEq {
if avg != r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
if math.Abs(avg) >= r.targetVal() {
shouldAlert = true
}
}
case ruletypes.InTotal:
// If the sum of all samples matches the condition, the rule is firing.
var sum float64
for _, smpl := range series.Points {
if math.IsNaN(smpl.Value) || math.IsInf(smpl.Value, 0) {
continue
}
sum += smpl.Value
}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: sum}, Metric: lbls}
if r.compareOp() == ruletypes.ValueIsAbove {
if sum > r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueIsBelow {
if sum < r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueIsEq {
if sum == r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueIsNotEq {
if sum != r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
if math.Abs(sum) >= r.targetVal() {
shouldAlert = true
}
}
case ruletypes.Last:
// If the last sample matches the condition, the rule is firing.
shouldAlert = false
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: series.Points[len(series.Points)-1].Value}, Metric: lbls}
if r.compareOp() == ruletypes.ValueIsAbove {
if series.Points[len(series.Points)-1].Value > r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueIsBelow {
if series.Points[len(series.Points)-1].Value < r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueIsEq {
if series.Points[len(series.Points)-1].Value == r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ruletypes.ValueIsNotEq {
if series.Points[len(series.Points)-1].Value != r.targetVal() {
shouldAlert = true
}
}
}
return alertSmpl, shouldAlert
}
func (r *BaseRule) RecordRuleStateHistory(ctx context.Context, prevState, currentState model.AlertState, itemsToAdd []model.RuleStateHistory) error {
zap.L().Debug("recording rule state history", zap.String("ruleid", r.ID()), zap.Any("prevState", prevState), zap.Any("currentState", currentState), zap.Any("itemsToAdd", itemsToAdd))
revisedItemsToAdd := map[uint64]model.RuleStateHistory{}

View File

@@ -1,7 +1,6 @@
package rules
import (
"github.com/stretchr/testify/require"
"testing"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
@@ -23,15 +22,6 @@ func TestBaseRule_RequireMinPoints(t *testing.T) {
RequireMinPoints: true,
RequiredNumPoints: 4,
},
Threshold: ruletypes.BasicRuleThresholds{
{
Name: "test-threshold",
TargetValue: &threshold,
CompareOp: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
},
},
},
series: &v3.Series{
Points: []v3.Point{
@@ -51,14 +41,6 @@ func TestBaseRule_RequireMinPoints(t *testing.T) {
MatchType: ruletypes.AtleastOnce,
Target: &threshold,
},
Threshold: ruletypes.BasicRuleThresholds{
{
Name: "test-threshold",
TargetValue: &threshold,
CompareOp: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
},
},
},
series: &v3.Series{
Points: []v3.Point{
@@ -74,9 +56,10 @@ func TestBaseRule_RequireMinPoints(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := test.rule.Threshold.ShouldAlert(*test.series, "")
require.NoError(t, err)
require.Equal(t, len(test.series.Points) >= test.rule.ruleCondition.RequiredNumPoints, test.shouldAlert)
_, shouldAlert := test.rule.ShouldAlert(*test.series)
if shouldAlert != test.shouldAlert {
t.Errorf("expected shouldAlert to be %v, got %v", test.shouldAlert, shouldAlert)
}
})
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"log/slog"
"sort"
"strings"
@@ -351,35 +350,39 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id valuer.UUID)
existingRule.Data = ruleStr
return m.ruleStore.EditRule(ctx, existingRule, func(ctx context.Context) error {
if parsedRule.NotificationSettings != nil {
config := parsedRule.NotificationSettings.GetAlertManagerNotificationConfig()
err = m.alertmanager.SetNotificationConfig(ctx, orgID, id.StringValue(), &config)
cfg, err := m.alertmanager.GetConfig(ctx, claims.OrgID)
if err != nil {
return err
}
var preferredChannels []string
if len(parsedRule.PreferredChannels) == 0 {
channels, err := m.alertmanager.ListChannels(ctx, claims.OrgID)
if err != nil {
return err
}
if !parsedRule.NotificationSettings.UsePolicy {
request, err := parsedRule.GetRuleRouteRequest(id.StringValue())
if err != nil {
return err
}
err = m.alertmanager.UpdateAllRoutePoliciesByRuleId(ctx, id.StringValue(), request)
if err != nil {
return err
}
err = m.alertmanager.DeleteAllInhibitRulesByRuleId(ctx, orgID, id.StringValue())
if err != nil {
return err
}
inhibitRules, err := parsedRule.GetInhibitRules(id.StringValue())
if err != nil {
return err
}
err = m.alertmanager.CreateInhibitRules(ctx, orgID, inhibitRules)
if err != nil {
return err
}
for _, channel := range channels {
preferredChannels = append(preferredChannels, channel.Name)
}
} else {
preferredChannels = parsedRule.PreferredChannels
}
err = cfg.UpdateRuleIDMatcher(id.StringValue(), preferredChannels)
if err != nil {
return err
}
if parsedRule.NotificationSettings != nil {
config := parsedRule.NotificationSettings.GetAlertManagerNotificationConfig()
err = m.alertmanager.SetNotificationConfig(ctx, orgID, existingRule.ID.StringValue(), &config)
if err != nil {
return err
}
}
err = m.alertmanager.SetConfig(ctx, cfg)
if err != nil {
return err
}
err = m.syncRuleStateWithTask(ctx, orgID, prepareTaskName(existingRule.ID.StringValue()), &parsedRule)
if err != nil {
@@ -485,19 +488,6 @@ func (m *Manager) DeleteRule(ctx context.Context, idStr string) error {
}
err = m.alertmanager.DeleteNotificationConfig(ctx, orgID, id.String())
if err != nil {
return err
}
err = m.alertmanager.DeleteAllRoutePoliciesByRuleId(ctx, id.String())
if err != nil {
return err
}
err = m.alertmanager.DeleteAllInhibitRulesByRuleId(ctx, orgID, id.String())
if err != nil {
return err
}
taskName := prepareTaskName(id.StringValue())
m.deleteTask(taskName)
@@ -558,30 +548,41 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*ruletypes.Ge
}
id, err := m.ruleStore.CreateRule(ctx, storedRule, func(ctx context.Context, id valuer.UUID) error {
if parsedRule.NotificationSettings != nil {
config := parsedRule.NotificationSettings.GetAlertManagerNotificationConfig()
err = m.alertmanager.SetNotificationConfig(ctx, orgID, id.StringValue(), &config)
cfg, err := m.alertmanager.GetConfig(ctx, claims.OrgID)
if err != nil {
return err
}
var preferredChannels []string
if len(parsedRule.PreferredChannels) == 0 {
channels, err := m.alertmanager.ListChannels(ctx, claims.OrgID)
if err != nil {
return err
}
if !parsedRule.NotificationSettings.UsePolicy {
request, err := parsedRule.GetRuleRouteRequest(id.StringValue())
if err != nil {
return err
}
_, err = m.alertmanager.CreateRoutePolicies(ctx, request)
if err != nil {
return err
}
inhibitRules, err := parsedRule.GetInhibitRules(id.StringValue())
if err != nil {
return err
}
err = m.alertmanager.CreateInhibitRules(ctx, orgID, inhibitRules)
if err != nil {
return err
}
for _, channel := range channels {
preferredChannels = append(preferredChannels, channel.Name)
}
} else {
preferredChannels = parsedRule.PreferredChannels
}
if parsedRule.NotificationSettings != nil {
config := parsedRule.NotificationSettings.GetAlertManagerNotificationConfig()
err = m.alertmanager.SetNotificationConfig(ctx, orgID, storedRule.ID.StringValue(), &config)
if err != nil {
return err
}
}
err = cfg.CreateRuleIDMatcher(id.StringValue(), preferredChannels)
if err != nil {
return err
}
err = m.alertmanager.SetConfig(ctx, cfg)
if err != nil {
return err
}
taskName := prepareTaskName(id.StringValue())
@@ -755,30 +756,36 @@ func (m *Manager) prepareTestNotifyFunc() NotifyFunc {
if len(alerts) == 0 {
return
}
ruleID := alerts[0].Labels.Map()[labels.AlertRuleIdLabel]
receiverMap := make(map[*alertmanagertypes.PostableAlert][]string)
for _, alert := range alerts {
generatorURL := alert.GeneratorURL
a := &alertmanagertypes.PostableAlert{}
a.Annotations = alert.Annotations.Map()
a.StartsAt = strfmt.DateTime(alert.FiredAt)
a.Alert = alertmanagertypes.AlertModel{
Labels: alert.Labels.Map(),
GeneratorURL: strfmt.URI(generatorURL),
}
if !alert.ResolvedAt.IsZero() {
a.EndsAt = strfmt.DateTime(alert.ResolvedAt)
} else {
a.EndsAt = strfmt.DateTime(alert.ValidUntil)
}
receiverMap[a] = alert.Receivers
alert := alerts[0]
generatorURL := alert.GeneratorURL
a := &alertmanagertypes.PostableAlert{}
a.Annotations = alert.Annotations.Map()
a.StartsAt = strfmt.DateTime(alert.FiredAt)
a.Alert = alertmanagertypes.AlertModel{
Labels: alert.Labels.Map(),
GeneratorURL: strfmt.URI(generatorURL),
}
err := m.alertmanager.TestAlert(ctx, orgID, ruleID, receiverMap)
if err != nil {
zap.L().Error("failed to send test notification", zap.Error(err))
return
if !alert.ResolvedAt.IsZero() {
a.EndsAt = strfmt.DateTime(alert.ResolvedAt)
} else {
a.EndsAt = strfmt.DateTime(alert.ValidUntil)
}
if len(alert.Receivers) == 0 {
channels, err := m.alertmanager.ListChannels(ctx, orgID)
if err != nil {
zap.L().Error("failed to list channels while sending test notification", zap.Error(err))
return
}
for _, channel := range channels {
alert.Receivers = append(alert.Receivers, channel.Name)
}
}
m.alertmanager.TestAlert(ctx, orgID, a, alert.Receivers)
}
}
@@ -976,17 +983,6 @@ func (m *Manager) TestNotification(ctx context.Context, orgID valuer.UUID, ruleS
if err != nil {
return 0, model.BadRequest(err)
}
if !parsedRule.NotificationSettings.UsePolicy {
parsedRule.NotificationSettings.GroupBy = append(parsedRule.NotificationSettings.GroupBy, ruletypes.LabelThresholdName)
}
config := parsedRule.NotificationSettings.GetAlertManagerNotificationConfig()
err = m.alertmanager.SetNotificationConfig(ctx, orgID, parsedRule.AlertName, &config)
if err != nil {
return 0, &model.ApiError{
Typ: model.ErrorBadData,
Err: err,
}
}
alertCount, apiErr := m.prepareTestRuleFunc(PrepareTestRuleOptions{
Rule: &parsedRule,

View File

@@ -2,15 +2,10 @@ package rules
import (
"context"
"fmt"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/nfroutingstoretest"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification"
"github.com/prometheus/common/model"
"strings"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfmanagertest"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
@@ -37,38 +32,19 @@ func TestManager_PatchRule_PayloadVariations(t *testing.T) {
Email: "test@example.com",
Role: "admin",
}
manager, mockSQLRuleStore, mockRouteStore, nfmanager, orgId := setupTestManager(t)
manager, mockSQLRuleStore, orgId := setupTestManager(t)
claims.OrgID = orgId
testCases := []struct {
name string
originalData string
patchData string
Route []*alertmanagertypes.RoutePolicy
Config *alertmanagertypes.NotificationConfig
expectedResult func(*ruletypes.GettableRule) bool
expectError bool
description string
}{
{
name: "patch complete rule with task sync validation",
Route: []*alertmanagertypes.RoutePolicy{
{
Expression: fmt.Sprintf("ruleId == \"{{.ruleId}}\" && threshold.name == \"warning\""),
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Channels: []string{"test-alerts"},
Name: "{{.ruleId}}",
Enabled: true,
},
},
Config: &alertmanagertypes.NotificationConfig{
NotificationGroup: map[model.LabelName]struct{}{model.LabelName("ruleId"): {}},
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 4 * time.Hour,
NoDataInterval: 4 * time.Hour,
},
UsePolicy: false,
},
originalData: `{
"schemaVersion":"v1",
"alert": "test-original-alert",
@@ -119,23 +95,6 @@ func TestManager_PatchRule_PayloadVariations(t *testing.T) {
},
{
name: "patch rule to disabled state",
Route: []*alertmanagertypes.RoutePolicy{
{
Expression: fmt.Sprintf("ruleId == \"{{.ruleId}}\" && threshold.name == \"warning\""),
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Channels: []string{"test-alerts"},
Name: "{{.ruleId}}",
Enabled: true,
},
},
Config: &alertmanagertypes.NotificationConfig{
NotificationGroup: map[model.LabelName]struct{}{model.LabelName("ruleId"): {}},
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 4 * time.Hour,
NoDataInterval: 4 * time.Hour,
},
UsePolicy: false,
},
originalData: `{
"schemaVersion":"v2",
"alert": "test-disable-alert",
@@ -220,20 +179,6 @@ func TestManager_PatchRule_PayloadVariations(t *testing.T) {
OrgID: claims.OrgID,
}
// Update route expectations with actual rule ID
routesWithRuleID := make([]*alertmanagertypes.RoutePolicy, len(tc.Route))
for i, route := range tc.Route {
routesWithRuleID[i] = &alertmanagertypes.RoutePolicy{
Expression: strings.Replace(route.Expression, "{{.ruleId}}", ruleID.String(), -1),
ExpressionKind: route.ExpressionKind,
Channels: route.Channels,
Name: strings.Replace(route.Name, "{{.ruleId}}", ruleID.String(), -1),
Enabled: route.Enabled,
}
}
mockRouteStore.ExpectDeleteRouteByName(existingRule.OrgID, ruleID.String())
mockRouteStore.ExpectCreateBatch(routesWithRuleID)
mockSQLRuleStore.ExpectGetStoredRule(ruleID, existingRule)
mockSQLRuleStore.ExpectEditRule(existingRule)
@@ -255,12 +200,6 @@ func TestManager_PatchRule_PayloadVariations(t *testing.T) {
assert.Nil(t, findTaskByName(manager.RuleTasks(), taskName), "Task should be removed for disabled rule")
} else {
syncCompleted := waitForTaskSync(manager, taskName, true, 2*time.Second)
// Verify notification config
config, err := nfmanager.GetNotificationConfig(orgId, result.Id)
assert.NoError(t, err)
assert.Equal(t, tc.Config, config)
assert.True(t, syncCompleted, "Task synchronization should complete within timeout")
assert.NotNil(t, findTaskByName(manager.RuleTasks(), taskName), "Task should be created/updated for enabled rule")
assert.Greater(t, len(manager.Rules()), 0, "Rules should be updated in manager")
@@ -295,7 +234,7 @@ func findTaskByName(tasks []Task, taskName string) Task {
return nil
}
func setupTestManager(t *testing.T) (*Manager, *rulestoretest.MockSQLRuleStore, *nfroutingstoretest.MockSQLRouteStore, nfmanager.NotificationManager, string) {
func setupTestManager(t *testing.T) (*Manager, *rulestoretest.MockSQLRuleStore, string) {
settings := instrumentationtest.New().ToProviderSettings()
testDB := utils.NewQueryServiceDBForTests(t)
@@ -327,11 +266,7 @@ func setupTestManager(t *testing.T) (*Manager, *rulestoretest.MockSQLRuleStore,
t.Fatalf("Failed to create noop sharder: %v", err)
}
orgGetter := implorganization.NewGetter(implorganization.NewStore(testDB), noopSharder)
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
notificationManager, err := rulebasednotification.New(t.Context(), settings, nfmanager.Config{}, routeStore)
if err != nil {
t.Fatalf("Failed to create alert manager: %v", err)
}
notificationManager := nfmanagertest.NewMock()
alertManager, err := signozalertmanager.New(context.TODO(), settings, alertmanager.Config{Provider: "signoz", Signoz: alertmanager.Signoz{PollInterval: 10 * time.Second, Config: alertmanagerserver.NewConfig()}}, testDB, orgGetter, notificationManager)
if err != nil {
t.Fatalf("Failed to create alert manager: %v", err)
@@ -355,40 +290,21 @@ func setupTestManager(t *testing.T) (*Manager, *rulestoretest.MockSQLRuleStore,
}
close(manager.block)
return manager, mockSQLRuleStore, routeStore, notificationManager, testOrgID.StringValue()
return manager, mockSQLRuleStore, testOrgID.StringValue()
}
func TestCreateRule(t *testing.T) {
claims := &authtypes.Claims{
Email: "test@example.com",
}
manager, mockSQLRuleStore, mockRouteStore, nfmanager, orgId := setupTestManager(t)
manager, mockSQLRuleStore, orgId := setupTestManager(t)
claims.OrgID = orgId
testCases := []struct {
name string
Route []*alertmanagertypes.RoutePolicy
Config *alertmanagertypes.NotificationConfig
ruleStr string
}{
{
name: "validate stored rule data structure",
Route: []*alertmanagertypes.RoutePolicy{
{
Expression: fmt.Sprintf("ruleId == \"{{.ruleId}}\" && threshold.name == \"warning\""),
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Channels: []string{"test-alerts"},
Name: "{{.ruleId}}",
Enabled: true,
},
},
Config: &alertmanagertypes.NotificationConfig{
NotificationGroup: map[model.LabelName]struct{}{model.LabelName("ruleId"): {}},
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 4 * time.Hour,
NoDataInterval: 4 * time.Hour,
},
UsePolicy: false,
},
ruleStr: `{
"alert": "cpu usage",
"ruleType": "threshold_rule",
@@ -425,30 +341,6 @@ func TestCreateRule(t *testing.T) {
},
{
name: "create complete v2 rule with thresholds",
Route: []*alertmanagertypes.RoutePolicy{
{
Expression: fmt.Sprintf("ruleId == \"{{.ruleId}}\" && threshold.name == \"critical\""),
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Channels: []string{"test-alerts"},
Name: "{{.ruleId}}",
Enabled: true,
},
{
Expression: fmt.Sprintf("ruleId == \"{{.ruleId}}\" && threshold.name == \"warning\""),
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Channels: []string{"test-alerts"},
Name: "{{.ruleId}}",
Enabled: true,
},
},
Config: &alertmanagertypes.NotificationConfig{
NotificationGroup: map[model.LabelName]struct{}{model.LabelName("k8s.node.name"): {}, model.LabelName("ruleId"): {}},
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 10 * time.Minute,
NoDataInterval: 4 * time.Hour,
},
UsePolicy: false,
},
ruleStr: `{
"schemaVersion":"v2",
"state": "firing",
@@ -507,18 +399,6 @@ func TestCreateRule(t *testing.T) {
"frequency": "1m"
}
},
"notificationSettings": {
"GroupBy": [
"k8s.node.name"
],
"renotify": {
"interval": "10m",
"enabled": true,
"alertStates": [
"firing"
]
}
},
"labels": {
"severity": "warning"
},
@@ -549,20 +429,6 @@ func TestCreateRule(t *testing.T) {
},
OrgID: claims.OrgID,
}
// Update route expectations with actual rule ID
routesWithRuleID := make([]*alertmanagertypes.RoutePolicy, len(tc.Route))
for i, route := range tc.Route {
routesWithRuleID[i] = &alertmanagertypes.RoutePolicy{
Expression: strings.Replace(route.Expression, "{{.ruleId}}", rule.ID.String(), -1),
ExpressionKind: route.ExpressionKind,
Channels: route.Channels,
Name: strings.Replace(route.Name, "{{.ruleId}}", rule.ID.String(), -1),
Enabled: route.Enabled,
}
}
mockRouteStore.ExpectCreateBatch(routesWithRuleID)
mockSQLRuleStore.ExpectCreateRule(rule)
ctx := authtypes.NewContextWithClaims(context.Background(), *claims)
@@ -575,12 +441,6 @@ func TestCreateRule(t *testing.T) {
// Wait for task creation with proper synchronization
taskName := prepareTaskName(result.Id)
syncCompleted := waitForTaskSync(manager, taskName, true, 2*time.Second)
// Verify notification config
config, err := nfmanager.GetNotificationConfig(orgId, result.Id)
assert.NoError(t, err)
assert.Equal(t, tc.Config, config)
assert.True(t, syncCompleted, "Task creation should complete within timeout")
assert.NotNil(t, findTaskByName(manager.RuleTasks(), taskName), "Task should be created with correct name")
assert.Greater(t, len(manager.Rules()), 0, "Rules should be added to manager")
@@ -595,35 +455,14 @@ func TestEditRule(t *testing.T) {
claims := &authtypes.Claims{
Email: "test@example.com",
}
manager, mockSQLRuleStore, mockRouteStore, nfmanager, orgId := setupTestManager(t)
manager, mockSQLRuleStore, orgId := setupTestManager(t)
claims.OrgID = orgId
testCases := []struct {
ruleID string
name string
Route []*alertmanagertypes.RoutePolicy
Config *alertmanagertypes.NotificationConfig
ruleStr string
}{
{
ruleID: "12345678-1234-1234-1234-123456789012",
name: "validate edit rule functionality",
Route: []*alertmanagertypes.RoutePolicy{
{
Expression: fmt.Sprintf("ruleId == \"rule1\" && threshold.name == \"critical\""),
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Channels: []string{"critical-alerts"},
Name: "12345678-1234-1234-1234-123456789012",
Enabled: true,
},
},
Config: &alertmanagertypes.NotificationConfig{
NotificationGroup: map[model.LabelName]struct{}{model.LabelName("ruleId"): {}},
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 4 * time.Hour,
NoDataInterval: 4 * time.Hour,
},
UsePolicy: false,
},
name: "validate edit rule functionality",
ruleStr: `{
"alert": "updated cpu usage",
"ruleType": "threshold_rule",
@@ -659,32 +498,7 @@ func TestEditRule(t *testing.T) {
}`,
},
{
ruleID: "12345678-1234-1234-1234-123456789013",
name: "edit complete v2 rule with thresholds",
Route: []*alertmanagertypes.RoutePolicy{
{
Expression: fmt.Sprintf("ruleId == \"rule2\" && threshold.name == \"critical\""),
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Channels: []string{"test-alerts"},
Name: "12345678-1234-1234-1234-123456789013",
Enabled: true,
},
{
Expression: fmt.Sprintf("ruleId == \"rule2\" && threshold.name == \"warning\""),
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Channels: []string{"test-alerts"},
Name: "12345678-1234-1234-1234-123456789013",
Enabled: true,
},
},
Config: &alertmanagertypes.NotificationConfig{
NotificationGroup: map[model.LabelName]struct{}{model.LabelName("ruleId"): {}, model.LabelName("k8s.node.name"): {}},
Renotify: alertmanagertypes.ReNotificationConfig{
RenotifyInterval: 10 * time.Minute,
NoDataInterval: 4 * time.Hour,
},
UsePolicy: false,
},
name: "edit complete v2 rule with thresholds",
ruleStr: `{
"schemaVersion":"v2",
"state": "firing",
@@ -746,18 +560,6 @@ func TestEditRule(t *testing.T) {
"labels": {
"severity": "critical"
},
"notificationSettings": {
"GroupBy": [
"k8s.node.name"
],
"renotify": {
"interval": "10m",
"enabled": true,
"alertStates": [
"firing"
]
}
},
"annotations": {
"description": "This alert is fired when memory usage crosses the threshold",
"summary": "Memory usage threshold exceeded"
@@ -771,13 +573,11 @@ func TestEditRule(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ruleId, err := valuer.NewUUID(tc.ruleID)
if err != nil {
t.Errorf("error creating ruleId: %s", err)
}
ruleID := valuer.GenerateUUID()
existingRule := &ruletypes.Rule{
Identifiable: types.Identifiable{
ID: ruleId,
ID: ruleID,
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
@@ -790,24 +590,18 @@ func TestEditRule(t *testing.T) {
Data: `{"alert": "original cpu usage", "disabled": false}`,
OrgID: claims.OrgID,
}
mockRouteStore.ExpectDeleteRouteByName(existingRule.OrgID, ruleId.String())
mockRouteStore.ExpectCreateBatch(tc.Route)
mockSQLRuleStore.ExpectGetStoredRule(ruleId, existingRule)
mockSQLRuleStore.ExpectGetStoredRule(ruleID, existingRule)
mockSQLRuleStore.ExpectEditRule(existingRule)
ctx := authtypes.NewContextWithClaims(context.Background(), *claims)
err = manager.EditRule(ctx, tc.ruleStr, ruleId)
err := manager.EditRule(ctx, tc.ruleStr, ruleID)
assert.NoError(t, err)
// Wait for task update with proper synchronization
taskName := prepareTaskName(ruleId.String())
taskName := prepareTaskName(ruleID.StringValue())
syncCompleted := waitForTaskSync(manager, taskName, true, 2*time.Second)
config, err := nfmanager.GetNotificationConfig(orgId, ruleId.String())
assert.NoError(t, err)
assert.Equal(t, tc.Config, config)
assert.True(t, syncCompleted, "Task update should complete within timeout")
assert.NotNil(t, findTaskByName(manager.RuleTasks(), taskName), "Task should be updated with correct name")
assert.Greater(t, len(manager.Rules()), 0, "Rules should be updated in manager")

View File

@@ -147,19 +147,13 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
var alerts = make(map[uint64]*ruletypes.Alert, len(res))
ruleReceivers := r.Threshold.GetRuleReceivers()
ruleReceiverMap := make(map[string][]string)
for _, value := range ruleReceivers {
ruleReceiverMap[value.Name] = value.Channels
}
for _, series := range res {
if len(series.Floats) == 0 {
continue
}
results, err := r.Threshold.ShouldAlert(toCommonSeries(series), r.Unit())
results, err := r.Threshold.ShouldAlert(toCommonSeries(series))
if err != nil {
return nil, err
}
@@ -171,7 +165,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
}
r.logger.DebugContext(ctx, "alerting for series", "rule_name", r.Name(), "series", series)
threshold := valueFormatter.Format(result.Target, result.TargetUnit)
threshold := valueFormatter.Format(r.targetVal(), r.Unit())
tmplData := ruletypes.AlertTemplateData(l, valueFormatter.Format(result.V, r.Unit()), threshold)
// Inject some convenience variables that are easier to remember for users
@@ -224,6 +218,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
r.lastError = err
return nil, err
}
alerts[h] = &ruletypes.Alert{
Labels: lbs,
QueryResultLables: resultLabels,
@@ -232,12 +227,13 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
State: model.StatePending,
Value: result.V,
GeneratorURL: r.GeneratorURL(),
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
Receivers: r.preferredChannels,
}
}
}
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
// alerts[h] is ready, add or update active list now
for h, a := range alerts {
// Check whether we already have alerting state for the identifying label set.
@@ -245,9 +241,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
alert.Value = a.Value
alert.Annotations = a.Annotations
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
alert.Receivers = ruleReceiverMap[v]
}
alert.Receivers = r.preferredChannels
continue
}

View File

@@ -696,7 +696,7 @@ func TestPromRuleShouldAlert(t *testing.T) {
assert.NoError(t, err)
}
resultVectors, err := rule.Threshold.ShouldAlert(toCommonSeries(c.values), rule.Unit())
resultVectors, err := rule.Threshold.ShouldAlert(toCommonSeries(c.values))
assert.NoError(t, err)
// Compare full result vector with expected vector

View File

@@ -38,6 +38,7 @@ func defaultTestNotification(opts PrepareTestRuleOptions) (int, *model.ApiError)
if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
// add special labels for test alerts
parsedRule.Annotations[labels.AlertSummaryLabel] = fmt.Sprintf("The rule threshold is set to %.4f, and the observed metric value is {{$value}}.", *parsedRule.RuleCondition.Target)
parsedRule.Labels[labels.RuleSourceLabel] = ""
parsedRule.Labels[labels.AlertRuleIdLabel] = ""

View File

@@ -488,7 +488,7 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID,
continue
}
}
resultSeries, err := r.Threshold.ShouldAlert(*series, r.Unit())
resultSeries, err := r.Threshold.ShouldAlert(*series)
if err != nil {
return nil, err
}
@@ -565,7 +565,7 @@ func (r *ThresholdRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUI
continue
}
}
resultSeries, err := r.Threshold.ShouldAlert(*series, r.Unit())
resultSeries, err := r.Threshold.ShouldAlert(*series)
if err != nil {
return nil, err
}
@@ -602,12 +602,6 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er
resultFPs := map[uint64]struct{}{}
var alerts = make(map[uint64]*ruletypes.Alert, len(res))
ruleReceivers := r.Threshold.GetRuleReceivers()
ruleReceiverMap := make(map[string][]string)
for _, value := range ruleReceivers {
ruleReceiverMap[value.Name] = value.Channels
}
for _, smpl := range res {
l := make(map[string]string, len(smpl.Metric))
for _, lbl := range smpl.Metric {
@@ -616,7 +610,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er
value := valueFormatter.Format(smpl.V, r.Unit())
//todo(aniket): handle different threshold
threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
threshold := valueFormatter.Format(r.targetVal(), r.Unit())
r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
@@ -696,7 +690,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er
State: model.StatePending,
Value: smpl.V,
GeneratorURL: r.GeneratorURL(),
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
Receivers: r.preferredChannels,
Missing: smpl.IsMissing,
}
}
@@ -711,9 +705,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er
alert.Value = a.Value
alert.Annotations = a.Annotations
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
alert.Receivers = ruleReceiverMap[v]
}
alert.Receivers = r.preferredChannels
continue
}

View File

@@ -824,7 +824,7 @@ func TestThresholdRuleShouldAlert(t *testing.T) {
values.Points[i].Timestamp = time.Now().UnixMilli()
}
resultVectors, err := rule.Threshold.ShouldAlert(c.values, rule.Unit())
resultVectors, err := rule.Threshold.ShouldAlert(c.values)
assert.NoError(t, err, "Test case %d", idx)
// Compare result vectors with expected behavior
@@ -1201,7 +1201,7 @@ func TestThresholdRuleLabelNormalization(t *testing.T) {
values.Points[i].Timestamp = time.Now().UnixMilli()
}
vector, err := rule.Threshold.ShouldAlert(c.values, rule.Unit())
vector, err := rule.Threshold.ShouldAlert(c.values)
assert.NoError(t, err)
for name, value := range c.values.Labels {
@@ -1211,7 +1211,7 @@ func TestThresholdRuleLabelNormalization(t *testing.T) {
}
// Get result vectors from threshold evaluation
resultVectors, err := rule.Threshold.ShouldAlert(c.values, rule.Unit())
resultVectors, err := rule.Threshold.ShouldAlert(c.values)
assert.NoError(t, err, "Test case %d", idx)
// Compare result vectors with expected behavior
@@ -1501,11 +1501,13 @@ func TestThresholdRuleUnitCombinations(t *testing.T) {
Kind: ruletypes.BasicThresholdKind,
Spec: ruletypes.BasicRuleThresholds{
{
Name: postableRule.AlertName,
TargetValue: &c.target,
TargetUnit: c.targetUnit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
Name: postableRule.AlertName,
TargetValue: &c.target,
TargetUnit: c.targetUnit,
RuleUnit: postableRule.RuleCondition.CompositeQuery.Unit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
SelectedQuery: postableRule.RuleCondition.SelectedQuery,
},
},
}
@@ -1610,10 +1612,12 @@ func TestThresholdRuleNoData(t *testing.T) {
Kind: ruletypes.BasicThresholdKind,
Spec: ruletypes.BasicRuleThresholds{
{
Name: postableRule.AlertName,
TargetValue: &target,
MatchType: ruletypes.AtleastOnce,
CompareOp: ruletypes.ValueIsEq,
Name: postableRule.AlertName,
TargetValue: &target,
RuleUnit: postableRule.RuleCondition.CompositeQuery.Unit,
MatchType: ruletypes.AtleastOnce,
CompareOp: ruletypes.ValueIsEq,
SelectedQuery: postableRule.RuleCondition.SelectedQuery,
},
},
}
@@ -1730,11 +1734,13 @@ func TestThresholdRuleTracesLink(t *testing.T) {
Kind: ruletypes.BasicThresholdKind,
Spec: ruletypes.BasicRuleThresholds{
{
Name: postableRule.AlertName,
TargetValue: &c.target,
TargetUnit: c.targetUnit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
Name: postableRule.AlertName,
TargetValue: &c.target,
TargetUnit: c.targetUnit,
RuleUnit: postableRule.RuleCondition.CompositeQuery.Unit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
SelectedQuery: postableRule.RuleCondition.SelectedQuery,
},
},
}
@@ -1867,11 +1873,13 @@ func TestThresholdRuleLogsLink(t *testing.T) {
Kind: ruletypes.BasicThresholdKind,
Spec: ruletypes.BasicRuleThresholds{
{
Name: postableRule.AlertName,
TargetValue: &c.target,
TargetUnit: c.targetUnit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
Name: postableRule.AlertName,
TargetValue: &c.target,
TargetUnit: c.targetUnit,
RuleUnit: postableRule.RuleCondition.CompositeQuery.Unit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
SelectedQuery: postableRule.RuleCondition.SelectedQuery,
},
},
}
@@ -2117,18 +2125,22 @@ func TestMultipleThresholdRule(t *testing.T) {
Kind: ruletypes.BasicThresholdKind,
Spec: ruletypes.BasicRuleThresholds{
{
Name: "first_threshold",
TargetValue: &c.target,
TargetUnit: c.targetUnit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
Name: "first_threshold",
TargetValue: &c.target,
TargetUnit: c.targetUnit,
RuleUnit: postableRule.RuleCondition.CompositeQuery.Unit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
SelectedQuery: postableRule.RuleCondition.SelectedQuery,
},
{
Name: "second_threshold",
TargetValue: &c.secondTarget,
TargetUnit: c.targetUnit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
Name: "second_threshold",
TargetValue: &c.secondTarget,
TargetUnit: c.targetUnit,
RuleUnit: postableRule.RuleCondition.CompositeQuery.Unit,
MatchType: ruletypes.MatchType(c.matchType),
CompareOp: ruletypes.CompareOp(c.compareOp),
SelectedQuery: postableRule.RuleCondition.SelectedQuery,
},
},
}

View File

@@ -853,7 +853,7 @@ func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any {
}
}
if len(fieldKeysForName) > 1 {
if len(fieldKeysForName) > 1 && !v.keysWithWarnings[keyName] {
warnMsg := fmt.Sprintf(
"Key `%s` is ambiguous, found %d different combinations of field context / data type: %v.",
fieldKey.Name,
@@ -865,7 +865,6 @@ func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any {
mixedFieldContext[item.FieldContext.StringValue()] = true
}
// when there is both resource and attribute context, default to resource only
if mixedFieldContext[telemetrytypes.FieldContextResource.StringValue()] &&
mixedFieldContext[telemetrytypes.FieldContextAttribute.StringValue()] {
filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
@@ -879,12 +878,9 @@ func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any {
warnMsg += " " + "Using `resource` context by default. To query attributes explicitly, " +
fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s')", fieldKey.Name)
}
if !v.keysWithWarnings[keyName] {
v.mainWarnURL = "https://signoz.io/docs/userguide/field-context-data-types/"
// this is warning state, we must have a unambiguous key
v.warnings = append(v.warnings, warnMsg)
}
v.mainWarnURL = "https://signoz.io/docs/userguide/field-context-data-types/"
// this is warning state, we must have a unambiguous key
v.warnings = append(v.warnings, warnMsg)
v.keysWithWarnings[keyName] = true
v.logger.Warn("ambiguous key", "field_key_name", fieldKey.Name) //nolint:sloglint
}

View File

@@ -38,7 +38,6 @@ import (
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/clickhousetelemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystorehook"
routeTypes "github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/web"
"github.com/SigNoz/signoz/pkg/web/noopweb"
@@ -134,7 +133,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewQueryBuilderV5MigrationFactory(sqlstore, telemetryStore),
sqlmigration.NewAddMeterQuickFiltersFactory(sqlstore, sqlschema),
sqlmigration.NewUpdateTTLSettingForCustomRetentionFactory(sqlstore, sqlschema),
sqlmigration.NewAddRoutePolicyFactory(sqlstore, sqlschema),
)
}
@@ -157,9 +155,9 @@ func NewPrometheusProviderFactories(telemetryStore telemetrystore.TelemetryStore
)
}
func NewNotificationManagerProviderFactories(routeStore routeTypes.RouteStore) factory.NamedMap[factory.ProviderFactory[nfmanager.NotificationManager, nfmanager.Config]] {
func NewNotificationManagerProviderFactories() factory.NamedMap[factory.ProviderFactory[nfmanager.NotificationManager, nfmanager.Config]] {
return factory.MustNewNamedMap(
rulebasednotification.NewFactory(routeStore),
rulebasednotification.NewFactory(),
)
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/sqlroutingstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/emailing"
@@ -231,14 +230,12 @@ func New(
// Initialize user getter
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
// will need to create factory for all stores
routeStore := sqlroutingstore.NewStore(sqlstore)
// shared NotificationManager instance for both alertmanager and rules
notificationManager, err := factory.NewProviderFromNamedMap(
ctx,
providerSettings,
nfmanager.Config{},
NewNotificationManagerProviderFactories(routeStore),
NewNotificationManagerProviderFactories(),
"rulebased",
)
if err != nil {

View File

@@ -1,260 +0,0 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"log/slog"
"time"
)
// Shared types for migration
type expressionRoute struct {
bun.BaseModel `bun:"table:route_policy"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
Expression string `bun:"expression,type:text"`
ExpressionKind string `bun:"kind,type:text"`
Channels []string `bun:"channels,type:text"`
Name string `bun:"name,type:text"`
Description string `bun:"description,type:text"`
Enabled bool `bun:"enabled,type:boolean,default:true"`
Tags []string `bun:"tags,type:text"`
OrgID string `bun:"org_id,type:text"`
}
type rule struct {
bun.BaseModel `bun:"table:rule"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
Deleted int `bun:"deleted,default:0"`
Data string `bun:"data,type:text"`
OrgID string `bun:"org_id,type:text"`
}
type addRoutePolicies struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
logger *slog.Logger
}
func NewAddRoutePolicyFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_route_policy"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return newAddRoutePolicy(ctx, providerSettings, config, sqlstore, sqlschema)
})
}
func newAddRoutePolicy(_ context.Context, settings factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
return &addRoutePolicies{
sqlstore: sqlstore,
sqlschema: sqlschema,
logger: settings.Logger,
}, nil
}
func (migration *addRoutePolicies) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addRoutePolicies) Up(ctx context.Context, db *bun.DB) error {
_, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("route_policy"))
if err == nil {
return nil
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
sqls := [][]byte{}
// Create the route_policy table
table := &sqlschema.Table{
Name: "route_policy",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "created_by", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "updated_by", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "expression", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "kind", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "channels", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "description", DataType: sqlschema.DataTypeText, Nullable: true},
{Name: "enabled", DataType: sqlschema.DataTypeBoolean, Nullable: false, Default: "true"},
{Name: "tags", DataType: sqlschema.DataTypeText, Nullable: true},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: "org_id",
ReferencedTableName: "organizations",
ReferencedColumnName: "id",
},
},
}
tableSQLs := migration.sqlschema.Operator().CreateTable(table)
sqls = append(sqls, tableSQLs...)
for _, sqlStmt := range sqls {
if _, err := tx.ExecContext(ctx, string(sqlStmt)); err != nil {
return err
}
}
err = migration.migrateRulesToRoutePolicies(ctx, tx)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *addRoutePolicies) migrateRulesToRoutePolicies(ctx context.Context, tx bun.Tx) error {
var rules []*rule
err := tx.NewSelect().
Model(&rules).
Where("deleted = ?", 0).
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil // No rules to migrate
}
return errors.NewInternalf(errors.CodeInternal, "failed to fetch rules")
}
channelsByOrg, err := migration.getAllChannels(ctx, tx)
if err != nil {
return errors.NewInternalf(errors.CodeInternal, "fetching channels error: %v", err)
}
var routesToInsert []*expressionRoute
routesToInsert, err = migration.convertRulesToRoutes(rules, channelsByOrg)
if err != nil {
return errors.NewInternalf(errors.CodeInternal, "converting rules to routes error: %v", err)
}
// Insert all routes in a single batch operation
if len(routesToInsert) > 0 {
_, err = tx.NewInsert().
Model(&routesToInsert).
Exec(ctx)
if err != nil {
return errors.NewInternalf(errors.CodeInternal, "failed to insert notification routes")
}
}
return nil
}
func (migration *addRoutePolicies) convertRulesToRoutes(rules []*rule, channelsByOrg map[string][]string) ([]*expressionRoute, error) {
var routes []*expressionRoute
for _, r := range rules {
var gettableRule ruletypes.GettableRule
if err := json.Unmarshal([]byte(r.Data), &gettableRule); err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "failed to unmarshal rule data for rule ID %s: %v", r.ID, err)
}
if len(gettableRule.PreferredChannels) == 0 {
channels, exists := channelsByOrg[r.OrgID]
if !exists || len(channels) == 0 {
continue
}
gettableRule.PreferredChannels = channels
}
severity := "critical"
if v, ok := gettableRule.Labels["severity"]; ok {
severity = v
}
expression := fmt.Sprintf(`%s == "%s" && %s == "%s"`, "threshold.name", severity, "ruleId", r.ID.String())
route := &expressionRoute{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: r.CreatedBy,
UpdatedBy: r.UpdatedBy,
},
Expression: expression,
ExpressionKind: "rule",
Channels: gettableRule.PreferredChannels,
Name: r.ID.StringValue(),
Enabled: true,
OrgID: r.OrgID,
}
routes = append(routes, route)
}
return routes, nil
}
func (migration *addRoutePolicies) getAllChannels(ctx context.Context, tx bun.Tx) (map[string][]string, error) {
type channel struct {
bun.BaseModel `bun:"table:notification_channel"`
types.Identifiable
types.TimeAuditable
Name string `json:"name" bun:"name"`
Type string `json:"type" bun:"type"`
Data string `json:"data" bun:"data"`
OrgID string `json:"org_id" bun:"org_id"`
}
var channels []*channel
err := tx.NewSelect().
Model(&channels).
Scan(ctx)
if err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "failed to fetch all channels")
}
// Group channels by org ID
channelsByOrg := make(map[string][]string)
for _, ch := range channels {
channelsByOrg[ch.OrgID] = append(channelsByOrg[ch.OrgID], ch.Name)
}
return channelsByOrg, nil
}
func (migration *addRoutePolicies) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -114,3 +114,78 @@ func (index *UniqueIndex) ToDropSQL(fmter SQLFormatter) []byte {
return sql
}
type NormalIndex struct {
TableName TableName
ColumnNames []ColumnName
name string
}
func (index *NormalIndex) Name() string {
if index.name != "" {
return index.name
}
var b strings.Builder
b.WriteString(IndexTypeIndex.String())
b.WriteString("_")
b.WriteString(string(index.TableName))
b.WriteString("_")
for i, column := range index.ColumnNames {
if i > 0 {
b.WriteString("_")
}
b.WriteString(string(column))
}
return b.String()
}
func (index *NormalIndex) Named(name string) Index {
copyOfColumnNames := make([]ColumnName, len(index.ColumnNames))
copy(copyOfColumnNames, index.ColumnNames)
return &NormalIndex{
TableName: index.TableName,
ColumnNames: copyOfColumnNames,
name: name,
}
}
func (*NormalIndex) Type() IndexType {
return IndexTypeIndex
}
func (index *NormalIndex) Columns() []ColumnName {
return index.ColumnNames
}
func (index *NormalIndex) ToCreateSQL(fmter SQLFormatter) []byte {
sql := []byte{}
sql = append(sql, "CREATE INDEX IF NOT EXISTS "...)
sql = fmter.AppendIdent(sql, index.Name())
sql = append(sql, " ON "...)
sql = fmter.AppendIdent(sql, string(index.TableName))
sql = append(sql, " ("...)
for i, column := range index.ColumnNames {
if i > 0 {
sql = append(sql, ", "...)
}
sql = fmter.AppendIdent(sql, string(column))
}
sql = append(sql, ")"...)
return sql
}
func (index *NormalIndex) ToDropSQL(fmter SQLFormatter) []byte {
sql := []byte{}
sql = append(sql, "DROP INDEX IF EXISTS "...)
sql = fmter.AppendIdent(sql, index.Name())
return sql
}

View File

@@ -38,6 +38,31 @@ func TestIndexToCreateSQL(t *testing.T) {
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "my_index" ON "users" ("id", "name", "email")`,
},
{
name: "Normal_1Column",
index: &NormalIndex{
TableName: "users",
ColumnNames: []ColumnName{"org_id"},
},
sql: `CREATE INDEX IF NOT EXISTS "ix_users_org_id" ON "users" ("org_id")`,
},
{
name: "Normal_2Columns",
index: &NormalIndex{
TableName: "users",
ColumnNames: []ColumnName{"org_id", "status"},
},
sql: `CREATE INDEX IF NOT EXISTS "ix_users_org_id_status" ON "users" ("org_id", "status")`,
},
{
name: "Normal_3Columns_Named",
index: &NormalIndex{
TableName: "route_policy",
ColumnNames: []ColumnName{"org_id", "enabled", "kind"},
name: "idx_custom_name",
},
sql: `CREATE INDEX IF NOT EXISTS "idx_custom_name" ON "route_policy" ("org_id", "enabled", "kind")`,
},
}
for _, testCase := range testCases {

View File

@@ -166,10 +166,11 @@ func (c *conditionBuilder) conditionFor(
var value any
switch column.Type {
case schema.JSONColumnType{}:
value = "NULL"
if operator == qbtypes.FilterOperatorExists {
return sb.IsNotNull(tblFieldName), nil
return sb.NE(tblFieldName, value), nil
} else {
return sb.IsNull(tblFieldName), nil
return sb.E(tblFieldName, value), nil
}
case schema.ColumnTypeString, schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}:
value = ""

View File

@@ -233,30 +233,6 @@ func TestConditionFor(t *testing.T) {
expectedArgs: []any{true},
expectedError: nil,
},
{
name: "Exists operator - json field",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "WHERE multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL",
expectedError: nil,
},
{
name: "Not Exists operator - json field",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotExists,
value: nil,
expectedSQL: "WHERE multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NULL",
expectedError: nil,
},
{
name: "Non-existent column",
key: telemetrytypes.TelemetryFieldKey{

View File

@@ -452,8 +452,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with conditions",
query: "error service.name=authentication",
shouldPass: true,
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))",
expectedArgs: []any{"error", "authentication"},
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{"error", "authentication", "NULL"},
expectedErrorContains: "",
},
@@ -810,8 +810,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Basic equality",
query: "service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)",
expectedArgs: []any{"api"},
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@@ -1170,16 +1170,16 @@ func TestFilterExprLogs(t *testing.T) {
category: "IN operator (parentheses)",
query: "service.name IN (\"api\", \"web\", \"auth\")",
shouldPass: true,
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ?) AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)",
expectedArgs: []any{"api", "web", "auth"},
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ?) AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "web", "auth", "NULL"},
expectedErrorContains: "",
},
{
category: "IN operator (parentheses)",
query: "environment IN (\"dev\", \"test\", \"staging\", \"prod\")",
shouldPass: true,
expectedQuery: "WHERE ((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ?) AND multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) IS NOT NULL)",
expectedArgs: []any{"dev", "test", "staging", "prod"},
expectedQuery: "WHERE ((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ?) AND multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ?)",
expectedArgs: []any{"dev", "test", "staging", "prod", "NULL"},
expectedErrorContains: "",
},
@@ -1204,16 +1204,16 @@ func TestFilterExprLogs(t *testing.T) {
category: "IN operator (brackets)",
query: "service.name IN [\"api\", \"web\", \"auth\"]",
shouldPass: true,
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ?) AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)",
expectedArgs: []any{"api", "web", "auth"},
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ?) AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "web", "auth", "NULL"},
expectedErrorContains: "",
},
{
category: "IN operator (brackets)",
query: "environment IN [\"dev\", \"test\", \"staging\", \"prod\"]",
shouldPass: true,
expectedQuery: "WHERE ((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ?) AND multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) IS NOT NULL)",
expectedArgs: []any{"dev", "test", "staging", "prod"},
expectedQuery: "WHERE ((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ?) AND multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ?)",
expectedArgs: []any{"dev", "test", "staging", "prod", "NULL"},
expectedErrorContains: "",
},
@@ -1318,13 +1318,6 @@ func TestFilterExprLogs(t *testing.T) {
expectedArgs: []any{true},
expectedErrorContains: "",
},
{
category: "EXISTS operator on resource",
query: "service.name EXISTS",
shouldPass: true,
expectedQuery: "WHERE multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL",
expectedErrorContains: "",
},
// NOT EXISTS
{
@@ -1367,13 +1360,6 @@ func TestFilterExprLogs(t *testing.T) {
expectedArgs: []any{true},
expectedErrorContains: "",
},
{
category: "EXISTS operator on resource",
query: "service.name NOT EXISTS",
shouldPass: true,
expectedQuery: "WHERE multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NULL",
expectedErrorContains: "",
},
// Basic REGEXP
{
@@ -1544,8 +1530,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Explicit AND",
query: "status=200 AND service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))",
expectedArgs: []any{float64(200), true, "api"},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
expectedErrorContains: "",
},
{
@@ -1578,8 +1564,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Explicit OR",
query: "service.name=\"api\" OR service.name=\"web\"",
shouldPass: true,
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))",
expectedArgs: []any{"api", "web"},
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{"api", "NULL", "web", "NULL"},
expectedErrorContains: "",
},
{
@@ -1604,8 +1590,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "NOT with expressions",
query: "NOT service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))",
expectedArgs: []any{"api"},
expectedQuery: "WHERE NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@@ -1622,8 +1608,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "AND + OR combinations",
query: "status=200 AND (service.name=\"api\" OR service.name=\"web\")",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))))",
expectedArgs: []any{float64(200), true, "api", "web"},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))))",
expectedArgs: []any{float64(200), true, "api", "NULL", "web", "NULL"},
expectedErrorContains: "",
},
{
@@ -1648,8 +1634,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "AND + NOT combinations",
query: "status=200 AND NOT service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)))",
expectedArgs: []any{float64(200), true, "api"},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
expectedErrorContains: "",
},
{
@@ -1666,8 +1652,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "OR + NOT combinations",
query: "NOT status=200 OR NOT service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) OR NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)))",
expectedArgs: []any{float64(200), true, "api"},
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) OR NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
expectedErrorContains: "",
},
{
@@ -1684,8 +1670,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "AND + OR + NOT combinations",
query: "status=200 AND (service.name=\"api\" OR NOT duration>1000)",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR NOT ((toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?)))))",
expectedArgs: []any{float64(200), true, "api", float64(1000), true},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR NOT ((toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?)))))",
expectedArgs: []any{float64(200), true, "api", "NULL", float64(1000), true},
expectedErrorContains: "",
},
{
@@ -1700,8 +1686,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "AND + OR + NOT combinations",
query: "NOT (status=200 AND service.name=\"api\") OR count>0",
shouldPass: true,
expectedQuery: "WHERE (NOT ((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)))) OR (toFloat64(attributes_number['count']) > ? AND mapContains(attributes_number, 'count') = ?))",
expectedArgs: []any{float64(200), true, "api", float64(0), true},
expectedQuery: "WHERE (NOT ((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))) OR (toFloat64(attributes_number['count']) > ? AND mapContains(attributes_number, 'count') = ?))",
expectedArgs: []any{float64(200), true, "api", "NULL", float64(0), true},
expectedErrorContains: "",
},
@@ -1710,8 +1696,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Implicit AND",
query: "status=200 service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))",
expectedArgs: []any{float64(200), true, "api"},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
expectedErrorContains: "",
},
{
@@ -1736,8 +1722,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Mixed implicit/explicit AND",
query: "status=200 AND service.name=\"api\" duration<1000",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) AND (toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?))",
expectedArgs: []any{float64(200), true, "api", float64(1000), true},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) AND (toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?))",
expectedArgs: []any{float64(200), true, "api", "NULL", float64(1000), true},
expectedErrorContains: "",
},
{
@@ -1762,8 +1748,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Simple grouping",
query: "service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)",
expectedArgs: []any{"api"},
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@@ -1788,8 +1774,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Nested grouping",
query: "(((service.name=\"api\")))",
shouldPass: true,
expectedQuery: "WHERE ((((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))))",
expectedArgs: []any{"api"},
expectedQuery: "WHERE ((((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))))",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@@ -1806,8 +1792,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Complex nested grouping",
query: "(status=200 AND (service.name=\"api\" OR service.name=\"web\"))",
shouldPass: true,
expectedQuery: "WHERE (((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)))))",
expectedArgs: []any{float64(200), true, "api", "web"},
expectedQuery: "WHERE (((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))))",
expectedArgs: []any{float64(200), true, "api", "NULL", "web", "NULL"},
expectedErrorContains: "",
},
{
@@ -1832,16 +1818,16 @@ func TestFilterExprLogs(t *testing.T) {
category: "Deep nesting",
query: "(((status=200 OR status=201) AND service.name=\"api\") OR ((status=202 OR status=203) AND service.name=\"web\"))",
shouldPass: true,
expectedQuery: "WHERE (((((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))) OR (((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)))))",
expectedArgs: []any{float64(200), true, float64(201), true, "api", float64(202), true, float64(203), true, "web"},
expectedQuery: "WHERE (((((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))) OR (((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))))",
expectedArgs: []any{float64(200), true, float64(201), true, "api", "NULL", float64(202), true, float64(203), true, "web", "NULL"},
expectedErrorContains: "",
},
{
category: "Deep nesting",
query: "(count>0 AND ((duration<1000 AND service.name=\"api\") OR (duration<500 AND service.name=\"web\")))",
shouldPass: true,
expectedQuery: "WHERE (((toFloat64(attributes_number['count']) > ? AND mapContains(attributes_number, 'count') = ?) AND (((((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))) OR (((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)))))))",
expectedArgs: []any{float64(0), true, float64(1000), true, "api", float64(500), true, "web"},
expectedQuery: "WHERE (((toFloat64(attributes_number['count']) > ? AND mapContains(attributes_number, 'count') = ?) AND (((((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))) OR (((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))))))",
expectedArgs: []any{float64(0), true, float64(1000), true, "api", "NULL", float64(500), true, "web", "NULL"},
expectedErrorContains: "",
},
@@ -1850,16 +1836,16 @@ func TestFilterExprLogs(t *testing.T) {
category: "String quote styles",
query: "service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)",
expectedArgs: []any{"api"},
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
category: "String quote styles",
query: "service.name='api'",
shouldPass: true,
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)",
expectedArgs: []any{"api"},
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@@ -2018,29 +2004,29 @@ func TestFilterExprLogs(t *testing.T) {
category: "Operator precedence",
query: "NOT status=200 AND service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))",
expectedArgs: []any{float64(200), true, "api"}, // Should be (NOT status=200) AND service.name="api"
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"}, // Should be (NOT status=200) AND service.name="api"
},
{
category: "Operator precedence",
query: "status=200 AND service.name=\"api\" OR service.name=\"web\"",
shouldPass: true,
expectedQuery: "WHERE (((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))",
expectedArgs: []any{float64(200), true, "api", "web"}, // Should be (status=200 AND service.name="api") OR service.name="web"
expectedQuery: "WHERE (((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL", "web", "NULL"}, // Should be (status=200 AND service.name="api") OR service.name="web"
},
{
category: "Operator precedence",
query: "NOT status=200 OR NOT service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) OR NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)))",
expectedArgs: []any{float64(200), true, "api"}, // Should be (NOT status=200) OR (NOT service.name="api")
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) OR NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))",
expectedArgs: []any{float64(200), true, "api", "NULL"}, // Should be (NOT status=200) OR (NOT service.name="api")
},
{
category: "Operator precedence",
query: "status=200 OR service.name=\"api\" AND level=\"ERROR\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) AND (attributes_string['level'] = ? AND mapContains(attributes_string, 'level') = ?)))",
expectedArgs: []any{float64(200), true, "api", "ERROR", true}, // Should be status=200 OR (service.name="api" AND level="ERROR")
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) AND (attributes_string['level'] = ? AND mapContains(attributes_string, 'level') = ?)))",
expectedArgs: []any{float64(200), true, "api", "NULL", "ERROR", true}, // Should be status=200 OR (service.name="api" AND level="ERROR")
},
// Different whitespace patterns
@@ -2064,8 +2050,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Whitespace patterns",
query: "status=200 AND service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))",
expectedArgs: []any{float64(200), true, "api"}, // Multiple spaces
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"}, // Multiple spaces
},
// More Unicode characters
@@ -2234,8 +2220,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "More common filters",
query: "service.name=\"api\" AND (status>=500 OR duration>1000) AND NOT message CONTAINS \"expected\"",
shouldPass: true,
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) AND (((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?))) AND NOT ((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?)))",
expectedArgs: []any{"api", float64(500), true, float64(1000), true, "%expected%", true},
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) AND (((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?))) AND NOT ((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?)))",
expectedArgs: []any{"api", "NULL", float64(500), true, float64(1000), true, "%expected%", true},
},
// Edge cases
@@ -2300,8 +2286,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Unusual whitespace",
query: "status = 200 AND service.name = \"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL))",
expectedArgs: []any{float64(200), true, "api"},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
},
{
category: "Unusual whitespace",
@@ -2361,13 +2347,13 @@ func TestFilterExprLogs(t *testing.T) {
)
`,
shouldPass: true,
expectedQuery: "WHERE ((((((((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) AND (toFloat64(attributes_number['status']) < ? AND mapContains(attributes_number, 'status') = ?))) OR (((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) AND (toFloat64(attributes_number['status']) < ? AND mapContains(attributes_number, 'status') = ?) AND NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)))))) AND ((((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ?) AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (((multiIf(resource.`service.type` IS NOT NULL, resource.`service.type`::String, mapContains(resources_string, 'service.type'), resources_string['service.type'], NULL) = ? AND multiIf(resource.`service.type` IS NOT NULL, resource.`service.type`::String, mapContains(resources_string, 'service.type'), resources_string['service.type'], NULL) IS NOT NULL) AND NOT ((multiIf(resource.`service.deprecated` IS NOT NULL, resource.`service.deprecated`::String, mapContains(resources_string, 'service.deprecated'), resources_string['service.deprecated'], NULL) = ? AND multiIf(resource.`service.deprecated` IS NOT NULL, resource.`service.deprecated`::String, mapContains(resources_string, 'service.deprecated'), resources_string['service.deprecated'], NULL) IS NOT NULL)))))))) AND (((((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) OR ((toFloat64(attributes_number['duration']) BETWEEN ? AND ? AND mapContains(attributes_number, 'duration') = ?)))) AND ((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ? OR (((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? AND multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) IS NOT NULL) AND (attributes_bool['is_automated_test'] = ? AND mapContains(attributes_bool, 'is_automated_test') = ?))))))) AND NOT ((((((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?) OR (LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?))) AND (attributes_string['severity'] = ? AND mapContains(attributes_string, 'severity') = ?)))))",
expectedQuery: "WHERE ((((((((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) AND (toFloat64(attributes_number['status']) < ? AND mapContains(attributes_number, 'status') = ?))) OR (((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) AND (toFloat64(attributes_number['status']) < ? AND mapContains(attributes_number, 'status') = ?) AND NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)))))) AND ((((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ?) AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (((multiIf(resource.`service.type` IS NOT NULL, resource.`service.type`::String, mapContains(resources_string, 'service.type'), resources_string['service.type'], NULL) = ? AND multiIf(resource.`service.type` IS NOT NULL, resource.`service.type`::String, mapContains(resources_string, 'service.type'), resources_string['service.type'], NULL) <> ?) AND NOT ((multiIf(resource.`service.deprecated` IS NOT NULL, resource.`service.deprecated`::String, mapContains(resources_string, 'service.deprecated'), resources_string['service.deprecated'], NULL) = ? AND multiIf(resource.`service.deprecated` IS NOT NULL, resource.`service.deprecated`::String, mapContains(resources_string, 'service.deprecated'), resources_string['service.deprecated'], NULL) <> ?)))))))) AND (((((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) OR ((toFloat64(attributes_number['duration']) BETWEEN ? AND ? AND mapContains(attributes_number, 'duration') = ?)))) AND ((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ? OR (((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? AND multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ?) AND (attributes_bool['is_automated_test'] = ? AND mapContains(attributes_bool, 'is_automated_test') = ?))))))) AND NOT ((((((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?) OR (LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?))) AND (attributes_string['severity'] = ? AND mapContains(attributes_string, 'severity') = ?)))))",
expectedArgs: []any{
float64(200), true, float64(300), true, float64(400), true, float64(500), true, float64(404), true,
"api", "web", "auth",
"internal", true,
"api", "web", "auth", "NULL",
"internal", "NULL", true, "NULL",
float64(1000), true, float64(1000), float64(5000), true,
"test", "test", true, true,
"test", "test", "NULL", true, true,
"%warning%", true, "%deprecated%", true,
"low", true,
},

View File

@@ -69,8 +69,8 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "NULL", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, "NULL", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
@@ -98,8 +98,8 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "redis-manual", "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, "redis-manual", "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", "redis-manual", "NULL", "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, "NULL", "redis-manual", "NULL", "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
@@ -137,8 +137,8 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "NULL", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, "NULL", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
@@ -488,101 +488,3 @@ func TestStatementBuilderTimeSeriesBodyGroupBy(t *testing.T) {
})
}
}
func TestStatementBuilderListQueryServiceCollision(t *testing.T) {
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
expected qbtypes.Statement
expectedErr error
expectWarn bool
}{
{
name: "default list",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{
Expression: "(service.name = 'cartservice' AND body CONTAINS 'error')",
},
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND true)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((LOWER(body) LIKE LOWER(?))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
expectWarn: true,
},
{
name: "list query with mat col order by",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{
Expression: "service.name = 'cartservice' AND body CONTAINS 'error'",
},
Limit: 10,
Order: []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "materialized.key.name",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (LOWER(body) LIKE LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
expectWarn: true,
},
}
fm := NewFieldMapper()
cb := NewConditionBuilder(fm)
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
mockMetadataStore.KeysMap = buildCompleteFieldKeyMapCollision()
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, "", nil)
resourceFilterStmtBuilder := resourceFilterStmtBuilder()
statementBuilder := NewLogQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
mockMetadataStore,
fm,
cb,
resourceFilterStmtBuilder,
aggExprRewriter,
DefaultFullTextColumn,
BodyJSONStringSearchPrefix,
GetBodyJSONKey,
)
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErr != nil {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErr.Error())
} else {
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
if c.expectWarn {
require.True(t, len(q.Warnings) > 0)
}
}
})
}
}

View File

@@ -899,55 +899,3 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
}
return keysMap
}
func buildCompleteFieldKeyMapCollision() map[string][]*telemetrytypes.TelemetryFieldKey {
keysMap := map[string][]*telemetrytypes.TelemetryFieldKey{
"service.name": {
{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"body": {
{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"error.code": {
{
Name: "error.code",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeInt64,
},
},
"environment": {
{
Name: "environment",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"user.id": {
{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
}
for _, keys := range keysMap {
for _, key := range keys {
key.Signal = telemetrytypes.SignalLogs
}
}
return keysMap
}

View File

@@ -164,10 +164,11 @@ func (c *conditionBuilder) conditionFor(
var value any
switch column.Type {
case schema.JSONColumnType{}:
value = "NULL"
if operator == qbtypes.FilterOperatorExists {
return sb.IsNotNull(tblFieldName), nil
return sb.NE(tblFieldName, value), nil
} else {
return sb.IsNull(tblFieldName), nil
return sb.E(tblFieldName, value), nil
}
case schema.ColumnTypeString,
schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},

View File

@@ -207,30 +207,6 @@ func TestConditionFor(t *testing.T) {
expectedSQL: "mapContains(attributes_string, 'user.id') <> ?",
expectedError: nil,
},
{
name: "Exists operator - json field",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "WHERE multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL",
expectedError: nil,
},
{
name: "Not Exists operator - json field",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotExists,
value: nil,
expectedSQL: "WHERE multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NULL",
expectedError: nil,
},
{
name: "Contains operator - map field",
key: telemetrytypes.TelemetryFieldKey{

View File

@@ -68,14 +68,12 @@ func TestGetFieldKeyName(t *testing.T) {
expectedError: nil,
},
{
name: "Map column type - resource attribute - materialized",
name: "Map column type - resource attribute - legacy",
key: telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
Materialized: true,
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
expectedResult: "multiIf(resource.`deployment.environment` IS NOT NULL, resource.`deployment.environment`::String, `resource_string_deployment$$environment_exists`==true, `resource_string_deployment$$environment`, NULL)",
expectedResult: "multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL)",
expectedError: nil,
},
{

View File

@@ -60,8 +60,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
@@ -89,8 +89,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "redis-manual", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "redis-manual", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", "redis-manual", "NULL", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", "redis-manual", "NULL", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
@@ -187,8 +187,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
@@ -216,8 +216,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
@@ -255,8 +255,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},

View File

@@ -263,8 +263,8 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND true), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND true), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM A_DIR_DESC_B GROUP BY ts, `service.name` ORDER BY ts desc SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND true), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND true), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM A_DIR_DESC_B GROUP BY ts, `service.name` ORDER BY ts desc SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "NULL"},
},
expectedErr: nil,
},
@@ -322,8 +322,8 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND true), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND toFloat64(response_status_code) < ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, avg(multiIf(duration_nano <> ?, duration_nano, NULL)) AS __result_0 FROM A_AND_B GROUP BY `service.name` ORDER BY __result_0 desc SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), float64(400), 0},
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND true), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND toFloat64(response_status_code) < ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, avg(multiIf(duration_nano <> ?, duration_nano, NULL)) AS __result_0 FROM A_AND_B GROUP BY `service.name` ORDER BY __result_0 desc SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), float64(400), "NULL", 0},
},
expectedErr: nil,
},

View File

@@ -27,8 +27,6 @@ type (
// An alias for the Alert type from the alertmanager package.
Alert = types.Alert
AlertSlice = types.AlertSlice
PostableAlert = models.PostableAlert
PostableAlerts = models.PostableAlerts
@@ -40,10 +38,6 @@ type (
GettableAlerts = models.GettableAlerts
)
const (
NoDataLabel = model.LabelName("nodata")
)
type DeprecatedGettableAlert struct {
*model.Alert
Status types.AlertStatus `json:"status"`
@@ -313,11 +307,3 @@ func receiversMatchFilter(receivers []string, filter *regexp.Regexp) bool {
return false
}
func NoDataAlert(alert *types.Alert) bool {
if _, ok := alert.Labels[NoDataLabel]; ok {
return true
} else {
return false
}
}

View File

@@ -21,7 +21,6 @@ import (
const (
DefaultReceiverName string = "default-receiver"
DefaultGroupBy string = "ruleId"
DefaultGroupByAll string = "__all__"
)
var (
@@ -194,20 +193,6 @@ func (c *Config) SetRouteConfig(routeConfig RouteConfig) error {
return nil
}
func (c *Config) AddInhibitRules(rules []config.InhibitRule) error {
if c.alertmanagerConfig == nil {
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "config is nil")
}
c.alertmanagerConfig.InhibitRules = append(c.alertmanagerConfig.InhibitRules, rules...)
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}
func (c *Config) AlertmanagerConfig() *config.Config {
return c.alertmanagerConfig
}
@@ -319,27 +304,6 @@ func (c *Config) CreateRuleIDMatcher(ruleID string, receiverNames []string) erro
return nil
}
func (c *Config) DeleteRuleIDInhibitor(ruleID string) error {
if c.alertmanagerConfig.InhibitRules == nil {
return nil // already nil
}
var filteredRules []config.InhibitRule
for _, inhibitor := range c.alertmanagerConfig.InhibitRules {
sourceContainsRuleID := matcherContainsRuleID(inhibitor.SourceMatchers, ruleID)
targetContainsRuleID := matcherContainsRuleID(inhibitor.TargetMatchers, ruleID)
if !sourceContainsRuleID && !targetContainsRuleID {
filteredRules = append(filteredRules, inhibitor)
}
}
c.alertmanagerConfig.InhibitRules = filteredRules
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}
func (c *Config) UpdateRuleIDMatcher(ruleID string, receiverNames []string) error {
err := c.DeleteRuleIDMatcher(ruleID)
if err != nil {
@@ -441,8 +405,6 @@ func init() {
type NotificationConfig struct {
NotificationGroup map[model.LabelName]struct{}
Renotify ReNotificationConfig
UsePolicy bool
GroupByAll bool
}
func (nc *NotificationConfig) DeepCopy() NotificationConfig {
@@ -453,7 +415,6 @@ func (nc *NotificationConfig) DeepCopy() NotificationConfig {
for k, v := range nc.NotificationGroup {
deepCopy.NotificationGroup[k] = v
}
deepCopy.UsePolicy = nc.UsePolicy
return deepCopy
}
@@ -462,7 +423,7 @@ type ReNotificationConfig struct {
RenotifyInterval time.Duration
}
func NewNotificationConfig(groups []string, renotifyInterval time.Duration, noDataRenotifyInterval time.Duration, policy bool) NotificationConfig {
func NewNotificationConfig(groups []string, renotifyInterval time.Duration, noDataRenotifyInterval time.Duration) NotificationConfig {
notificationConfig := GetDefaultNotificationConfig()
if renotifyInterval != 0 {
@@ -474,13 +435,8 @@ func NewNotificationConfig(groups []string, renotifyInterval time.Duration, noDa
}
for _, group := range groups {
notificationConfig.NotificationGroup[model.LabelName(group)] = struct{}{}
if group == DefaultGroupByAll {
notificationConfig.GroupByAll = true
}
}
notificationConfig.UsePolicy = policy
return notificationConfig
}

View File

@@ -1,139 +0,0 @@
package alertmanagertypes
import (
"context"
"github.com/expr-lang/expr"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type PostableRoutePolicy struct {
Expression string `json:"expression"`
ExpressionKind ExpressionKind `json:"kind"`
Channels []string `json:"channels"`
Name string `json:"name"`
Description string `json:"description"`
Tags []string `json:"tags,omitempty"`
}
func (p *PostableRoutePolicy) Validate() error {
if p.Expression == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "expression is required")
}
if p.Name == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "name is required")
}
if len(p.Channels) == 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "at least one channel is required")
}
// Validate channels are not empty
for i, channel := range p.Channels {
if channel == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "channel at index %d cannot be empty", i)
}
}
if p.ExpressionKind != PolicyBasedExpression && p.ExpressionKind != RuleBasedExpression {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported expression kind: %s", p.ExpressionKind.StringValue())
}
_, err := expr.Compile(p.Expression)
if err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid expression syntax: %v", err)
}
return nil
}
type GettableRoutePolicy struct {
PostableRoutePolicy // Embedded
ID string `json:"id"`
// Audit fields
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CreatedBy *string `json:"createdBy"`
UpdatedBy *string `json:"updatedBy"`
}
type ExpressionKind struct {
valuer.String
}
var (
RuleBasedExpression = ExpressionKind{valuer.NewString("rule")}
PolicyBasedExpression = ExpressionKind{valuer.NewString("policy")}
)
// RoutePolicy represents the database model for expression routes
type RoutePolicy struct {
bun.BaseModel `bun:"table:route_policy"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
Expression string `bun:"expression,type:text,notnull" json:"expression"`
ExpressionKind ExpressionKind `bun:"kind,type:text" json:"kind"`
Channels []string `bun:"channels,type:jsonb" json:"channels"`
Name string `bun:"name,type:text" json:"name"`
Description string `bun:"description,type:text" json:"description"`
Enabled bool `bun:"enabled,type:boolean,default:true" json:"enabled"`
Tags []string `bun:"tags,type:jsonb" json:"tags,omitempty"`
OrgID string `bun:"org_id,type:text,notnull" json:"orgId"`
}
func (er *RoutePolicy) Validate() error {
if er == nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "route_policy cannot be nil")
}
if er.Expression == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "expression is required")
}
if er.Name == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "name is required")
}
if er.OrgID == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "organization ID is required")
}
if len(er.Channels) == 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "at least one channel is required")
}
// Validate channels are not empty
for i, channel := range er.Channels {
if channel == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "channel at index %d cannot be empty", i)
}
}
if er.ExpressionKind != PolicyBasedExpression && er.ExpressionKind != RuleBasedExpression {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported expression kind: %s", er.ExpressionKind.StringValue())
}
return nil
}
type RouteStore interface {
GetByID(ctx context.Context, orgId string, id string) (*RoutePolicy, error)
Create(ctx context.Context, route *RoutePolicy) error
CreateBatch(ctx context.Context, routes []*RoutePolicy) error
Delete(ctx context.Context, orgId string, id string) error
GetAllByKind(ctx context.Context, orgID string, kind ExpressionKind) ([]*RoutePolicy, error)
GetAllByName(ctx context.Context, orgID string, name string) ([]*RoutePolicy, error)
DeleteRouteByName(ctx context.Context, orgID string, name string) error
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/prometheus/common/model"
"log/slog"
"time"
@@ -50,9 +49,9 @@ func NewReceiver(input string) (Receiver, error) {
return receiverWithDefaults, nil
}
func TestReceiver(ctx context.Context, receiver Receiver, receiverIntegrationsFunc ReceiverIntegrationsFunc, config *Config, tmpl *template.Template, logger *slog.Logger, lSet model.LabelSet, alert ...*Alert) error {
ctx = notify.WithGroupKey(ctx, fmt.Sprintf("%s-%s-%d", receiver.Name, lSet.Fingerprint(), time.Now().Unix()))
ctx = notify.WithGroupLabels(ctx, lSet)
func TestReceiver(ctx context.Context, receiver Receiver, receiverIntegrationsFunc ReceiverIntegrationsFunc, config *Config, tmpl *template.Template, logger *slog.Logger, alert *Alert) error {
ctx = notify.WithGroupKey(ctx, fmt.Sprintf("%s-%s-%d", receiver.Name, alert.Labels.Fingerprint(), time.Now().Unix()))
ctx = notify.WithGroupLabels(ctx, alert.Labels)
ctx = notify.WithReceiverName(ctx, receiver.Name)
// We need to create a new config with the same global and route config but empty receivers and routes
@@ -81,7 +80,7 @@ func TestReceiver(ctx context.Context, receiver Receiver, receiverIntegrationsFu
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "no integrations found for receiver %s", receiver.Name)
}
if _, err = integrations[0].Notify(ctx, alert...); err != nil {
if _, err = integrations[0].Notify(ctx, alert); err != nil {
return err
}

View File

@@ -15,8 +15,6 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/alertmanager/config"
)
type AlertType string
@@ -67,95 +65,21 @@ type PostableRule struct {
}
type NotificationSettings struct {
GroupBy []string `json:"groupBy,omitempty"`
Renotify Renotify `json:"renotify,omitempty"`
UsePolicy bool `json:"usePolicy,omitempty"`
}
type Renotify struct {
Enabled bool `json:"enabled"`
ReNotifyInterval Duration `json:"interval,omitempty"`
AlertStates []model.AlertState `json:"alertStates,omitempty"`
NotificationGroupBy []string `json:"notificationGroupBy,omitempty"`
ReNotifyInterval Duration `json:"renotify,omitempty"`
AlertStates []model.AlertState `json:"alertStates,omitempty"`
}
func (ns *NotificationSettings) GetAlertManagerNotificationConfig() alertmanagertypes.NotificationConfig {
var renotifyInterval time.Duration
var noDataRenotifyInterval time.Duration
if ns.Renotify.Enabled {
if slices.Contains(ns.Renotify.AlertStates, model.StateNoData) {
noDataRenotifyInterval = time.Duration(ns.Renotify.ReNotifyInterval)
}
if slices.Contains(ns.Renotify.AlertStates, model.StateFiring) {
renotifyInterval = time.Duration(ns.Renotify.ReNotifyInterval)
}
} else {
renotifyInterval = 8760 * time.Hour //1 year for no renotify substitute
noDataRenotifyInterval = 8760 * time.Hour
var renotifyInterval Duration
var noDataRenotifyInterval Duration
if slices.Contains(ns.AlertStates, model.StateNoData) {
noDataRenotifyInterval = ns.ReNotifyInterval
}
return alertmanagertypes.NewNotificationConfig(ns.GroupBy, renotifyInterval, noDataRenotifyInterval, ns.UsePolicy)
}
func (r *PostableRule) GetRuleRouteRequest(ruleId string) ([]*alertmanagertypes.PostableRoutePolicy, error) {
threshold, err := r.RuleCondition.Thresholds.GetRuleThreshold()
if err != nil {
return nil, err
if slices.Contains(ns.AlertStates, model.StateFiring) {
renotifyInterval = ns.ReNotifyInterval
}
receivers := threshold.GetRuleReceivers()
routeRequests := make([]*alertmanagertypes.PostableRoutePolicy, 0)
for _, receiver := range receivers {
expression := fmt.Sprintf(`%s == "%s" && %s == "%s"`, LabelThresholdName, receiver.Name, LabelRuleId, ruleId)
routeRequests = append(routeRequests, &alertmanagertypes.PostableRoutePolicy{
Expression: expression,
ExpressionKind: alertmanagertypes.RuleBasedExpression,
Channels: receiver.Channels,
Name: ruleId,
Description: fmt.Sprintf("Auto-generated route for rule %s", ruleId),
Tags: []string{"auto-generated", "rule-based"},
})
}
return routeRequests, nil
}
func (r *PostableRule) GetInhibitRules(ruleId string) ([]config.InhibitRule, error) {
threshold, err := r.RuleCondition.Thresholds.GetRuleThreshold()
if err != nil {
return nil, err
}
var groups []string
if r.NotificationSettings != nil {
for k := range r.NotificationSettings.GetAlertManagerNotificationConfig().NotificationGroup {
groups = append(groups, string(k))
}
}
receivers := threshold.GetRuleReceivers()
var inhibitRules []config.InhibitRule
for i := 0; i < len(receivers)-1; i++ {
rule := config.InhibitRule{
SourceMatchers: config.Matchers{
{
Name: LabelThresholdName,
Value: receivers[i].Name,
},
{
Name: LabelRuleId,
Value: ruleId,
},
},
TargetMatchers: config.Matchers{
{
Name: LabelThresholdName,
Value: receivers[i+1].Name,
},
{
Name: LabelRuleId,
Value: ruleId,
},
},
Equal: groups,
}
inhibitRules = append(inhibitRules, rule)
}
return inhibitRules, nil
return alertmanagertypes.NewNotificationConfig(ns.NotificationGroupBy, time.Duration(renotifyInterval), time.Duration(noDataRenotifyInterval))
}
func (ns *NotificationSettings) UnmarshalJSON(data []byte) error {
@@ -171,7 +95,7 @@ func (ns *NotificationSettings) UnmarshalJSON(data []byte) error {
}
// Validate states after unmarshaling
for _, state := range ns.Renotify.AlertStates {
for _, state := range ns.AlertStates {
if state != model.StateFiring && state != model.StateNoData {
return fmt.Errorf("invalid alert state: %s", state)
}
@@ -219,25 +143,15 @@ func (r *PostableRule) processRuleDefaults() error {
Kind: BasicThresholdKind,
Spec: BasicRuleThresholds{{
Name: thresholdName,
RuleUnit: r.RuleCondition.CompositeQuery.Unit,
TargetUnit: r.RuleCondition.TargetUnit,
TargetValue: r.RuleCondition.Target,
MatchType: r.RuleCondition.MatchType,
CompareOp: r.RuleCondition.CompareOp,
Channels: r.PreferredChannels,
}},
}
r.RuleCondition.Thresholds = &thresholdData
r.Evaluation = &EvaluationEnvelope{RollingEvaluation, RollingWindow{EvalWindow: r.EvalWindow, Frequency: r.Frequency}}
r.NotificationSettings = &NotificationSettings{
Renotify: Renotify{
Enabled: true,
ReNotifyInterval: Duration(4 * time.Hour),
AlertStates: []model.AlertState{model.StateFiring},
},
}
if r.RuleCondition.AlertOnAbsent {
r.NotificationSettings.Renotify.AlertStates = append(r.NotificationSettings.Renotify.AlertStates, model.StateNoData)
}
}
}
@@ -256,7 +170,6 @@ func (r *PostableRule) MarshalJSON() ([]byte, error) {
}
aux.Evaluation = nil
aux.SchemaVersion = ""
aux.NotificationSettings = nil
return json.Marshal(aux)
default:
copyStruct := *r
@@ -279,7 +192,7 @@ func isValidLabelName(ln string) bool {
return false
}
for i, b := range ln {
if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == '.' || (b >= '0' && b <= '9' && i > 0)) {
if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) {
return false
}
}
@@ -434,7 +347,6 @@ func (g *GettableRule) MarshalJSON() ([]byte, error) {
}
aux.Evaluation = nil
aux.SchemaVersion = ""
aux.NotificationSettings = nil
return json.Marshal(aux)
default:
copyStruct := *g

View File

@@ -2,11 +2,10 @@ package ruletypes
import (
"encoding/json"
"github.com/stretchr/testify/assert"
"testing"
"time"
"github.com/stretchr/testify/assert"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
)
@@ -304,6 +303,10 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
t.Errorf("Expected threshold name 'warning' from severity label, got '%s'", spec.Name)
}
// Verify all fields are copied from RuleCondition
if spec.RuleUnit != "percent" {
t.Errorf("Expected RuleUnit 'percent', got '%s'", spec.RuleUnit)
}
if spec.TargetUnit != "%" {
t.Errorf("Expected TargetUnit '%%', got '%s'", spec.TargetUnit)
}
@@ -452,6 +455,9 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
if spec.TargetUnit != "%" {
t.Errorf("Expected TargetUnit '%%' (overwritten), got '%s'", spec.TargetUnit)
}
if spec.RuleUnit != "percent" {
t.Errorf("Expected RuleUnit 'percent' (overwritten), got '%s'", spec.RuleUnit)
}
if rule.Evaluation == nil {
t.Fatal("Expected Evaluation to be populated")
@@ -624,9 +630,9 @@ func TestParseIntoRuleThresholdGeneration(t *testing.T) {
vector, err := threshold.ShouldAlert(v3.Series{
Points: []v3.Point{{Value: 0.15, Timestamp: 1000}}, // 150ms in seconds
Labels: map[string]string{"test": "label"},
}, "")
})
if err != nil {
t.Fatalf("Unexpected error in shouldAlert: %v", err)
t.Fatalf("Unexpected error in ShouldAlert: %v", err)
}
if len(vector) == 0 {
@@ -701,9 +707,9 @@ func TestParseIntoRuleMultipleThresholds(t *testing.T) {
vector, err := threshold.ShouldAlert(v3.Series{
Points: []v3.Point{{Value: 95.0, Timestamp: 1000}}, // 95% CPU usage
Labels: map[string]string{"service": "test"},
}, "")
})
if err != nil {
t.Fatalf("Unexpected error in shouldAlert: %v", err)
t.Fatalf("Unexpected error in ShouldAlert: %v", err)
}
assert.Equal(t, 2, len(vector))
@@ -711,9 +717,9 @@ func TestParseIntoRuleMultipleThresholds(t *testing.T) {
vector, err = threshold.ShouldAlert(v3.Series{
Points: []v3.Point{{Value: 75.0, Timestamp: 1000}}, // 75% CPU usage
Labels: map[string]string{"service": "test"},
}, "")
})
if err != nil {
t.Fatalf("Unexpected error in shouldAlert: %v", err)
t.Fatalf("Unexpected error in ShouldAlert: %v", err)
}
assert.Equal(t, 1, len(vector))

View File

@@ -2,4 +2,3 @@ package ruletypes
const CriticalThresholdName = "CRITICAL"
const LabelThresholdName = "threshold.name"
const LabelRuleId = "ruleId"

View File

@@ -18,10 +18,6 @@ type Sample struct {
Metric labels.Labels
IsMissing bool
Target float64
TargetUnit string
}
func (s Sample) String() string {

View File

@@ -51,41 +51,23 @@ func (r *RuleThresholdData) UnmarshalJSON(data []byte) error {
return nil
}
type RuleReceivers struct {
Channels []string `json:"channels"`
Name string `json:"name"`
}
type RuleThreshold interface {
ShouldAlert(series v3.Series, unit string) (Vector, error)
GetRuleReceivers() []RuleReceivers
ShouldAlert(series v3.Series) (Vector, error)
}
type BasicRuleThreshold struct {
Name string `json:"name"`
TargetValue *float64 `json:"target"`
TargetUnit string `json:"targetUnit"`
RuleUnit string `json:"ruleUnit"`
RecoveryTarget *float64 `json:"recoveryTarget"`
MatchType MatchType `json:"matchType"`
CompareOp CompareOp `json:"op"`
Channels []string `json:"channels"`
SelectedQuery string `json:"selectedQuery"`
}
type BasicRuleThresholds []BasicRuleThreshold
func (r BasicRuleThresholds) GetRuleReceivers() []RuleReceivers {
thresholds := []BasicRuleThreshold(r)
var receiverRoutes []RuleReceivers
sortThresholds(thresholds)
for _, threshold := range thresholds {
receiverRoutes = append(receiverRoutes, RuleReceivers{
Name: threshold.Name,
Channels: threshold.Channels,
})
}
return receiverRoutes
}
func (r BasicRuleThresholds) Validate() error {
var errs []error
for _, basicThreshold := range r {
@@ -96,27 +78,13 @@ func (r BasicRuleThresholds) Validate() error {
return errors.Join(errs...)
}
func (r BasicRuleThresholds) ShouldAlert(series v3.Series, unit string) (Vector, error) {
func (r BasicRuleThresholds) ShouldAlert(series v3.Series) (Vector, error) {
var resultVector Vector
thresholds := []BasicRuleThreshold(r)
sortThresholds(thresholds)
for _, threshold := range thresholds {
smpl, shouldAlert := threshold.shouldAlert(series, unit)
if shouldAlert {
smpl.Target = threshold.target(unit)
smpl.TargetUnit = threshold.TargetUnit
resultVector = append(resultVector, smpl)
}
}
return resultVector, nil
}
func sortThresholds(thresholds []BasicRuleThreshold) {
sort.Slice(thresholds, func(i, j int) bool {
compareOp := thresholds[i].getCompareOp()
targetI := thresholds[i].target(thresholds[i].TargetUnit) //for sorting we dont need rule unit
targetJ := thresholds[j].target(thresholds[j].TargetUnit)
compareOp := thresholds[i].GetCompareOp()
targetI := thresholds[i].Target()
targetJ := thresholds[j].Target()
switch compareOp {
case ValueIsAbove, ValueAboveOrEq, ValueOutsideBounds:
@@ -130,22 +98,49 @@ func sortThresholds(thresholds []BasicRuleThreshold) {
return targetI > targetJ
}
})
for _, threshold := range thresholds {
smpl, shouldAlert := threshold.ShouldAlert(series)
if shouldAlert {
resultVector = append(resultVector, smpl)
}
}
return resultVector, nil
}
func (b BasicRuleThreshold) target(ruleUnit string) float64 {
func (b BasicRuleThreshold) GetName() string {
return b.Name
}
func (b BasicRuleThreshold) Target() float64 {
unitConverter := converter.FromUnit(converter.Unit(b.TargetUnit))
// convert the target value to the y-axis unit
value := unitConverter.Convert(converter.Value{
F: *b.TargetValue,
U: converter.Unit(b.TargetUnit),
}, converter.Unit(ruleUnit))
}, converter.Unit(b.RuleUnit))
return value.F
}
func (b BasicRuleThreshold) getCompareOp() CompareOp {
func (b BasicRuleThreshold) GetRecoveryTarget() float64 {
if b.RecoveryTarget == nil {
return 0
} else {
return *b.RecoveryTarget
}
}
func (b BasicRuleThreshold) GetMatchType() MatchType {
return b.MatchType
}
func (b BasicRuleThreshold) GetCompareOp() CompareOp {
return b.CompareOp
}
func (b BasicRuleThreshold) GetSelectedQuery() string {
return b.SelectedQuery
}
func (b BasicRuleThreshold) Validate() error {
var errs []error
if b.Name == "" {
@@ -187,7 +182,7 @@ func removeGroupinSetPoints(series v3.Series) []v3.Point {
return result
}
func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Sample, bool) {
func (b BasicRuleThreshold) ShouldAlert(series v3.Series) (Sample, bool) {
var shouldAlert bool
var alertSmpl Sample
var lbls labels.Labels
@@ -196,8 +191,6 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
lbls = append(lbls, labels.Label{Name: name, Value: value})
}
target := b.target(ruleUnit)
lbls = append(lbls, labels.Label{Name: LabelThresholdName, Value: b.Name})
series.Points = removeGroupinSetPoints(series)
@@ -212,7 +205,7 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
// If any sample matches the condition, the rule is firing.
if b.CompareOp == ValueIsAbove {
for _, smpl := range series.Points {
if smpl.Value > target {
if smpl.Value > b.Target() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
@@ -220,7 +213,7 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
}
} else if b.CompareOp == ValueIsBelow {
for _, smpl := range series.Points {
if smpl.Value < target {
if smpl.Value < b.Target() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
@@ -228,7 +221,7 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
}
} else if b.CompareOp == ValueIsEq {
for _, smpl := range series.Points {
if smpl.Value == target {
if smpl.Value == b.Target() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
@@ -236,7 +229,7 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
}
} else if b.CompareOp == ValueIsNotEq {
for _, smpl := range series.Points {
if smpl.Value != target {
if smpl.Value != b.Target() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
@@ -244,7 +237,7 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
}
} else if b.CompareOp == ValueOutsideBounds {
for _, smpl := range series.Points {
if math.Abs(smpl.Value) >= target {
if math.Abs(smpl.Value) >= b.Target() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
@@ -254,10 +247,10 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
case AllTheTimes:
// If all samples match the condition, the rule is firing.
shouldAlert = true
alertSmpl = Sample{Point: Point{V: target}, Metric: lbls}
alertSmpl = Sample{Point: Point{V: b.Target()}, Metric: lbls}
if b.CompareOp == ValueIsAbove {
for _, smpl := range series.Points {
if smpl.Value <= target {
if smpl.Value <= b.Target() {
shouldAlert = false
break
}
@@ -274,7 +267,7 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
}
} else if b.CompareOp == ValueIsBelow {
for _, smpl := range series.Points {
if smpl.Value >= target {
if smpl.Value >= b.Target() {
shouldAlert = false
break
}
@@ -290,14 +283,14 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
}
} else if b.CompareOp == ValueIsEq {
for _, smpl := range series.Points {
if smpl.Value != target {
if smpl.Value != b.Target() {
shouldAlert = false
break
}
}
} else if b.CompareOp == ValueIsNotEq {
for _, smpl := range series.Points {
if smpl.Value == target {
if smpl.Value == b.Target() {
shouldAlert = false
break
}
@@ -313,7 +306,7 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
}
} else if b.CompareOp == ValueOutsideBounds {
for _, smpl := range series.Points {
if math.Abs(smpl.Value) < target {
if math.Abs(smpl.Value) < b.Target() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
shouldAlert = false
break
@@ -333,23 +326,23 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
avg := sum / count
alertSmpl = Sample{Point: Point{V: avg}, Metric: lbls}
if b.CompareOp == ValueIsAbove {
if avg > target {
if avg > b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueIsBelow {
if avg < target {
if avg < b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueIsEq {
if avg == target {
if avg == b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueIsNotEq {
if avg != target {
if avg != b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueOutsideBounds {
if math.Abs(avg) >= target {
if math.Abs(avg) >= b.Target() {
shouldAlert = true
}
}
@@ -365,23 +358,23 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
}
alertSmpl = Sample{Point: Point{V: sum}, Metric: lbls}
if b.CompareOp == ValueIsAbove {
if sum > target {
if sum > b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueIsBelow {
if sum < target {
if sum < b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueIsEq {
if sum == target {
if sum == b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueIsNotEq {
if sum != target {
if sum != b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueOutsideBounds {
if math.Abs(sum) >= target {
if math.Abs(sum) >= b.Target() {
shouldAlert = true
}
}
@@ -390,19 +383,19 @@ func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Samp
shouldAlert = false
alertSmpl = Sample{Point: Point{V: series.Points[len(series.Points)-1].Value}, Metric: lbls}
if b.CompareOp == ValueIsAbove {
if series.Points[len(series.Points)-1].Value > target {
if series.Points[len(series.Points)-1].Value > b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueIsBelow {
if series.Points[len(series.Points)-1].Value < target {
if series.Points[len(series.Points)-1].Value < b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueIsEq {
if series.Points[len(series.Points)-1].Value == target {
if series.Points[len(series.Points)-1].Value == b.Target() {
shouldAlert = true
}
} else if b.CompareOp == ValueIsNotEq {
if series.Points[len(series.Points)-1].Value != target {
if series.Points[len(series.Points)-1].Value != b.Target() {
shouldAlert = true
}
}

View File

@@ -141,7 +141,7 @@ func detectPlatform() string {
}
// Azure metadata
if req, err := http.NewRequest(http.MethodGet, "http://169.254.169.254/metadata/instance?api-version=2017-03-01", nil); err == nil {
if req, err := http.NewRequest(http.MethodGet, "http://169.254.169.254/metadata/instance", nil); err == nil {
req.Header.Add("Metadata", "true")
if resp, err := client.Do(req); err == nil {
resp.Body.Close()