mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-22 00:00:27 +01:00
Compare commits
1 Commits
v0.97.1
...
issue_3019
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
445d3e8c3e |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -17,7 +17,6 @@ jobs:
|
||||
- bootstrap
|
||||
- auth
|
||||
- querier
|
||||
- ttl
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)),
|
||||
|
||||
@@ -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 || ''}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
)
|
||||
|
||||
var SupportedFunctions = []string{
|
||||
"abs",
|
||||
"exp",
|
||||
"log",
|
||||
"ln",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user