Compare commits

..

1 Commits

Author SHA1 Message Date
nityanandagohain
445d3e8c3e fix: update validation for select and group by 2025-10-10 14:41:10 +05:30
19 changed files with 310 additions and 1800 deletions

View File

@@ -17,7 +17,6 @@ jobs:
- bootstrap
- auth
- querier
- ttl
sqlstore-provider:
- postgres
- sqlite

View File

@@ -25,8 +25,8 @@ function QuerySection(): JSX.Element {
const alertDef = buildAlertDefForChartPreview({ alertType, thresholdState });
const onQueryCategoryChange = (queryType: EQueryType): void => {
const query: Query = { ...currentQuery, queryType };
const onQueryCategoryChange = (val: EQueryType): void => {
const query: Query = { ...currentQuery, queryType: val };
redirectWithQueryBuilderData(query);
};

View File

@@ -2,28 +2,16 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryParams } from 'constants/query';
import {
initialClickHouseData,
initialQueryPromQLData,
} from 'constants/queryBuilder';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { CreateAlertProvider } from '../../context';
import QuerySection from '../QuerySection';
jest.mock('uuid', () => ({
v4: (): string => 'test-uuid-12345',
}));
const MOCK_UUID = 'test-uuid-12345';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
@@ -60,27 +48,12 @@ jest.mock(
queryCategory,
alertType,
panelType,
setQueryCategory,
}: any): JSX.Element {
return (
<div data-testid="query-section-component">
<div data-testid="query-category">{queryCategory}</div>
<div data-testid="alert-type">{alertType}</div>
<div data-testid="panel-type">{panelType}</div>
<button
type="button"
data-testid="change-to-promql"
onClick={(): void => setQueryCategory(EQueryType.PROM)}
>
Change to PromQL
</button>
<button
type="button"
data-testid="change-to-query-builder"
onClick={(): void => setQueryCategory(EQueryType.QUERY_BUILDER)}
>
Change to Query Builder
</button>
</div>
);
},
@@ -267,6 +240,17 @@ describe('QuerySection', () => {
expect(screen.getByTestId('panel-type')).toHaveTextContent('graph');
});
it('has correct CSS classes for tab styling', () => {
renderQuerySection();
const tabs = screen.getAllByRole('button');
tabs.forEach((tab) => {
expect(tab).toHaveClass('list-view-tab');
expect(tab).toHaveClass('explorer-view-option');
});
});
it('renders with correct container structure', () => {
renderQuerySection();
@@ -323,172 +307,4 @@ describe('QuerySection', () => {
expect(metricsButton).toHaveClass(ACTIVE_TAB_CLASS);
expect(logsButton).not.toHaveClass(ACTIVE_TAB_CLASS);
});
it('updates the query data when the alert type changes', async () => {
const user = userEvent.setup();
renderQuerySection();
const metricsTab = screen.getByText(METRICS_TEXT);
await user.click(metricsTab);
const result = mockUseQueryBuilder.redirectWithQueryBuilderData.mock.calls[0];
expect(result[0]).toEqual({
id: MOCK_UUID,
queryType: EQueryType.QUERY_BUILDER,
unit: undefined,
builder: {
queryData: [
expect.objectContaining({
dataSource: DataSource.METRICS,
queryName: 'A',
}),
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [initialQueryPromQLData],
clickhouse_sql: [initialClickHouseData],
});
expect(result[1]).toEqual({
[QueryParams.alertType]: AlertTypes.METRICS_BASED_ALERT,
[QueryParams.ruleType]: AlertDetectionTypes.THRESHOLD_ALERT,
});
});
it('updates the query data when the query type changes from query_builder to promql', async () => {
const user = userEvent.setup();
renderQuerySection();
const changeToPromQLButton = screen.getByTestId('change-to-promql');
await user.click(changeToPromQLButton);
expect(
mockUseQueryBuilder.redirectWithQueryBuilderData,
).toHaveBeenCalledTimes(1);
const [
queryArg,
] = mockUseQueryBuilder.redirectWithQueryBuilderData.mock.calls[0];
expect(queryArg).toEqual({
...mockUseQueryBuilder.currentQuery,
queryType: EQueryType.PROM,
});
expect(mockUseQueryBuilder.redirectWithQueryBuilderData).toHaveBeenCalledWith(
queryArg,
);
});
it('updates the query data when switching from promql to query_builder for logs', async () => {
const user = userEvent.setup();
const mockCurrentQueryWithPromQL = {
...mockUseQueryBuilder.currentQuery,
queryType: EQueryType.PROM,
builder: {
queryData: [
{
dataSource: DataSource.LOGS,
},
],
},
};
useQueryBuilder.mockReturnValue({
...mockUseQueryBuilder,
currentQuery: mockCurrentQueryWithPromQL,
});
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<CreateAlertProvider initialAlertType={AlertTypes.LOGS_BASED_ALERT}>
<QuerySection />
</CreateAlertProvider>
</MemoryRouter>
</QueryClientProvider>
</Provider>,
);
const changeToQueryBuilderButton = screen.getByTestId(
'change-to-query-builder',
);
await user.click(changeToQueryBuilderButton);
expect(
mockUseQueryBuilder.redirectWithQueryBuilderData,
).toHaveBeenCalledTimes(1);
const [
queryArg,
] = mockUseQueryBuilder.redirectWithQueryBuilderData.mock.calls[0];
expect(queryArg).toEqual({
...mockCurrentQueryWithPromQL,
queryType: EQueryType.QUERY_BUILDER,
});
expect(mockUseQueryBuilder.redirectWithQueryBuilderData).toHaveBeenCalledWith(
queryArg,
);
});
it('updates the query data when switching from clickhouse_sql to query_builder for traces', async () => {
const user = userEvent.setup();
const mockCurrentQueryWithClickhouseSQL = {
...mockUseQueryBuilder.currentQuery,
queryType: EQueryType.CLICKHOUSE,
builder: {
queryData: [
{
dataSource: DataSource.TRACES,
},
],
},
};
useQueryBuilder.mockReturnValue({
...mockUseQueryBuilder,
currentQuery: mockCurrentQueryWithClickhouseSQL,
});
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<CreateAlertProvider initialAlertType={AlertTypes.TRACES_BASED_ALERT}>
<QuerySection />
</CreateAlertProvider>
</MemoryRouter>
</QueryClientProvider>
</Provider>,
);
const changeToQueryBuilderButton = screen.getByTestId(
'change-to-query-builder',
);
await user.click(changeToQueryBuilderButton);
expect(
mockUseQueryBuilder.redirectWithQueryBuilderData,
).toHaveBeenCalledTimes(1);
const [
queryArg,
] = mockUseQueryBuilder.redirectWithQueryBuilderData.mock.calls[0];
expect(queryArg).toEqual({
...mockCurrentQueryWithClickhouseSQL,
queryType: EQueryType.QUERY_BUILDER,
});
expect(mockUseQueryBuilder.redirectWithQueryBuilderData).toHaveBeenCalledWith(
queryArg,
);
});
});

View File

@@ -1,678 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { initialQueriesMap } from 'constants/queryBuilder';
import {
alertDefaults,
anamolyAlertDefaults,
exceptionAlertDefaults,
logAlertDefaults,
traceAlertDefaults,
} from 'container/CreateAlertRule/defaults';
import dayjs from 'dayjs';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
INITIAL_ADVANCED_OPTIONS_STATE,
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from '../constants';
import {
AdvancedOptionsState,
AlertState,
AlertThresholdMatchType,
AlertThresholdOperator,
AlertThresholdState,
Algorithm,
EvaluationWindowState,
NotificationSettingsState,
Seasonality,
TimeDuration,
} from '../types';
import {
advancedOptionsReducer,
alertCreationReducer,
alertThresholdReducer,
buildInitialAlertDef,
evaluationWindowReducer,
getInitialAlertType,
getInitialAlertTypeFromURL,
notificationSettingsReducer,
} from '../utils';
const UNKNOWN_ACTION_TYPE = 'UNKNOWN_ACTION_TYPE';
const TEST_RESET_TO_INITIAL_STATE = 'should reset to initial state';
const TEST_SET_INITIAL_STATE_FROM_PAYLOAD =
'should set initial state from payload';
const TEST_RETURN_STATE_FOR_UNKNOWN_ACTION =
'should return current state for unknown action';
describe('CreateAlertV2 Context Utils', () => {
describe('alertCreationReducer', () => {
it('should set alert name', () => {
const result = alertCreationReducer(INITIAL_ALERT_STATE, {
type: 'SET_ALERT_NAME',
payload: 'Test Alert',
});
expect(result).toEqual({
...INITIAL_ALERT_STATE,
name: 'Test Alert',
});
});
it('should set alert labels', () => {
const labels = { severity: 'critical', team: 'backend' };
const result = alertCreationReducer(INITIAL_ALERT_STATE, {
type: 'SET_ALERT_LABELS',
payload: labels,
});
expect(result).toEqual({
...INITIAL_ALERT_STATE,
labels,
});
});
it('should set y-axis unit', () => {
const result = alertCreationReducer(INITIAL_ALERT_STATE, {
type: 'SET_Y_AXIS_UNIT',
payload: 'ms',
});
expect(result).toEqual({
...INITIAL_ALERT_STATE,
yAxisUnit: 'ms',
});
});
it(TEST_RESET_TO_INITIAL_STATE, () => {
const modifiedState: AlertState = {
name: 'Modified',
labels: { test: 'value' },
yAxisUnit: 'ms',
};
const result = alertCreationReducer(modifiedState, { type: 'RESET' });
expect(result).toEqual(INITIAL_ALERT_STATE);
});
it(TEST_SET_INITIAL_STATE_FROM_PAYLOAD, () => {
const newState: AlertState = {
name: 'Custom Alert',
labels: { env: 'production' },
yAxisUnit: 'bytes',
};
const result = alertCreationReducer(INITIAL_ALERT_STATE, {
type: 'SET_INITIAL_STATE',
payload: newState,
});
expect(result).toEqual(newState);
});
it(TEST_RETURN_STATE_FOR_UNKNOWN_ACTION, () => {
const result = alertCreationReducer(
INITIAL_ALERT_STATE,
{ type: UNKNOWN_ACTION_TYPE } as any,
);
expect(result).toEqual(INITIAL_ALERT_STATE);
});
});
describe('getInitialAlertType', () => {
it('should return METRICS_BASED_ALERT for metrics data source', () => {
const result = getInitialAlertType(initialQueriesMap.metrics);
expect(result).toBe(AlertTypes.METRICS_BASED_ALERT);
});
it('should return LOGS_BASED_ALERT for logs data source', () => {
const result = getInitialAlertType(initialQueriesMap.logs);
expect(result).toBe(AlertTypes.LOGS_BASED_ALERT);
});
it('should return TRACES_BASED_ALERT for traces data source', () => {
const result = getInitialAlertType(initialQueriesMap.traces);
expect(result).toBe(AlertTypes.TRACES_BASED_ALERT);
});
it('should return METRICS_BASED_ALERT for unknown data source', () => {
const queryWithUnknownDataSource = {
...initialQueriesMap.metrics,
builder: {
...initialQueriesMap.metrics.builder,
queryData: [],
},
};
const result = getInitialAlertType(queryWithUnknownDataSource);
expect(result).toBe(AlertTypes.METRICS_BASED_ALERT);
});
});
describe('buildInitialAlertDef', () => {
it('should return logAlertDefaults for LOGS_BASED_ALERT', () => {
const result = buildInitialAlertDef(AlertTypes.LOGS_BASED_ALERT);
expect(result).toBe(logAlertDefaults);
});
it('should return traceAlertDefaults for TRACES_BASED_ALERT', () => {
const result = buildInitialAlertDef(AlertTypes.TRACES_BASED_ALERT);
expect(result).toBe(traceAlertDefaults);
});
it('should return exceptionAlertDefaults for EXCEPTIONS_BASED_ALERT', () => {
const result = buildInitialAlertDef(AlertTypes.EXCEPTIONS_BASED_ALERT);
expect(result).toBe(exceptionAlertDefaults);
});
it('should return anamolyAlertDefaults for ANOMALY_BASED_ALERT', () => {
const result = buildInitialAlertDef(AlertTypes.ANOMALY_BASED_ALERT);
expect(result).toBe(anamolyAlertDefaults);
});
it('should return alertDefaults for METRICS_BASED_ALERT', () => {
const result = buildInitialAlertDef(AlertTypes.METRICS_BASED_ALERT);
expect(result).toBe(alertDefaults);
});
it('should return alertDefaults for unknown alert type', () => {
const result = buildInitialAlertDef('UNKNOWN' as AlertTypes);
expect(result).toBe(alertDefaults);
});
});
describe('getInitialAlertTypeFromURL', () => {
it('should return ANOMALY_BASED_ALERT when ruleType is anomaly_rule', () => {
const params = new URLSearchParams('?ruleType=anomaly_rule');
const result = getInitialAlertTypeFromURL(params, initialQueriesMap.metrics);
expect(result).toBe(AlertTypes.ANOMALY_BASED_ALERT);
});
it('should return alert type from alertType param', () => {
const params = new URLSearchParams('?alertType=LOGS_BASED_ALERT');
const result = getInitialAlertTypeFromURL(params, initialQueriesMap.metrics);
expect(result).toBe(AlertTypes.LOGS_BASED_ALERT);
});
it('should prioritize ruleType over alertType', () => {
const params = new URLSearchParams(
'?ruleType=anomaly_rule&alertType=LOGS_BASED_ALERT',
);
const result = getInitialAlertTypeFromURL(params, initialQueriesMap.metrics);
expect(result).toBe(AlertTypes.ANOMALY_BASED_ALERT);
});
it('should fall back to query data source when no URL params', () => {
const params = new URLSearchParams('');
const result = getInitialAlertTypeFromURL(params, initialQueriesMap.traces);
expect(result).toBe(AlertTypes.TRACES_BASED_ALERT);
});
});
describe('alertThresholdReducer', () => {
it('should set selected query', () => {
const result = alertThresholdReducer(INITIAL_ALERT_THRESHOLD_STATE, {
type: 'SET_SELECTED_QUERY',
payload: 'B',
});
expect(result).toEqual({
...INITIAL_ALERT_THRESHOLD_STATE,
selectedQuery: 'B',
});
});
it('should set operator', () => {
const result = alertThresholdReducer(INITIAL_ALERT_THRESHOLD_STATE, {
type: 'SET_OPERATOR',
payload: AlertThresholdOperator.IS_BELOW,
});
expect(result).toEqual({
...INITIAL_ALERT_THRESHOLD_STATE,
operator: AlertThresholdOperator.IS_BELOW,
});
});
it('should set match type', () => {
const result = alertThresholdReducer(INITIAL_ALERT_THRESHOLD_STATE, {
type: 'SET_MATCH_TYPE',
payload: AlertThresholdMatchType.ALL_THE_TIME,
});
expect(result).toEqual({
...INITIAL_ALERT_THRESHOLD_STATE,
matchType: AlertThresholdMatchType.ALL_THE_TIME,
});
});
it('should set thresholds', () => {
const newThresholds = [
{
id: '1',
label: 'critical',
thresholdValue: 100,
recoveryThresholdValue: 90,
unit: 'ms',
channels: ['channel1'],
color: '#FF0000',
},
];
const result = alertThresholdReducer(INITIAL_ALERT_THRESHOLD_STATE, {
type: 'SET_THRESHOLDS',
payload: newThresholds,
});
expect(result).toEqual({
...INITIAL_ALERT_THRESHOLD_STATE,
thresholds: newThresholds,
});
});
it(TEST_RESET_TO_INITIAL_STATE, () => {
const modifiedState: AlertThresholdState = {
selectedQuery: 'B',
operator: AlertThresholdOperator.IS_BELOW,
matchType: AlertThresholdMatchType.ALL_THE_TIME,
evaluationWindow: TimeDuration.TEN_MINUTES,
algorithm: Algorithm.STANDARD,
seasonality: Seasonality.DAILY,
thresholds: [],
};
const result = alertThresholdReducer(modifiedState, { type: 'RESET' });
expect(result).toEqual(INITIAL_ALERT_THRESHOLD_STATE);
});
it(TEST_SET_INITIAL_STATE_FROM_PAYLOAD, () => {
const newState: AlertThresholdState = {
selectedQuery: 'C',
operator: AlertThresholdOperator.IS_EQUAL_TO,
matchType: AlertThresholdMatchType.ON_AVERAGE,
evaluationWindow: TimeDuration.ONE_HOUR,
algorithm: Algorithm.STANDARD,
seasonality: Seasonality.WEEKLY,
thresholds: [],
};
const result = alertThresholdReducer(INITIAL_ALERT_THRESHOLD_STATE, {
type: 'SET_INITIAL_STATE',
payload: newState,
});
expect(result).toEqual(newState);
});
it(TEST_RETURN_STATE_FOR_UNKNOWN_ACTION, () => {
const result = alertThresholdReducer(
INITIAL_ALERT_THRESHOLD_STATE,
{ type: UNKNOWN_ACTION_TYPE } as any,
);
expect(result).toEqual(INITIAL_ALERT_THRESHOLD_STATE);
});
});
describe('advancedOptionsReducer', () => {
it('should set send notification if data is missing', () => {
const result = advancedOptionsReducer(INITIAL_ADVANCED_OPTIONS_STATE, {
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING',
payload: { toleranceLimit: 21, timeUnit: UniversalYAxisUnit.HOURS },
});
expect(result).toEqual({
...INITIAL_ADVANCED_OPTIONS_STATE,
sendNotificationIfDataIsMissing: {
...INITIAL_ADVANCED_OPTIONS_STATE.sendNotificationIfDataIsMissing,
toleranceLimit: 21,
timeUnit: UniversalYAxisUnit.HOURS,
},
});
});
it('should toggle send notification if data is missing', () => {
const result = advancedOptionsReducer(INITIAL_ADVANCED_OPTIONS_STATE, {
type: 'TOGGLE_SEND_NOTIFICATION_IF_DATA_IS_MISSING',
payload: true,
});
expect(result).toEqual({
...INITIAL_ADVANCED_OPTIONS_STATE,
sendNotificationIfDataIsMissing: {
...INITIAL_ADVANCED_OPTIONS_STATE.sendNotificationIfDataIsMissing,
enabled: true,
},
});
});
it('should set enforce minimum datapoints', () => {
const result = advancedOptionsReducer(INITIAL_ADVANCED_OPTIONS_STATE, {
type: 'SET_ENFORCE_MINIMUM_DATAPOINTS',
payload: { minimumDatapoints: 10 },
});
expect(result).toEqual({
...INITIAL_ADVANCED_OPTIONS_STATE,
enforceMinimumDatapoints: {
...INITIAL_ADVANCED_OPTIONS_STATE.enforceMinimumDatapoints,
minimumDatapoints: 10,
},
});
});
it('should toggle enforce minimum datapoints', () => {
const result = advancedOptionsReducer(INITIAL_ADVANCED_OPTIONS_STATE, {
type: 'TOGGLE_ENFORCE_MINIMUM_DATAPOINTS',
payload: true,
});
expect(result).toEqual({
...INITIAL_ADVANCED_OPTIONS_STATE,
enforceMinimumDatapoints: {
...INITIAL_ADVANCED_OPTIONS_STATE.enforceMinimumDatapoints,
enabled: true,
},
});
});
it('should set delay evaluation', () => {
const result = advancedOptionsReducer(INITIAL_ADVANCED_OPTIONS_STATE, {
type: 'SET_DELAY_EVALUATION',
payload: { delay: 10, timeUnit: UniversalYAxisUnit.HOURS },
});
expect(result).toEqual({
...INITIAL_ADVANCED_OPTIONS_STATE,
delayEvaluation: { delay: 10, timeUnit: UniversalYAxisUnit.HOURS },
});
});
it('should set evaluation cadence', () => {
const newCadence = {
default: { value: 5, timeUnit: UniversalYAxisUnit.HOURS },
custom: {
repeatEvery: 'week',
startAt: '12:00:00',
timezone: 'America/New_York',
occurence: ['Monday', 'Friday'],
},
rrule: { date: dayjs(), startAt: '10:00:00', rrule: 'test-rrule' },
};
const result = advancedOptionsReducer(INITIAL_ADVANCED_OPTIONS_STATE, {
type: 'SET_EVALUATION_CADENCE',
payload: newCadence,
});
expect(result).toEqual({
...INITIAL_ADVANCED_OPTIONS_STATE,
evaluationCadence: {
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
...newCadence,
},
});
});
it('should set evaluation cadence mode', () => {
const result = advancedOptionsReducer(INITIAL_ADVANCED_OPTIONS_STATE, {
type: 'SET_EVALUATION_CADENCE_MODE',
payload: 'custom',
});
expect(result).toEqual({
...INITIAL_ADVANCED_OPTIONS_STATE,
evaluationCadence: {
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
mode: 'custom',
},
});
});
it(TEST_RESET_TO_INITIAL_STATE, () => {
const modifiedState: AdvancedOptionsState = {
...INITIAL_ADVANCED_OPTIONS_STATE,
delayEvaluation: { delay: 10, timeUnit: UniversalYAxisUnit.HOURS },
};
const result = advancedOptionsReducer(modifiedState, { type: 'RESET' });
expect(result).toEqual(INITIAL_ADVANCED_OPTIONS_STATE);
});
it(TEST_SET_INITIAL_STATE_FROM_PAYLOAD, () => {
const newState: AdvancedOptionsState = {
...INITIAL_ADVANCED_OPTIONS_STATE,
sendNotificationIfDataIsMissing: {
toleranceLimit: 45,
timeUnit: UniversalYAxisUnit.SECONDS,
enabled: true,
},
};
const result = advancedOptionsReducer(INITIAL_ADVANCED_OPTIONS_STATE, {
type: 'SET_INITIAL_STATE',
payload: newState,
});
expect(result).toEqual(newState);
});
it(TEST_RETURN_STATE_FOR_UNKNOWN_ACTION, () => {
const result = advancedOptionsReducer(
INITIAL_ADVANCED_OPTIONS_STATE,
{ type: UNKNOWN_ACTION_TYPE } as any,
);
expect(result).toEqual(INITIAL_ADVANCED_OPTIONS_STATE);
});
});
describe('evaluationWindowReducer', () => {
it('should set window type to rolling and reset timeframe', () => {
const modifiedState: EvaluationWindowState = {
...INITIAL_EVALUATION_WINDOW_STATE,
windowType: 'cumulative',
timeframe: 'currentHour',
};
const result = evaluationWindowReducer(modifiedState, {
type: 'SET_WINDOW_TYPE',
payload: 'rolling',
});
expect(result).toEqual({
windowType: 'rolling',
timeframe: INITIAL_EVALUATION_WINDOW_STATE.timeframe,
startingAt: INITIAL_EVALUATION_WINDOW_STATE.startingAt,
});
});
it('should set window type to cumulative and set timeframe to currentHour', () => {
const result = evaluationWindowReducer(INITIAL_EVALUATION_WINDOW_STATE, {
type: 'SET_WINDOW_TYPE',
payload: 'cumulative',
});
expect(result).toEqual({
windowType: 'cumulative',
timeframe: 'currentHour',
startingAt: INITIAL_EVALUATION_WINDOW_STATE.startingAt,
});
});
it('should set timeframe', () => {
const result = evaluationWindowReducer(INITIAL_EVALUATION_WINDOW_STATE, {
type: 'SET_TIMEFRAME',
payload: '10m0s',
});
expect(result).toEqual({
...INITIAL_EVALUATION_WINDOW_STATE,
timeframe: '10m0s',
});
});
it('should set starting at', () => {
const newStartingAt = {
time: '14:30:00',
number: '5',
timezone: 'Europe/London',
unit: UniversalYAxisUnit.HOURS,
};
const result = evaluationWindowReducer(INITIAL_EVALUATION_WINDOW_STATE, {
type: 'SET_STARTING_AT',
payload: newStartingAt,
});
expect(result).toEqual({
...INITIAL_EVALUATION_WINDOW_STATE,
startingAt: newStartingAt,
});
});
it(TEST_RESET_TO_INITIAL_STATE, () => {
const modifiedState: EvaluationWindowState = {
windowType: 'cumulative',
timeframe: 'currentHour',
startingAt: {
time: '12:00:00',
number: '2',
timezone: 'America/New_York',
unit: UniversalYAxisUnit.HOURS,
},
};
const result = evaluationWindowReducer(modifiedState, { type: 'RESET' });
expect(result).toEqual(INITIAL_EVALUATION_WINDOW_STATE);
});
it(TEST_SET_INITIAL_STATE_FROM_PAYLOAD, () => {
const newState: EvaluationWindowState = {
windowType: 'cumulative',
timeframe: 'currentDay',
startingAt: {
time: '09:00:00',
number: '3',
timezone: 'Asia/Tokyo',
unit: UniversalYAxisUnit.HOURS,
},
};
const result = evaluationWindowReducer(INITIAL_EVALUATION_WINDOW_STATE, {
type: 'SET_INITIAL_STATE',
payload: newState,
});
expect(result).toEqual(newState);
});
it(TEST_RETURN_STATE_FOR_UNKNOWN_ACTION, () => {
const result = evaluationWindowReducer(
INITIAL_EVALUATION_WINDOW_STATE,
{ type: UNKNOWN_ACTION_TYPE } as any,
);
expect(result).toEqual(INITIAL_EVALUATION_WINDOW_STATE);
});
});
describe('notificationSettingsReducer', () => {
it('should set multiple notifications', () => {
const notifications = ['channel1', 'channel2', 'channel3'];
const result = notificationSettingsReducer(
INITIAL_NOTIFICATION_SETTINGS_STATE,
{
type: 'SET_MULTIPLE_NOTIFICATIONS',
payload: notifications,
},
);
expect(result).toEqual({
...INITIAL_NOTIFICATION_SETTINGS_STATE,
multipleNotifications: notifications,
});
});
it('should set multiple notifications to null', () => {
const modifiedState = {
...INITIAL_NOTIFICATION_SETTINGS_STATE,
multipleNotifications: ['channel1', 'channel2'],
};
const result = notificationSettingsReducer(modifiedState, {
type: 'SET_MULTIPLE_NOTIFICATIONS',
payload: null,
});
expect(result).toEqual({
...modifiedState,
multipleNotifications: null,
});
});
it('should set re-notification', () => {
const reNotification = {
enabled: true,
value: 60,
unit: UniversalYAxisUnit.HOURS,
conditions: ['firing' as const, 'nodata' as const],
};
const result = notificationSettingsReducer(
INITIAL_NOTIFICATION_SETTINGS_STATE,
{
type: 'SET_RE_NOTIFICATION',
payload: reNotification,
},
);
expect(result).toEqual({
...INITIAL_NOTIFICATION_SETTINGS_STATE,
reNotification,
});
});
it('should set description', () => {
const description = 'Custom alert description with {{$value}}';
const result = notificationSettingsReducer(
INITIAL_NOTIFICATION_SETTINGS_STATE,
{
type: 'SET_DESCRIPTION',
payload: description,
},
);
expect(result).toEqual({
...INITIAL_NOTIFICATION_SETTINGS_STATE,
description,
});
});
it('should set routing policies', () => {
const result = notificationSettingsReducer(
INITIAL_NOTIFICATION_SETTINGS_STATE,
{
type: 'SET_ROUTING_POLICIES',
payload: true,
},
);
expect(result).toEqual({
...INITIAL_NOTIFICATION_SETTINGS_STATE,
routingPolicies: true,
});
});
it(TEST_RESET_TO_INITIAL_STATE, () => {
const modifiedState: NotificationSettingsState = {
multipleNotifications: ['channel1'],
reNotification: {
enabled: true,
value: 120,
unit: UniversalYAxisUnit.HOURS,
conditions: ['firing'],
},
description: 'Modified description',
routingPolicies: true,
};
const result = notificationSettingsReducer(modifiedState, {
type: 'RESET',
});
expect(result).toEqual(INITIAL_NOTIFICATION_SETTINGS_STATE);
});
it(TEST_SET_INITIAL_STATE_FROM_PAYLOAD, () => {
const newState: NotificationSettingsState = {
multipleNotifications: ['channel4', 'channel5'],
reNotification: {
enabled: true,
value: 90,
unit: UniversalYAxisUnit.MINUTES,
conditions: ['nodata'],
},
description: 'New description',
routingPolicies: true,
};
const result = notificationSettingsReducer(
INITIAL_NOTIFICATION_SETTINGS_STATE,
{
type: 'SET_INITIAL_STATE',
payload: newState,
},
);
expect(result).toEqual(newState);
});
it(TEST_RETURN_STATE_FOR_UNKNOWN_ACTION, () => {
const result = notificationSettingsReducer(
INITIAL_NOTIFICATION_SETTINGS_STATE,
{ type: UNKNOWN_ACTION_TYPE } as any,
);
expect(result).toEqual(INITIAL_NOTIFICATION_SETTINGS_STATE);
});
});
});

View File

@@ -51,13 +51,7 @@ export const useCreateAlertState = (): ICreateAlertContextProps => {
export function CreateAlertProvider(
props: ICreateAlertProviderProps,
): JSX.Element {
const {
children,
initialAlertState,
isEditMode,
ruleId,
initialAlertType,
} = props;
const { children, initialAlertState, isEditMode, ruleId } = props;
const [alertState, setAlertState] = useReducer(
alertCreationReducer,
@@ -68,12 +62,9 @@ export function CreateAlertProvider(
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const [alertType, setAlertType] = useState<AlertTypes>(() => {
if (isEditMode) {
return initialAlertType;
}
return getInitialAlertTypeFromURL(queryParams, currentQuery);
});
const [alertType, setAlertType] = useState<AlertTypes>(() =>
getInitialAlertTypeFromURL(queryParams, currentQuery),
);
const handleAlertTypeChange = useCallback(
(value: AlertTypes): void => {

View File

@@ -62,7 +62,7 @@ export const alertCreationReducer = (
export function getInitialAlertType(currentQuery: Query): AlertTypes {
const dataSource =
currentQuery.builder.queryData?.[0]?.dataSource || DataSource.METRICS;
currentQuery.builder.queryData[0].dataSource || DataSource.METRICS;
switch (dataSource) {
case DataSource.METRICS:
return AlertTypes.METRICS_BASED_ALERT;

View File

@@ -1,16 +0,0 @@
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
export function sanitizeDefaultAlertQuery(
query: Query,
alertType: AlertTypes,
): Query {
// If there are no queries, add a default one based on the alert type
if (query.builder.queryData.length === 0) {
const dataSource = ALERTS_DATA_SOURCE_MAP[alertType];
query.builder.queryData.push(initialQueryBuilderFormValuesMap[dataSource]);
}
return query;
}

View File

@@ -23,7 +23,6 @@ import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { sanitizeDefaultAlertQuery } from 'container/EditAlertV2/utils';
import useSortableTable from 'hooks/ResizeTable/useSortableTable';
import useComponentPermission from 'hooks/useComponentPermission';
import useDebouncedFn from 'hooks/useDebouncedFunction';
@@ -37,7 +36,6 @@ import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { GettableAlert } from 'types/api/alerts/get';
import DeleteAlert from './DeleteAlert';
@@ -143,10 +141,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
];
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
const compositeQuery = sanitizeDefaultAlertQuery(
mapQueryDataFromApi(record.condition.compositeQuery),
record.alertType as AlertTypes,
);
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),

View File

@@ -117,11 +117,6 @@ function AlertDetails(): JSX.Element {
}
};
// Show spinner until we have alert data loaded
if (isLoading && !alertRuleDetails) {
return <Spinner />;
}
return (
<CreateAlertProvider
ruleId={ruleId || ''}

View File

@@ -198,7 +198,7 @@ func (r *provider) Match(ctx context.Context, orgID string, ruleID string, set m
}
for _, route := range expressionRoutes {
evaluateExpr, err := r.evaluateExpr(ctx, route.Expression, set)
evaluateExpr, err := r.evaluateExpr(route.Expression, set)
if err != nil {
continue
}
@@ -210,71 +210,32 @@ func (r *provider) Match(ctx context.Context, orgID string, ruleID string, set m
return matchedChannels, nil
}
// convertLabelSetToEnv converts a flat label set with dotted keys into a nested map structure for expr env.
// when both a leaf and a deeper nested path exist (e.g. "foo" and "foo.bar"),
// the nested structure takes precedence. That means we will replace an existing leaf at any
// intermediate path with a map so we can materialize the deeper structure.
// TODO(srikanthccv): we need a better solution to handle this, remove the following
// when we update the expr to support dotted keys
func (r *provider) convertLabelSetToEnv(ctx context.Context, labelSet model.LabelSet) map[string]interface{} {
func (r *provider) evaluateExpr(expression string, labelSet model.LabelSet) (bool, error) {
env := make(map[string]interface{})
logForReview := false
for lk, lv := range labelSet {
key := strings.TrimSpace(string(lk))
value := string(lv)
for k, v := range labelSet {
key := string(k)
value := string(v)
if strings.Contains(key, ".") {
parts := strings.Split(key, ".")
current := env
for i, raw := range parts {
part := strings.TrimSpace(raw)
last := i == len(parts)-1
if last {
if _, isMap := current[part].(map[string]interface{}); isMap {
logForReview = true
// deeper structure already exists; do not overwrite.
break
}
for i, part := range parts {
if i == len(parts)-1 {
current[part] = value
break
} else {
if current[part] == nil {
current[part] = make(map[string]interface{})
}
current = current[part].(map[string]interface{})
}
// ensure a map so we can keep descending.
if nextMap, ok := current[part].(map[string]interface{}); ok {
current = nextMap
continue
}
// if absent or a leaf, replace it with a map.
newMap := make(map[string]interface{})
current[part] = newMap
current = newMap
}
continue
} else {
env[key] = value
}
// if a map already sits here (due to nested keys), keep the map (nested wins).
if _, isMap := env[key].(map[string]interface{}); isMap {
logForReview = true
continue
}
env[key] = value
}
if logForReview {
r.settings.Logger().InfoContext(ctx, "found label set with conflicting prefix dotted keys", "labels", labelSet)
}
return env
}
func (r *provider) evaluateExpr(ctx context.Context, expression string, labelSet model.LabelSet) (bool, error) {
env := r.convertLabelSetToEnv(ctx, labelSet)
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)

View File

@@ -278,9 +278,7 @@ func TestProvider_ConcurrentAccess(t *testing.T) {
}
func TestProvider_EvaluateExpression(t *testing.T) {
provider := &provider{
settings: factory.NewScopedProviderSettings(createTestProviderSettings(), "provider_test"),
}
provider := &provider{}
tests := []struct {
name string
@@ -648,7 +646,7 @@ func TestProvider_EvaluateExpression(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := provider.evaluateExpr(context.Background(), tt.expression, tt.labelSet)
result, err := provider.evaluateExpr(tt.expression, tt.labelSet)
assert.NoError(t, err)
assert.Equal(t, tt.expected, result, "Expression: %s", tt.expression)
})
@@ -909,72 +907,3 @@ func TestProvider_CreateRoutes(t *testing.T) {
})
}
}
func TestConvertLabelSetToEnv(t *testing.T) {
tests := []struct {
name string
labelSet model.LabelSet
expected map[string]interface{}
}{
{
name: "simple keys",
labelSet: model.LabelSet{
"key1": "value1",
"key2": "value2",
},
expected: map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
{
name: "nested keys",
labelSet: model.LabelSet{
"foo.bar": "value1",
"foo.baz": "value2",
},
expected: map[string]interface{}{
"foo": map[string]interface{}{
"bar": "value1",
"baz": "value2",
},
},
},
{
name: "conflict - nested structure wins",
labelSet: model.LabelSet{
"foo.bar.baz": "deep",
"foo.bar": "shallow",
},
expected: map[string]interface{}{
"foo": map[string]interface{}{
"bar": map[string]interface{}{
"baz": "deep",
},
},
},
},
{
name: "conflict - leaf value vs nested",
labelSet: model.LabelSet{
"foo.bar": "value",
"foo": "should_be_ignored",
},
expected: map[string]interface{}{
"foo": map[string]interface{}{
"bar": "value",
},
},
},
}
provider := &provider{
settings: factory.NewScopedProviderSettings(createTestProviderSettings(), "provider_test"),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := provider.convertLabelSetToEnv(context.Background(), tt.labelSet)
assert.Equal(t, tt.expected, result)
})
}
}

View File

@@ -1276,6 +1276,154 @@ func getLocalTableName(tableName string) string {
}
func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
hasCustomRetention, err := r.hasCustomRetentionColumn(ctx)
if hasCustomRetention {
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("SetTTLV2 only supported")}
}
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing TTL")}
}
// uuid is used as transaction id
uuidWithHyphen := uuid.New()
uuid := strings.Replace(uuidWithHyphen.String(), "-", "", -1)
coldStorageDuration := -1
if len(params.ColdStorageVolume) > 0 {
coldStorageDuration = int(params.ToColdStorageDuration)
}
tableNameArray := []string{r.logsDB + "." + r.logsLocalTableV2, r.logsDB + "." + r.logsResourceLocalTableV2}
// check if there is existing things to be done
for _, tableName := range tableNameArray {
statusItem, err := r.checkTTLStatusItem(ctx, orgID, tableName)
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing ttl_status check sql query")}
}
if statusItem.Status == constants.StatusPending {
return nil, &model.ApiError{Typ: model.ErrorConflict, Err: fmt.Errorf("TTL is already running")}
}
}
// TTL query for logs_v2 table
ttlLogsV2 := fmt.Sprintf(
"ALTER TABLE %v ON CLUSTER %s MODIFY TTL toDateTime(timestamp / 1000000000) + "+
"INTERVAL %v SECOND DELETE", tableNameArray[0], r.cluster, params.DelDuration)
if len(params.ColdStorageVolume) > 0 {
ttlLogsV2 += fmt.Sprintf(", toDateTime(timestamp / 1000000000)"+
" + INTERVAL %v SECOND TO VOLUME '%s'",
params.ToColdStorageDuration, params.ColdStorageVolume)
}
// TTL query for logs_v2_resource table
// adding 1800 as our bucket size is 1800 seconds
ttlLogsV2Resource := fmt.Sprintf(
"ALTER TABLE %v ON CLUSTER %s MODIFY TTL toDateTime(seen_at_ts_bucket_start) + toIntervalSecond(1800) + "+
"INTERVAL %v SECOND DELETE", tableNameArray[1], r.cluster, params.DelDuration)
if len(params.ColdStorageVolume) > 0 {
ttlLogsV2Resource += fmt.Sprintf(", toDateTime(seen_at_ts_bucket_start) + toIntervalSecond(1800) + "+
"INTERVAL %v SECOND TO VOLUME '%s'",
params.ToColdStorageDuration, params.ColdStorageVolume)
}
ttlPayload := map[string]string{
tableNameArray[0]: ttlLogsV2,
tableNameArray[1]: ttlLogsV2Resource,
}
// set the ttl if nothing is pending/ no errors
go func(ttlPayload map[string]string) {
for tableName, query := range ttlPayload {
// https://github.com/SigNoz/signoz/issues/5470
// we will change ttl for only the new parts and not the old ones
query += " SETTINGS materialize_ttl_after_modify=0"
ttl := types.TTLSetting{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
TransactionID: uuid,
TableName: tableName,
TTL: int(params.DelDuration),
Status: constants.StatusPending,
ColdStorageTTL: coldStorageDuration,
OrgID: orgID,
}
_, dbErr := r.
sqlDB.
BunDB().
NewInsert().
Model(&ttl).
Exec(ctx)
if dbErr != nil {
zap.L().Error("error in inserting to ttl_status table", zap.Error(dbErr))
return
}
err := r.setColdStorage(context.Background(), tableName, params.ColdStorageVolume)
if err != nil {
zap.L().Error("error in setting cold storage", zap.Error(err))
statusItem, err := r.checkTTLStatusItem(ctx, orgID, tableName)
if err == nil {
_, dbErr := r.
sqlDB.
BunDB().
NewUpdate().
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Where("id = ?", statusItem.ID.StringValue()).
Exec(ctx)
if dbErr != nil {
zap.L().Error("Error in processing ttl_status update sql query", zap.Error(dbErr))
return
}
}
return
}
zap.L().Info("Executing TTL request: ", zap.String("request", query))
statusItem, _ := r.checkTTLStatusItem(ctx, orgID, tableName)
if err := r.db.Exec(ctx, query); err != nil {
zap.L().Error("error while setting ttl", zap.Error(err))
_, dbErr := r.
sqlDB.
BunDB().
NewUpdate().
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Where("id = ?", statusItem.ID.StringValue()).
Exec(ctx)
if dbErr != nil {
zap.L().Error("Error in processing ttl_status update sql query", zap.Error(dbErr))
return
}
return
}
_, dbErr = r.
sqlDB.
BunDB().
NewUpdate().
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusSuccess).
Where("id = ?", statusItem.ID.StringValue()).
Exec(ctx)
if dbErr != nil {
zap.L().Error("Error in processing ttl_status update sql query", zap.Error(dbErr))
return
}
}
}(ttlPayload)
return &model.SetTTLResponseItem{Message: "move ttl has been successfully set up"}, nil
}
func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
// uuid is used as transaction id
uuidWithHyphen := uuid.New()
@@ -1895,19 +2043,6 @@ func (r *ClickHouseReader) validateTTLConditions(ctx context.Context, ttlConditi
func (r *ClickHouseReader) SetTTL(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
// Keep only latest 100 transactions/requests
r.deleteTtlTransactions(ctx, orgID, 100)
switch params.Type {
case constants.TraceTTL:
return r.setTTLTraces(ctx, orgID, params)
case constants.MetricsTTL:
return r.setTTLMetrics(ctx, orgID, params)
default:
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error while setting ttl. ttl type should be <metrics|traces>, got %v", params.Type)}
}
}
func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
// uuid is used as transaction id
uuidWithHyphen := uuid.New()
uuid := strings.Replace(uuidWithHyphen.String(), "-", "", -1)
@@ -1916,69 +2051,95 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para
if len(params.ColdStorageVolume) > 0 {
coldStorageDuration = int(params.ToColdStorageDuration)
}
tableNames := []string{
signozMetricDBName + "." + signozSampleLocalTableName,
signozMetricDBName + "." + signozSamplesAgg5mLocalTableName,
signozMetricDBName + "." + signozSamplesAgg30mLocalTableName,
signozMetricDBName + "." + signozExpHistLocalTableName,
signozMetricDBName + "." + signozTSLocalTableNameV4,
signozMetricDBName + "." + signozTSLocalTableNameV46Hrs,
signozMetricDBName + "." + signozTSLocalTableNameV41Day,
signozMetricDBName + "." + signozTSLocalTableNameV41Week,
}
for _, tableName := range tableNames {
statusItem, err := r.checkTTLStatusItem(ctx, orgID, tableName)
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing ttl_status check sql query")}
}
if statusItem.Status == constants.StatusPending {
return nil, &model.ApiError{Typ: model.ErrorConflict, Err: fmt.Errorf("TTL is already running")}
}
}
metricTTL := func(tableName string) {
ttl := types.TTLSetting{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
TransactionID: uuid,
TableName: tableName,
TTL: int(params.DelDuration),
Status: constants.StatusPending,
ColdStorageTTL: coldStorageDuration,
OrgID: orgID,
}
_, dbErr := r.
sqlDB.
BunDB().
NewInsert().
Model(&ttl).
Exec(ctx)
if dbErr != nil {
zap.L().Error("error in inserting to ttl_status table", zap.Error(dbErr))
return
}
timeColumn := "timestamp_ms"
if strings.Contains(tableName, "v4") || strings.Contains(tableName, "exp_hist") {
timeColumn = "unix_milli"
}
req := fmt.Sprintf(
"ALTER TABLE %v ON CLUSTER %s MODIFY TTL toDateTime(toUInt32(%s / 1000), 'UTC') + "+
"INTERVAL %v SECOND DELETE", tableName, r.cluster, timeColumn, params.DelDuration)
if len(params.ColdStorageVolume) > 0 {
req += fmt.Sprintf(", toDateTime(toUInt32(%s / 1000), 'UTC')"+
" + INTERVAL %v SECOND TO VOLUME '%s'",
timeColumn, params.ToColdStorageDuration, params.ColdStorageVolume)
switch params.Type {
case constants.TraceTTL:
return r.setTTLTraces(ctx, orgID, params)
case constants.MetricsTTL:
tableNames := []string{
signozMetricDBName + "." + signozSampleLocalTableName,
signozMetricDBName + "." + signozSamplesAgg5mLocalTableName,
signozMetricDBName + "." + signozSamplesAgg30mLocalTableName,
signozMetricDBName + "." + signozExpHistLocalTableName,
signozMetricDBName + "." + signozTSLocalTableNameV4,
signozMetricDBName + "." + signozTSLocalTableNameV46Hrs,
signozMetricDBName + "." + signozTSLocalTableNameV41Day,
signozMetricDBName + "." + signozTSLocalTableNameV41Week,
}
err := r.setColdStorage(context.Background(), tableName, params.ColdStorageVolume)
if err != nil {
zap.L().Error("Error in setting cold storage", zap.Error(err))
for _, tableName := range tableNames {
statusItem, err := r.checkTTLStatusItem(ctx, orgID, tableName)
if err == nil {
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing ttl_status check sql query")}
}
if statusItem.Status == constants.StatusPending {
return nil, &model.ApiError{Typ: model.ErrorConflict, Err: fmt.Errorf("TTL is already running")}
}
}
metricTTL := func(tableName string) {
ttl := types.TTLSetting{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
TransactionID: uuid,
TableName: tableName,
TTL: int(params.DelDuration),
Status: constants.StatusPending,
ColdStorageTTL: coldStorageDuration,
OrgID: orgID,
}
_, dbErr := r.
sqlDB.
BunDB().
NewInsert().
Model(&ttl).
Exec(ctx)
if dbErr != nil {
zap.L().Error("error in inserting to ttl_status table", zap.Error(dbErr))
return
}
timeColumn := "timestamp_ms"
if strings.Contains(tableName, "v4") || strings.Contains(tableName, "exp_hist") {
timeColumn = "unix_milli"
}
req := fmt.Sprintf(
"ALTER TABLE %v ON CLUSTER %s MODIFY TTL toDateTime(toUInt32(%s / 1000), 'UTC') + "+
"INTERVAL %v SECOND DELETE", tableName, r.cluster, timeColumn, params.DelDuration)
if len(params.ColdStorageVolume) > 0 {
req += fmt.Sprintf(", toDateTime(toUInt32(%s / 1000), 'UTC')"+
" + INTERVAL %v SECOND TO VOLUME '%s'",
timeColumn, params.ToColdStorageDuration, params.ColdStorageVolume)
}
err := r.setColdStorage(context.Background(), tableName, params.ColdStorageVolume)
if err != nil {
zap.L().Error("Error in setting cold storage", zap.Error(err))
statusItem, err := r.checkTTLStatusItem(ctx, orgID, tableName)
if err == nil {
_, dbErr := r.
sqlDB.
BunDB().
NewUpdate().
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Where("id = ?", statusItem.ID.StringValue()).
Exec(ctx)
if dbErr != nil {
zap.L().Error("Error in processing ttl_status update sql query", zap.Error(dbErr))
return
}
}
return
}
req += " SETTINGS materialize_ttl_after_modify=0"
zap.L().Info("Executing TTL request: ", zap.String("request", req))
statusItem, _ := r.checkTTLStatusItem(ctx, orgID, tableName)
if err := r.db.Exec(ctx, req); err != nil {
zap.L().Error("error while setting ttl.", zap.Error(err))
_, dbErr := r.
sqlDB.
BunDB().
@@ -1992,46 +2153,32 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para
zap.L().Error("Error in processing ttl_status update sql query", zap.Error(dbErr))
return
}
return
}
return
}
req += " SETTINGS materialize_ttl_after_modify=0"
zap.L().Info("Executing TTL request: ", zap.String("request", req))
statusItem, _ := r.checkTTLStatusItem(ctx, orgID, tableName)
if err := r.db.Exec(ctx, req); err != nil {
zap.L().Error("error while setting ttl.", zap.Error(err))
_, dbErr := r.
_, dbErr = r.
sqlDB.
BunDB().
NewUpdate().
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Set("status = ?", constants.StatusSuccess).
Where("id = ?", statusItem.ID.StringValue()).
Exec(ctx)
if dbErr != nil {
zap.L().Error("Error in processing ttl_status update sql query", zap.Error(dbErr))
return
}
return
}
_, dbErr = r.
sqlDB.
BunDB().
NewUpdate().
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusSuccess).
Where("id = ?", statusItem.ID.StringValue()).
Exec(ctx)
if dbErr != nil {
zap.L().Error("Error in processing ttl_status update sql query", zap.Error(dbErr))
return
for _, tableName := range tableNames {
go metricTTL(tableName)
}
case constants.LogsTTL:
return r.setTTLLogs(ctx, orgID, params)
default:
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error while setting ttl. ttl type should be <metrics|traces>, got %v", params.Type)}
}
for _, tableName := range tableNames {
go metricTTL(tableName)
}
return &model.SetTTLResponseItem{Message: "move ttl has been successfully set up"}, nil
}

View File

@@ -13,7 +13,6 @@ import (
)
var SupportedFunctions = []string{
"abs",
"exp",
"log",
"ln",

View File

@@ -197,14 +197,10 @@ func processResults(
}, nil
}
var SupportedFunctions = []string{"abs", "exp", "log", "ln", "exp2", "log2", "exp10", "log10", "sqrt", "cbrt", "erf", "erfc", "lgamma", "tgamma", "sin", "cos", "tan", "asin", "acos", "atan", "degrees", "radians", "now", "toUnixTimestamp"}
var SupportedFunctions = []string{"exp", "log", "ln", "exp2", "log2", "exp10", "log10", "sqrt", "cbrt", "erf", "erfc", "lgamma", "tgamma", "sin", "cos", "tan", "asin", "acos", "atan", "degrees", "radians", "now", "toUnixTimestamp"}
func EvalFuncs() map[string]govaluate.ExpressionFunction {
GoValuateFuncs := make(map[string]govaluate.ExpressionFunction)
// Returns the absolute value of the given argument.
GoValuateFuncs["abs"] = func(args ...interface{}) (interface{}, error) {
return math.Abs(args[0].(float64)), nil
}
// Returns e to the power of the given argument.
GoValuateFuncs["exp"] = func(args ...interface{}) (interface{}, error) {
return math.Exp(args[0].(float64)), nil

View File

@@ -545,9 +545,6 @@ func EvalFuncs() map[string]govaluate.ExpressionFunction {
rad180 := 180 / math.Pi
// Mathematical functions
funcs["abs"] = func(args ...any) (any, error) {
return math.Abs(args[0].(float64)), nil
}
funcs["exp"] = func(args ...any) (any, error) {
return math.Exp(args[0].(float64)), nil
}
@@ -626,7 +623,7 @@ func EvalFuncs() map[string]govaluate.ExpressionFunction {
// GetSupportedFunctions returns the list of supported function names
func GetSupportedFunctions() []string {
return []string{
"abs", "exp", "log", "ln", "exp2", "log2", "exp10", "log10",
"exp", "log", "ln", "exp2", "log2", "exp10", "log10",
"sqrt", "cbrt", "erf", "erfc", "lgamma", "tgamma",
"sin", "cos", "tan", "asin", "acos", "atan",
"degrees", "radians", "now",

View File

@@ -863,39 +863,3 @@ func TestComplexExpression(t *testing.T) {
}
}
}
func TestAbsValueExpression(t *testing.T) {
tsData := map[string]*TimeSeriesData{
"A": createFormulaTestTimeSeriesData("A", []*TimeSeries{
{
Labels: createLabels(map[string]string{"service_name": "frontend"}),
Values: createValues(map[int64]float64{
1: -10,
2: 20,
}),
},
}),
"B": createFormulaTestTimeSeriesData("B", []*TimeSeries{
{
Labels: createLabels(map[string]string{"service_name": "frontend"}),
Values: createValues(map[int64]float64{
1: 5,
2: -4,
}),
},
}),
}
evaluator, err := NewFormulaEvaluator("abs(A) + abs(B)", map[string]bool{"A": true, "B": true})
require.NoError(t, err)
result, err := evaluator.EvaluateFormula(tsData)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, 1, len(result))
series := result[0]
require.Equal(t, 2, len(series.Values))
assert.Equal(t, 15.0, series.Values[0].Value) // |10| + |5| = 15
assert.Equal(t, 24.0, series.Values[1].Value) // |20| + |4| = 24
}

View File

@@ -134,6 +134,11 @@ func (q *QueryBuilderQuery[T]) Validate(requestType RequestType) error {
return err
}
// Validate GroupBy
if err := q.validateGroupByFields(); err != nil {
return err
}
if requestType != RequestTypeRaw && requestType != RequestTypeTrace && len(q.Aggregations) > 0 {
if err := q.validateOrderByForAggregation(); err != nil {
return err
@@ -168,6 +173,27 @@ func (q *QueryBuilderQuery[T]) validateSelectFields() error {
"isRoot and isEntryPoint fields are not supported in selectFields",
)
}
// for logs the selectFields is not present.
// in traces, timestamp is added by default, so it will conflict with timestamp attribute.
if v.Name == "timestamp" {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"timestamp field is not supported in selectFields, it's added by default where needed",
)
}
}
return nil
}
func (q *QueryBuilderQuery[T]) validateGroupByFields() error {
for _, v := range q.GroupBy {
if v.Name == "timestamp" {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"timestamp field is not supported in groupBy, it's added by default where needed",
)
}
}
return nil
}

View File

@@ -29,15 +29,7 @@ class LogsResource(ABC):
self.seen_at_ts_bucket_start = seen_at_ts_bucket_start
def np_arr(self) -> np.array:
return np.array(
[
self.labels,
self.fingerprint,
self.seen_at_ts_bucket_start,
np.uint64(10),
np.uint64(15),
]
)
return np.array([self.labels, self.fingerprint, self.seen_at_ts_bucket_start, np.uint64(10),np.uint64(15)])
class LogsResourceOrAttributeKeys(ABC):
@@ -389,7 +381,7 @@ def insert_logs(
table="distributed_logs_resource_keys",
data=[resource_key.np_arr() for resource_key in resource_keys],
)
clickhouse.conn.insert(
database="signoz_logs",
table="distributed_logs_v2",

View File

@@ -1,603 +0,0 @@
"""
Summary:
This test file contains integration tests for Time-To-Live (TTL) and custom retention policies in SigNoz's query service.
It verifies the correct behavior of TTL settings for traces, metrics, and logs, including support for cold storage, custom retention conditions, error handling for invalid configurations, and retrieval of TTL settings.
"""
import time
from http import HTTPStatus
import pytest
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.logger import setup_logger
from fixtures.logs import Logs
logger = setup_logger(__name__)
@pytest.fixture(name="ttl_test_suite_setup", scope="package", autouse=True)
def ttl_test_suite_setup(create_user_admin): # pylint: disable=unused-argument
# This fixture creates a admin user for the entire ttl test suite
# The create_user_admin fixture is executed just by being a dependency
print("Setting up ttl test suite")
yield
def test_set_ttl_traces_success(signoz: types.SigNoz, get_jwt_token):
"""Test setting TTL for traces with new ttlConfig structure."""
payload = {
"type": "traces",
"duration": "3600h",
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/settings/ttl"),
params=payload,
headers=headers,
timeout=30,
)
print(response.text)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
assert "message" in response_data
assert "successfully set up" in response_data["message"].lower()
# Verify TTL settings in Clickhouse
# Allow some time for the TTL to be applied
time.sleep(2)
# Check TTL settings on relevant tables
tables_to_check = [
"signoz_index_v3",
"traces_v3_resource",
"signoz_error_index_v2",
"usage_explorer",
"dependency_graph_minutes_v2",
"trace_summary",
]
# Query to get table engine info which includes TTL
table_list = ", ".join(f"'{table}'" for table in tables_to_check)
query = f"SELECT engine_full FROM system.tables WHERE table in [{table_list}]"
result = signoz.telemetrystore.conn.query(query).result_rows
# Verify TTL exists in all table definitions
assert all("TTL" in r[0] for r in result)
assert all(" SETTINGS" in r[0] for r in result)
ttl_parts = [r[0].split("TTL ")[1].split(" SETTINGS")[0] for r in result]
# All TTLs should include toIntervalSecond(12960000) which is 3600h
assert all("toIntervalSecond(12960000)" in ttl_part for ttl_part in ttl_parts)
def test_set_ttl_traces_with_cold_storage(signoz: types.SigNoz, get_jwt_token):
"""Test setting TTL for traces with cold storage configuration."""
payload = {
"type": "traces",
"duration": f"{90*24}h", # 90 days in hours
"coldStorageVolume": "cold_storage_vol",
"toColdStorageDuration": f"{30*24}h", # 30 days in hours
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/settings/ttl"),
params=payload,
headers=headers,
timeout=30,
)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
assert "message" in response_data
assert "successfully set up" in response_data["message"].lower()
def test_set_ttl_metrics_success(signoz: types.SigNoz, get_jwt_token):
"""Test setting TTL for metrics using the new setTTLMetrics method."""
payload = {
"type": "metrics",
"duration": f"{90*24}h", # 90 days in hours
"coldStorageVolume": "",
"toColdStorageDuration": 0,
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/settings/ttl"),
params=payload,
headers=headers,
timeout=30,
)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
assert "message" in response_data
assert "successfully set up" in response_data["message"].lower()
# Verify TTL settings in Clickhouse
# Allow some time for the TTL to be applied
time.sleep(2)
# Check TTL settings on relevant metrics tables
tables_to_check = [
"samples_v4",
"samples_v4_agg_5m",
"samples_v4_agg_30m",
"time_series_v4",
"time_series_v4_6hrs",
"time_series_v4_1day",
"time_series_v4_1week",
]
# Query to get table engine info which includes TTL
table_list = "', '".join(tables_to_check)
query = f"SELECT engine_full FROM system.tables WHERE table in ['{table_list}']"
result = signoz.telemetrystore.conn.query(query).result_rows
# Verify TTL exists in all table definitions
assert all("TTL" in r[0] for r in result)
assert all(" SETTINGS" in r[0] for r in result)
ttl_parts = [r[0].split("TTL ")[1].split(" SETTINGS")[0] for r in result]
# All TTLs should include toIntervalSecond(7776000) which is 90*24h
assert all("toIntervalSecond(7776000)" in ttl_part for ttl_part in ttl_parts)
def test_set_ttl_metrics_with_cold_storage(signoz: types.SigNoz, get_jwt_token):
"""Test setting TTL for metrics with cold storage configuration."""
payload = {
"type": "metrics",
"duration": f"{90*24}h", # 90 days in hours
"coldStorageVolume": "metrics_cold_vol",
"toColdStorageDuration": f"{20*24}h", # 20 days in hours
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/settings/ttl"),
params=payload,
headers=headers,
timeout=30,
)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
assert "message" in response_data
assert "successfully set up" in response_data["message"].lower()
def test_set_ttl_invalid_type(signoz: types.SigNoz, get_jwt_token):
"""Test setting TTL with invalid type returns error."""
payload = {
"type": "invalid_type",
"duration": f"{90*24}h",
"coldStorageVolume": "",
"toColdStorageDuration": 0,
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/settings/ttl"),
params=payload,
headers=headers,
timeout=30,
)
assert response.status_code == HTTPStatus.BAD_REQUEST
def test_set_custom_retention_ttl_basic(signoz: types.SigNoz, get_jwt_token):
"""Test setting custom retention TTL with basic configuration."""
payload = {
"type": "logs",
"defaultTTLDays": 100,
"ttlConditions": [],
"coldStorageVolume": "",
"coldStorageDuration": 0,
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
json=payload,
headers=headers,
timeout=30,
)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
assert "message" in response_data
# Verify TTL settings in Clickhouse
# Allow some time for the TTL to be applied
time.sleep(2)
# Check TTL settings on relevant tables
tables_to_check = [
"logs_v2",
"logs_v2_resource",
]
# Query to get table engine info which includes TTL
table_list = "', '".join(tables_to_check)
query = f"SELECT engine_full FROM system.tables WHERE table in ['{table_list}']"
result = signoz.telemetrystore.conn.query(query).result_rows
# Verify TTL exists in all table definitions
assert all("TTL" in r[0] for r in result)
assert all(" SETTINGS" in r[0] for r in result)
ttl_parts = [r[0].split("TTL ")[1].split(" SETTINGS")[0] for r in result]
# Also verify the TTL parts contain retention_days
assert all("_retention_days" in ttl_part for ttl_part in ttl_parts)
# Query to describe tables and check retention_days column
for table in tables_to_check:
describe_query = f"DESCRIBE TABLE signoz_logs.{table}"
describe_result = signoz.telemetrystore.conn.query(describe_query).result_rows
# Find the _retention_days column
retention_col = next(
(row for row in describe_result if row[0] == "_retention_days"), None
)
assert (
retention_col is not None
), f"_retention_days column not found in table {table}"
assert (
retention_col[1] == "UInt16"
), f"Expected _retention_days to be UInt16 in table {table}, but got {retention_col[1]}"
assert (
retention_col[3] == "100"
), f"Expected default value of _retention_days to be 100 in table {table}, but got {retention_col[3]}"
def test_set_custom_retention_ttl_with_conditions(
signoz: types.SigNoz, get_jwt_token, insert_logs
):
"""Test setting custom retention TTL with filter conditions."""
payload = {
"type": "logs",
"defaultTTLDays": 30,
"ttlConditions": [
{
"conditions": [
{"key": "service_name", "values": ["frontend", "backend"]}
],
"ttlDays": 60,
}
],
"coldStorageVolume": "",
"coldStorageDuration": 0,
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
json=payload,
headers=headers,
timeout=30,
)
assert response.status_code == HTTPStatus.BAD_REQUEST
# Need to ensure that "severity" and "service_name" keys exist in logsAttributeKeys table
# Insert some logs with these attribute keys
logs = [
Logs(resources={"service_name": "frontend"}, severity_text="ERROR"),
Logs(resources={"service_name": "backend"}, severity_text="FATAL"),
]
insert_logs(logs)
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
json=payload,
headers=headers,
timeout=30,
)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
assert "message" in response_data
def test_set_custom_retention_ttl_with_cold_storage(
signoz: types.SigNoz, get_jwt_token, insert_logs
):
"""Test setting custom retention TTL with cold storage configuration."""
payload = {
"type": "logs",
"defaultTTLDays": 60,
"ttlConditions": [
{
"conditions": [{"key": "environment", "values": ["production"]}],
"ttlDays": 180,
}
],
"coldStorageVolume": "logs_cold_storage",
"coldStorageDuration": 30, # 30 days to cold storage
}
# Insert some logs with these attribute keys
logs = [
Logs(resources={"environment": "production"}, severity_text="ERROR"),
]
insert_logs(logs)
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
json=payload,
headers=headers,
timeout=30,
)
assert response.status_code == HTTPStatus.BAD_REQUEST
response_data = response.json()
assert "error" in response_data
assert "message" in response_data["error"]
assert "Unknown storage policy `tiered`" in response_data["error"]["message"]
def test_set_custom_retention_ttl_duplicate_conditions(
signoz: types.SigNoz, get_jwt_token
):
"""Test that duplicate TTL conditions are rejected."""
payload = {
"type": "logs",
"defaultTTLDays": 30,
"ttlConditions": [
{
"conditions": [{"key": "service_name", "values": ["frontend"]}],
"ttlDays": 60,
},
{
"conditions": [
{
"key": "service_name",
"values": ["frontend"], # Duplicate condition
}
],
"ttlDays": 90,
},
],
"coldStorageVolume": "",
"coldStorageDuration": 0,
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
json=payload,
headers=headers,
timeout=30,
)
# Should return error for duplicate conditions
assert response.status_code == HTTPStatus.BAD_REQUEST
def test_set_custom_retention_ttl_invalid_condition(
signoz: types.SigNoz, get_jwt_token
):
"""Test that conditions with empty values are rejected."""
payload = {
"type": "logs",
"defaultTTLDays": 30,
"ttlConditions": [
{
"conditions": [
{
"key": "service_name",
"values": [], # Empty values should be rejected
}
],
"ttlDays": 60,
}
],
"coldStorageVolume": "",
"coldStorageDuration": 0,
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
json=payload,
headers=headers,
timeout=30,
)
# Should return error for empty condition values
assert response.status_code == HTTPStatus.BAD_REQUEST
def test_get_custom_retention_ttl(signoz: types.SigNoz, get_jwt_token, insert_logs):
"""Test getting custom retention TTL configuration."""
# First set a custom retention TTL
set_payload = {
"type": "logs",
"defaultTTLDays": 45,
"ttlConditions": [
{
"conditions": [{"key": "service_name", "values": ["test-service"]}],
"ttlDays": 90,
}
],
"coldStorageVolume": "",
"coldStorageDuration": 0,
}
# Insert some logs with these attribute keys
logs = [
Logs(resources={"service_name": "test-service"}, severity_text="ERROR"),
]
insert_logs(logs)
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
set_response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
json=set_payload,
headers=headers,
timeout=30,
)
assert set_response.status_code == HTTPStatus.OK
# Allow some time for the TTL to be processed
time.sleep(2)
# Now get the TTL configuration
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
get_response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
params={"type": "logs"},
headers=headers,
timeout=30,
)
response_data = get_response.json()
# Verify the response contains expected fields
assert response_data["status"] == "success"
assert response_data["default_ttl_days"] == 45
assert response_data["cold_storage_ttl_days"] == -1
assert response_data["ttl_conditions"][0]["ttlDays"] == 90
assert response_data["ttl_conditions"][0]["conditions"][0]["key"] == "service_name"
assert response_data["ttl_conditions"][0]["conditions"][0]["values"] == [
"test-service"
]
def test_get_ttl_traces_success(signoz: types.SigNoz, get_jwt_token):
"""Test getting TTL for traces."""
# First set a TTL configuration for traces
set_payload = {
"type": "traces",
"duration": "720h", # 30 days in hours
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
set_response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/settings/ttl"),
params=set_payload,
headers=headers,
timeout=30,
)
print(set_response.text)
assert set_response.status_code == HTTPStatus.OK
# Allow some time for the TTL to be processed
time.sleep(2)
# Now get the TTL configuration for traces
get_response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/settings/ttl"),
params={"type": "traces"},
headers=headers,
timeout=30,
)
assert get_response.status_code == HTTPStatus.OK
response_data = get_response.json()
# Verify the response contains expected fields and values
assert response_data["status"] == "success"
assert "traces_ttl_duration_hrs" in response_data
assert "traces_move_ttl_duration_hrs" in response_data
assert (
response_data["traces_ttl_duration_hrs"] == 720
) # Note: response is in hours as integer
assert (
response_data["traces_move_ttl_duration_hrs"] == -1
) # -1 indicates no cold storage configured
def test_large_ttl_conditions_list(signoz: types.SigNoz, get_jwt_token, insert_logs):
"""Test custom retention TTL with many conditions."""
# Create a list of many TTL conditions to test performance and limits
conditions = []
for i in range(10): # Test with 10 conditions
conditions.append(
{
"conditions": [{"key": "service_name", "values": [f"service-{i}"]}],
"ttlDays": 30 + (i * 10),
}
)
logs = [
Logs(resources={"service_name": f"service-{i}"}, severity_text="ERROR")
for i in range(10)
]
insert_logs(logs)
payload = {
"type": "logs",
"defaultTTLDays": 30,
"ttlConditions": conditions,
"coldStorageVolume": "",
"coldStorageDuration": 0,
}
headers = {
"Authorization": f"Bearer {get_jwt_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
json=payload,
headers=headers,
timeout=30,
)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
assert "message" in response_data