mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-09 03:02:20 +00:00
Compare commits
1 Commits
playwright
...
chore/norm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e3449db63 |
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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] = ""
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 />);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -216,7 +216,7 @@ describe('CreateAlertV2 utils', () => {
|
||||
multipleNotifications: ['email'],
|
||||
reNotification: {
|
||||
enabled: false,
|
||||
value: 30,
|
||||
value: 1,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
conditions: [],
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ const defaultNotificationSettings: PostableAlertRuleV2['notificationSettings'] =
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '30m',
|
||||
interval: '1m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: false,
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -88,7 +88,6 @@ function RoutingPolicies(): JSX.Element {
|
||||
isRoutingPoliciesError={isErrorRoutingPolicies}
|
||||
handlePolicyDetailsModalOpen={handlePolicyDetailsModalOpen}
|
||||
handleDeleteModalOpen={handleDeleteModalOpen}
|
||||
hasSearchTerm={(searchTerm?.length ?? 0) > 0}
|
||||
/>
|
||||
{policyDetailsModalState.isOpen && (
|
||||
<RoutingPolicyDetails
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,6 @@ export interface RoutingPolicyListProps {
|
||||
isRoutingPoliciesError: boolean;
|
||||
handlePolicyDetailsModalOpen: HandlePolicyDetailsModalOpen;
|
||||
handleDeleteModalOpen: HandleDeleteModalOpen;
|
||||
hasSearchTerm: boolean;
|
||||
}
|
||||
|
||||
export interface RoutingPolicyListItemProps {
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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'] = [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
11
frontend_automation/.gitignore
vendored
11
frontend_automation/.gitignore
vendored
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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
4
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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...)
|
||||
}
|
||||
|
||||
|
||||
@@ -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", ¬ifConfig)
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] = ""
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -2,4 +2,3 @@ package ruletypes
|
||||
|
||||
const CriticalThresholdName = "CRITICAL"
|
||||
const LabelThresholdName = "threshold.name"
|
||||
const LabelRuleId = "ruleId"
|
||||
|
||||
@@ -18,10 +18,6 @@ type Sample struct {
|
||||
Metric labels.Labels
|
||||
|
||||
IsMissing bool
|
||||
|
||||
Target float64
|
||||
|
||||
TargetUnit string
|
||||
}
|
||||
|
||||
func (s Sample) String() string {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user