Merge branch 'main' into enhancement/cmd-click-across-routes

This commit is contained in:
manika-signoz 2025-09-16 10:08:50 +05:30 committed by GitHub
commit 6491e31242
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 4497 additions and 524 deletions

View File

@ -78,4 +78,5 @@ Need assistance? Join our Slack community:
- Set up your [development environment](docs/contributing/development.md)
- Deploy and observe [SigNoz in action with OpenTelemetry Demo Application](docs/otel-demo-docs.md)
- Explore the [SigNoz Community Advocate Program](ADVOCATE.md), which recognises contributors who support the community, share their expertise, and help shape SigNoz's future.
- Explore the [SigNoz Community Advocate Program](ADVOCATE.md), which recognises contributors who support the community, share their expertise, and help shape SigNoz's future.
- Write [integration tests](docs/contributing/go/integration.md)

View File

@ -0,0 +1,213 @@
# Integration Tests
SigNoz uses integration tests to verify that different components work together correctly in a real environment. These tests run against actual services (ClickHouse, PostgreSQL, etc.) to ensure end-to-end functionality.
## How to set up the integration test environment?
### Prerequisites
Before running integration tests, ensure you have the following installed:
- Python 3.13+
- Poetry (for dependency management)
- Docker (for containerized services)
### Initial Setup
1. Navigate to the integration tests directory:
```bash
cd tests/integration
```
2. Install dependencies using Poetry:
```bash
poetry install --no-root
```
### Starting the Test Environment
To spin up all the containers necessary for writing integration tests and keep them running:
```bash
poetry run pytest --basetemp=./tmp/ -vv --reuse src/bootstrap/setup.py::test_setup
```
This command will:
- Start all required services (ClickHouse, PostgreSQL, Zookeeper, etc.)
- Keep containers running due to the `--reuse` flag
- Verify that the setup is working correctly
### Stopping the Test Environment
When you're done writing integration tests, clean up the environment:
```bash
poetry run pytest --basetemp=./tmp/ -vv --teardown -s src/bootstrap/setup.py::test_teardown
```
This will destroy the running integration test setup and clean up resources.
## Understanding the Integration Test Framework
Python and pytest form the foundation of the integration testing framework. Testcontainers are used to spin up disposable integration environments. Wiremock is used to spin up **test doubles** of other services.
- **Why Python/pytest?** It's expressive, low-boilerplate, and has powerful fixture capabilities that make integration testing straightforward. Extensive libraries for HTTP requests, JSON handling, and data analysis (numpy) make it easier to test APIs and verify data
- **Why testcontainers?** They let us spin up isolated dependencies that match our production environment without complex setup.
- **Why wiremock?** Well maintained, documented and extensible.
```
.
├── conftest.py
├── fixtures
│ ├── __init__.py
│ ├── auth.py
│ ├── clickhouse.py
│ ├── fs.py
│ ├── http.py
│ ├── migrator.py
│ ├── network.py
│ ├── postgres.py
│ ├── signoz.py
│ ├── sql.py
│ ├── sqlite.py
│ ├── types.py
│ └── zookeeper.py
├── poetry.lock
├── pyproject.toml
└── src
└── bootstrap
├── __init__.py
├── a_database.py
├── b_register.py
└── c_license.py
```
Each test suite follows some important principles:
1. **Organization**: Test suites live under `src/` in self-contained packages. Fixtures (a pytest concept) live inside `fixtures/`.
2. **Execution Order**: Files are prefixed with `a_`, `b_`, `c_` to ensure sequential execution.
3. **Time Constraints**: Each suite should complete in under 10 minutes (setup takes ~4 mins).
### Test Suite Design
Test suites should target functional domains or subsystems within SigNoz. When designing a test suite, consider these principles:
- **Functional Cohesion**: Group tests around a specific capability or service boundary
- **Data Flow**: Follow the path of data through related components
- **Change Patterns**: Components frequently modified together should be tested together
The exact boundaries for modules are intentionally flexible, allowing teams to define logical groupings based on their specific context and knowledge of the system.
Eg: The **bootstrap** integration test suite validates core system functionality:
- Database initialization
- Version check
Other test suites can be **pipelines, auth, querier.**
## How to write an integration test?
Now start writing an integration test. Create a new file `src/bootstrap/e_version.py` and paste the following:
```python
import requests
from fixtures import types
from fixtures.logger import setup_logger
logger = setup_logger(__name__)
def test_version(signoz: types.SigNoz) -> None:
response = requests.get(signoz.self.host_config.get("/api/v1/version"), timeout=2)
logger.info(response)
```
We have written a simple test which calls the `version` endpoint of the container in step 1. In **order to just run this function, run the following command:**
```bash
poetry run pytest --basetemp=./tmp/ -vv --reuse src/bootstrap/e_version.py::test_version
```
> Note: The `--reuse` flag is used to reuse the environment if it is already running. Always use this flag when writing and running integration tests. If you don't use this flag, the environment will be destroyed and recreated every time you run the test.
Here's another example of how to write a more comprehensive integration test:
```python
from http import HTTPStatus
import requests
from fixtures import types
from fixtures.logger import setup_logger
logger = setup_logger(__name__)
def test_user_registration(signoz: types.SigNoz) -> None:
"""Test user registration functionality."""
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/register"),
json={
"name": "testuser",
"orgId": "",
"orgName": "test.org",
"email": "test@example.com",
"password": "password123Z$",
},
timeout=2,
)
assert response.status_code == HTTPStatus.OK
assert response.json()["setupCompleted"] is True
```
## How to run integration tests?
### Running All Tests
```bash
poetry run pytest --basetemp=./tmp/ -vv --reuse src/
```
### Running Specific Test Categories
```bash
poetry run pytest --basetemp=./tmp/ -vv --reuse src/<suite>
# Run querier tests
poetry run pytest --basetemp=./tmp/ -vv --reuse src/querier/
# Run auth tests
poetry run pytest --basetemp=./tmp/ -vv --reuse src/auth/
```
### Running Individual Tests
```bash
poetry run pytest --basetemp=./tmp/ -vv --reuse src/<suite>/<file>.py::test_name
# Run test_register in file a_register.py in auth suite
poetry run pytest --basetemp=./tmp/ -vv --reuse src/auth/a_register.py::test_register
```
## How to configure different options for integration tests?
Tests can be configured using pytest options:
- `--sqlstore-provider` - Choose database provider (default: postgres)
- `--postgres-version` - PostgreSQL version (default: 15)
- `--clickhouse-version` - ClickHouse version (default: 24.1.2-alpine)
- `--zookeeper-version` - Zookeeper version (default: 3.7.1)
Example:
```bash
poetry run pytest --basetemp=./tmp/ -vv --reuse --sqlstore-provider=postgres --postgres-version=14 src/auth/
```
## What should I remember?
- **Always use the `--reuse` flag** when setting up the environment to keep containers running
- **Use the `--teardown` flag** when cleaning up to avoid resource leaks
- **Follow the naming convention** with alphabetical prefixes for test execution order
- **Use proper timeouts** in HTTP requests to avoid hanging tests
- **Clean up test data** between tests to avoid interference
- **Use descriptive test names** that clearly indicate what is being tested
- **Leverage fixtures** for common setup and authentication
- **Test both success and failure scenarios** to ensure robust functionality

View File

@ -166,16 +166,9 @@ func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) (*v3.
ctx, "prepare query range request v4", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds(),
)
start := ts.Add(-time.Duration(r.EvalWindow())).UnixMilli()
end := ts.UnixMilli()
if r.EvalDelay() > 0 {
start = start - int64(r.EvalDelay().Milliseconds())
end = end - int64(r.EvalDelay().Milliseconds())
}
// round to minute otherwise we could potentially miss data
start = start - (start % (60 * 1000))
end = end - (end % (60 * 1000))
st, en := r.Timestamps(ts)
start := st.UnixMilli()
end := en.UnixMilli()
compositeQuery := r.Condition().CompositeQuery

View File

@ -3,8 +3,10 @@ package rules
import (
"context"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/errors"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
@ -20,6 +22,10 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
var task baserules.Task
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
evaluation, err := opts.Rule.Evaluation.GetEvaluation()
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := baserules.NewThresholdRule(
@ -40,7 +46,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, tr)
// create ch rule task for evalution
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
@ -62,7 +68,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, pr)
// create promql rule task for evalution
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
// create anomaly rule
@ -84,7 +90,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, ar)
// create anomaly rule task for evalution
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else {
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)

View File

@ -0,0 +1,82 @@
import './styles.scss';
import { Button, Tooltip } from 'antd';
import classNames from 'classnames';
import { Activity, ChartLine } from 'lucide-react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { useCreateAlertState } from '../context';
import Stepper from '../Stepper';
import AlertThreshold from './AlertThreshold';
import AnomalyThreshold from './AnomalyThreshold';
import { ANOMALY_TAB_TOOLTIP, THRESHOLD_TAB_TOOLTIP } from './constants';
function AlertCondition(): JSX.Element {
const { alertType, setAlertType } = useCreateAlertState();
const showMultipleTabs =
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
alertType === AlertTypes.METRICS_BASED_ALERT;
const tabs = [
{
label: 'Threshold',
icon: <ChartLine size={14} data-testid="threshold-view" />,
value: AlertTypes.METRICS_BASED_ALERT,
},
...(showMultipleTabs
? [
{
label: 'Anomaly',
icon: <Activity size={14} data-testid="anomaly-view" />,
value: AlertTypes.ANOMALY_BASED_ALERT,
},
]
: []),
];
const handleAlertTypeChange = (value: AlertTypes): void => {
if (!showMultipleTabs) {
return;
}
setAlertType(value);
};
const getTabTooltip = (tab: { value: AlertTypes }): string => {
if (tab.value === AlertTypes.ANOMALY_BASED_ALERT) {
return ANOMALY_TAB_TOOLTIP;
}
return THRESHOLD_TAB_TOOLTIP;
};
return (
<div className="alert-condition-container">
<Stepper stepNumber={2} label="Set alert conditions" />
<div className="alert-condition">
<div className="alert-condition-tabs">
{tabs.map((tab) => (
<Tooltip key={tab.value} title={getTabTooltip(tab)}>
<Button
className={classNames('list-view-tab', 'explorer-view-option', {
'active-tab': alertType === tab.value,
})}
onClick={(): void => {
if (alertType !== tab.value) {
handleAlertTypeChange(tab.value as AlertTypes);
}
}}
>
{tab.icon}
{tab.label}
</Button>
</Tooltip>
))}
</div>
</div>
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && <AlertThreshold />}
{alertType === AlertTypes.ANOMALY_BASED_ALERT && <AnomalyThreshold />}
</div>
);
}
export default AlertCondition;

View File

@ -0,0 +1,162 @@
import './styles.scss';
import { Button, Select, Typography } from 'antd';
import getAllChannels from 'api/channels/getAll';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Plus } from 'lucide-react';
import { useQuery } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { Channels } from 'types/api/channels/getAll';
import APIError from 'types/api/error';
import { useCreateAlertState } from '../context';
import {
INITIAL_INFO_THRESHOLD,
INITIAL_RANDOM_THRESHOLD,
INITIAL_WARNING_THRESHOLD,
THRESHOLD_MATCH_TYPE_OPTIONS,
THRESHOLD_OPERATOR_OPTIONS,
} from '../context/constants';
import ThresholdItem from './ThresholdItem';
import { UpdateThreshold } from './types';
import {
getCategoryByOptionId,
getCategorySelectOptionByName,
getQueryNames,
} from './utils';
function AlertThreshold(): JSX.Element {
const {
alertState,
thresholdState,
setThresholdState,
} = useCreateAlertState();
const { data, isLoading: isLoadingChannels } = useQuery<
SuccessResponseV2<Channels[]>,
APIError
>(['getChannels'], {
queryFn: () => getAllChannels(),
});
const channels = data?.data || [];
const { currentQuery } = useQueryBuilder();
const queryNames = getQueryNames(currentQuery);
const selectedCategory = getCategoryByOptionId(alertState.yAxisUnit || '');
const categorySelectOptions = getCategorySelectOptionByName(
selectedCategory || '',
);
const addThreshold = (): void => {
let newThreshold;
if (thresholdState.thresholds.length === 1) {
newThreshold = INITIAL_WARNING_THRESHOLD;
} else if (thresholdState.thresholds.length === 2) {
newThreshold = INITIAL_INFO_THRESHOLD;
} else {
newThreshold = INITIAL_RANDOM_THRESHOLD;
}
setThresholdState({
type: 'SET_THRESHOLDS',
payload: [...thresholdState.thresholds, newThreshold],
});
};
const removeThreshold = (id: string): void => {
if (thresholdState.thresholds.length > 1) {
setThresholdState({
type: 'SET_THRESHOLDS',
payload: thresholdState.thresholds.filter((t) => t.id !== id),
});
}
};
const updateThreshold: UpdateThreshold = (id, field, value) => {
setThresholdState({
type: 'SET_THRESHOLDS',
payload: thresholdState.thresholds.map((t) =>
t.id === id ? { ...t, [field]: value } : t,
),
});
};
return (
<div className="alert-threshold-container">
{/* Main condition sentence */}
<div className="alert-condition-sentences">
<div className="alert-condition-sentence">
<Typography.Text className="sentence-text">
Send a notification when
</Typography.Text>
<Select
value={thresholdState.selectedQuery}
onChange={(value): void => {
setThresholdState({
type: 'SET_SELECTED_QUERY',
payload: value,
});
}}
style={{ width: 80 }}
options={queryNames}
/>
</div>
<div className="alert-condition-sentence">
<Select
value={thresholdState.operator}
onChange={(value): void => {
setThresholdState({
type: 'SET_OPERATOR',
payload: value,
});
}}
style={{ width: 120 }}
options={THRESHOLD_OPERATOR_OPTIONS}
/>
<Typography.Text className="sentence-text">
the threshold(s)
</Typography.Text>
<Select
value={thresholdState.matchType}
onChange={(value): void => {
setThresholdState({
type: 'SET_MATCH_TYPE',
payload: value,
});
}}
style={{ width: 140 }}
options={THRESHOLD_MATCH_TYPE_OPTIONS}
/>
<Typography.Text className="sentence-text">
during the <strong>Evaluation Window.</strong>
</Typography.Text>
</div>
</div>
<div className="thresholds-section">
{thresholdState.thresholds.map((threshold, index) => (
<ThresholdItem
key={threshold.id}
threshold={threshold}
updateThreshold={updateThreshold}
removeThreshold={removeThreshold}
showRemoveButton={index !== 0 && thresholdState.thresholds.length > 1}
channels={channels}
isLoadingChannels={isLoadingChannels}
units={categorySelectOptions}
/>
))}
<Button
type="dashed"
icon={<Plus size={16} />}
onClick={addThreshold}
className="add-threshold-btn"
>
Add Threshold
</Button>
</div>
</div>
);
}
export default AlertThreshold;

View File

@ -0,0 +1,174 @@
import { Select, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useMemo } from 'react';
import { useCreateAlertState } from '../context';
import {
ANOMALY_ALGORITHM_OPTIONS,
ANOMALY_SEASONALITY_OPTIONS,
ANOMALY_THRESHOLD_MATCH_TYPE_OPTIONS,
ANOMALY_THRESHOLD_OPERATOR_OPTIONS,
ANOMALY_TIME_DURATION_OPTIONS,
} from '../context/constants';
import { getQueryNames } from './utils';
function AnomalyThreshold(): JSX.Element {
const { thresholdState, setThresholdState } = useCreateAlertState();
const { currentQuery } = useQueryBuilder();
const queryNames = getQueryNames(currentQuery);
const deviationOptions = useMemo(() => {
const options = [];
for (let i = 1; i <= 7; i++) {
options.push({ label: i.toString(), value: i });
}
return options;
}, []);
const updateThreshold = (id: string, field: string, value: string): void => {
setThresholdState({
type: 'SET_THRESHOLDS',
payload: thresholdState.thresholds.map((t) =>
t.id === id ? { ...t, [field]: value } : t,
),
});
};
return (
<div className="anomaly-threshold-container">
<div className="alert-condition-sentences">
{/* Sentence 1 */}
<div className="alert-condition-sentence">
<Typography.Text data-testid="notification-text" className="sentence-text">
Send notification when the observed value for
</Typography.Text>
<Select
value={thresholdState.selectedQuery}
data-testid="query-select"
onChange={(value): void => {
setThresholdState({
type: 'SET_SELECTED_QUERY',
payload: value,
});
}}
style={{ width: 80 }}
options={queryNames}
/>
<Typography.Text
data-testid="evaluation-window-text"
className="sentence-text"
>
during the last
</Typography.Text>
<Select
value={thresholdState.evaluationWindow}
data-testid="evaluation-window-select"
onChange={(value): void => {
setThresholdState({
type: 'SET_EVALUATION_WINDOW',
payload: value,
});
}}
style={{ width: 80 }}
options={ANOMALY_TIME_DURATION_OPTIONS}
/>
</div>
{/* Sentence 2 */}
<div className="alert-condition-sentence">
<Typography.Text data-testid="threshold-text" className="sentence-text">
is
</Typography.Text>
<Select
value={thresholdState.thresholds[0].thresholdValue}
data-testid="threshold-value-select"
onChange={(value): void => {
updateThreshold(
thresholdState.thresholds[0].id,
'thresholdValue',
value.toString(),
);
}}
style={{ width: 80 }}
options={deviationOptions}
/>
<Typography.Text data-testid="deviations-text" className="sentence-text">
deviations
</Typography.Text>
<Select
value={thresholdState.operator}
data-testid="operator-select"
onChange={(value): void => {
setThresholdState({
type: 'SET_OPERATOR',
payload: value,
});
}}
style={{ width: 80 }}
options={ANOMALY_THRESHOLD_OPERATOR_OPTIONS}
/>
<Typography.Text
data-testid="predicted-data-text"
className="sentence-text"
>
the predicted data
</Typography.Text>
<Select
value={thresholdState.matchType}
data-testid="match-type-select"
onChange={(value): void => {
setThresholdState({
type: 'SET_MATCH_TYPE',
payload: value,
});
}}
style={{ width: 80 }}
options={ANOMALY_THRESHOLD_MATCH_TYPE_OPTIONS}
/>
</div>
{/* Sentence 3 */}
<div className="alert-condition-sentence">
<Typography.Text data-testid="using-the-text" className="sentence-text">
using the
</Typography.Text>
<Select
value={thresholdState.algorithm}
data-testid="algorithm-select"
onChange={(value): void => {
setThresholdState({
type: 'SET_ALGORITHM',
payload: value,
});
}}
style={{ width: 80 }}
options={ANOMALY_ALGORITHM_OPTIONS}
/>
<Typography.Text
data-testid="algorithm-with-text"
className="sentence-text"
>
algorithm with
</Typography.Text>
<Select
value={thresholdState.seasonality}
data-testid="seasonality-select"
onChange={(value): void => {
setThresholdState({
type: 'SET_SEASONALITY',
payload: value,
});
}}
style={{ width: 80 }}
options={ANOMALY_SEASONALITY_OPTIONS}
/>
<Typography.Text data-testid="seasonality-text" className="sentence-text">
seasonality
</Typography.Text>
</div>
</div>
</div>
);
}
export default AnomalyThreshold;

View File

@ -0,0 +1,135 @@
import { Button, Input, Select, Space, Tooltip, Typography } from 'antd';
import { ChartLine, CircleX } from 'lucide-react';
import { useMemo, useState } from 'react';
import { ThresholdItemProps } from './types';
function ThresholdItem({
threshold,
updateThreshold,
removeThreshold,
showRemoveButton,
channels,
units,
}: ThresholdItemProps): JSX.Element {
const [showRecoveryThreshold, setShowRecoveryThreshold] = useState(false);
const yAxisUnitSelect = useMemo(() => {
let component = (
<Select
placeholder="Unit"
value={threshold.unit ? threshold.unit : null}
onChange={(value): void => updateThreshold(threshold.id, 'unit', value)}
style={{ width: 150 }}
options={units}
disabled={units.length === 0}
/>
);
if (units.length === 0) {
component = (
<Tooltip
trigger="hover"
title="Please select a Y-axis unit for the query first"
>
<Select
placeholder="Unit"
value={threshold.unit ? threshold.unit : null}
onChange={(value): void => updateThreshold(threshold.id, 'unit', value)}
style={{ width: 150 }}
options={units}
disabled={units.length === 0}
/>
</Tooltip>
);
}
return component;
}, [units, threshold.unit, updateThreshold, threshold.id]);
return (
<div key={threshold.id} className="threshold-item">
<div className="threshold-row">
<div className="threshold-indicator">
<div
className="threshold-dot"
style={{ backgroundColor: threshold.color }}
/>
</div>
<Space className="threshold-controls">
<div className="threshold-inputs">
<Input.Group>
<Input
placeholder="Enter threshold name"
value={threshold.label}
onChange={(e): void =>
updateThreshold(threshold.id, 'label', e.target.value)
}
style={{ width: 260 }}
/>
<Input
placeholder="Enter threshold value"
value={threshold.thresholdValue}
onChange={(e): void =>
updateThreshold(threshold.id, 'thresholdValue', e.target.value)
}
style={{ width: 210 }}
/>
{yAxisUnitSelect}
</Input.Group>
</div>
<Typography.Text className="sentence-text">to</Typography.Text>
<Select
value={threshold.channels}
onChange={(value): void =>
updateThreshold(threshold.id, 'channels', value)
}
style={{ width: 260 }}
options={channels.map((channel) => ({
value: channel.id,
label: channel.name,
}))}
mode="multiple"
placeholder="Select notification channels"
/>
<Button.Group>
{!showRecoveryThreshold && (
<Button
type="default"
icon={<ChartLine size={16} />}
className="icon-btn"
onClick={(): void => setShowRecoveryThreshold(true)}
/>
)}
{showRemoveButton && (
<Button
type="default"
icon={<CircleX size={16} />}
onClick={(): void => removeThreshold(threshold.id)}
className="icon-btn"
/>
)}
</Button.Group>
</Space>
</div>
{showRecoveryThreshold && (
<Input.Group className="recovery-threshold-input-group">
<Input
placeholder="Recovery threshold"
disabled
style={{ width: 260 }}
className="recovery-threshold-label"
/>
<Input
placeholder="Enter recovery threshold value"
value={threshold.recoveryThresholdValue}
onChange={(e): void =>
updateThreshold(threshold.id, 'recoveryThresholdValue', e.target.value)
}
style={{ width: 210 }}
/>
</Input.Group>
)}
</div>
);
}
export default ThresholdItem;

View File

@ -0,0 +1,271 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/jsx-props-no-spreading */
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import { CreateAlertProvider } from '../../context';
import AlertCondition from '../AlertCondition';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock: any = jest.fn(() => ({
paths,
}));
uplotMock.paths = paths;
return uplotMock;
});
const STEPPER_TEST_ID = 'stepper';
const ALERT_THRESHOLD_TEST_ID = 'alert-threshold';
const ANOMALY_THRESHOLD_TEST_ID = 'anomaly-threshold';
const THRESHOLD_VIEW_TEST_ID = 'threshold-view';
const ANOMALY_VIEW_TEST_ID = 'anomaly-view';
const ANOMALY_TAB_TEXT = 'Anomaly';
const THRESHOLD_TAB_TEXT = 'Threshold';
const ACTIVE_TAB_CLASS = '.active-tab';
// Mock the Stepper component
jest.mock('../../Stepper', () => ({
__esModule: true,
default: function MockStepper({
stepNumber,
label,
}: {
stepNumber: number;
label: string;
}): JSX.Element {
return (
<div data-testid={STEPPER_TEST_ID}>{`Step ${stepNumber}: ${label}`}</div>
);
},
}));
// Mock the AlertThreshold component
jest.mock('../AlertThreshold', () => ({
__esModule: true,
default: function MockAlertThreshold(): JSX.Element {
return (
<div data-testid={ALERT_THRESHOLD_TEST_ID}>Alert Threshold Component</div>
);
},
}));
// Mock the AnomalyThreshold component
jest.mock('../AnomalyThreshold', () => ({
__esModule: true,
default: function MockAnomalyThreshold(): JSX.Element {
return (
<div data-testid={ANOMALY_THRESHOLD_TEST_ID}>
Anomaly Threshold Component
</div>
);
},
}));
// Mock useQueryBuilder hook
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): {
currentQuery: {
builder: { queryData: unknown[]; queryFormulas: unknown[] };
dataSource: string;
queryName: string;
};
redirectWithQueryBuilderData: () => void;
} => ({
currentQuery: {
dataSource: 'METRICS',
queryName: 'A',
builder: {
queryData: [{ dataSource: 'METRICS' }],
queryFormulas: [],
},
},
redirectWithQueryBuilderData: jest.fn(),
}),
}));
const createTestQueryClient = (): QueryClient =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const renderAlertCondition = (
alertType?: string,
): ReturnType<typeof render> => {
const queryClient = createTestQueryClient();
const initialEntries = alertType ? [`/?alertType=${alertType}`] : undefined;
return render(
<MemoryRouter initialEntries={initialEntries}>
<QueryClientProvider client={queryClient}>
<CreateAlertProvider>
<AlertCondition />
</CreateAlertProvider>
</QueryClientProvider>
</MemoryRouter>,
);
};
describe('AlertCondition', () => {
it('renders the stepper with correct step number and label', () => {
renderAlertCondition();
expect(screen.getByTestId(STEPPER_TEST_ID)).toHaveTextContent(
'Step 2: Set alert conditions',
);
});
it('verifies default props and initial state', () => {
renderAlertCondition();
// Verify default alertType is METRICS_BASED_ALERT (shows AlertThreshold component)
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
expect(
screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
).not.toBeInTheDocument();
// Verify threshold tab is active by default
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
expect(thresholdTab.closest(ACTIVE_TAB_CLASS)).toBeInTheDocument();
// Verify both tabs are visible (METRICS_BASED_ALERT supports multiple tabs)
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
});
it('renders threshold tab by default', () => {
renderAlertCondition();
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
// Verify default props
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
expect(
screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
).not.toBeInTheDocument();
});
it('renders anomaly tab when alert type supports multiple tabs', () => {
renderAlertCondition();
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
});
it('shows AlertThreshold component when alert type is not anomaly based', () => {
renderAlertCondition();
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
expect(
screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
).not.toBeInTheDocument();
});
it('shows AnomalyThreshold component when alert type is anomaly based', () => {
renderAlertCondition();
// Click on anomaly tab to switch to anomaly-based alert
const anomalyTab = screen.getByText(ANOMALY_TAB_TEXT);
fireEvent.click(anomalyTab);
expect(screen.getByTestId(ANOMALY_THRESHOLD_TEST_ID)).toBeInTheDocument();
expect(screen.queryByTestId(ALERT_THRESHOLD_TEST_ID)).not.toBeInTheDocument();
});
it('switches between threshold and anomaly tabs', () => {
renderAlertCondition();
// Initially shows threshold component
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
// Click anomaly tab
const anomalyTab = screen.getByText(ANOMALY_TAB_TEXT);
fireEvent.click(anomalyTab);
// Should show anomaly component
expect(screen.getByTestId(ANOMALY_THRESHOLD_TEST_ID)).toBeInTheDocument();
expect(screen.queryByTestId(ALERT_THRESHOLD_TEST_ID)).not.toBeInTheDocument();
// Click threshold tab
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
fireEvent.click(thresholdTab);
// Should show threshold component again
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
expect(
screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
).not.toBeInTheDocument();
});
it('applies active tab styling correctly', () => {
renderAlertCondition();
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
const anomalyTab = screen.getByText(ANOMALY_TAB_TEXT);
// Threshold tab should be active by default
expect(thresholdTab.closest(ACTIVE_TAB_CLASS)).toBeInTheDocument();
expect(anomalyTab.closest(ACTIVE_TAB_CLASS)).not.toBeInTheDocument();
// Click anomaly tab
fireEvent.click(anomalyTab);
// Anomaly tab should be active now
expect(anomalyTab.closest(ACTIVE_TAB_CLASS)).toBeInTheDocument();
expect(thresholdTab.closest(ACTIVE_TAB_CLASS)).not.toBeInTheDocument();
});
it('shows multiple tabs for METRICS_BASED_ALERT', () => {
renderAlertCondition('METRIC_BASED_ALERT');
// Both tabs should be visible
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
});
it('shows multiple tabs for ANOMALY_BASED_ALERT', () => {
renderAlertCondition('ANOMALY_BASED_ALERT');
// Both tabs should be visible
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
});
it('shows only threshold tab for LOGS_BASED_ALERT', () => {
renderAlertCondition('LOGS_BASED_ALERT');
// Only threshold tab should be visible
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.queryByText(ANOMALY_TAB_TEXT)).not.toBeInTheDocument();
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
expect(screen.queryByTestId(ANOMALY_VIEW_TEST_ID)).not.toBeInTheDocument();
});
it('shows only threshold tab for TRACES_BASED_ALERT', () => {
renderAlertCondition('TRACES_BASED_ALERT');
// Only threshold tab should be visible
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.queryByText(ANOMALY_TAB_TEXT)).not.toBeInTheDocument();
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
expect(screen.queryByTestId(ANOMALY_VIEW_TEST_ID)).not.toBeInTheDocument();
});
it('shows only threshold tab for EXCEPTIONS_BASED_ALERT', () => {
renderAlertCondition('EXCEPTIONS_BASED_ALERT');
// Only threshold tab should be visible
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.queryByText(ANOMALY_TAB_TEXT)).not.toBeInTheDocument();
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
expect(screen.queryByTestId(ANOMALY_VIEW_TEST_ID)).not.toBeInTheDocument();
});
});

View File

@ -0,0 +1,272 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/jsx-props-no-spreading */
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import { Channels } from 'types/api/channels/getAll';
import { CreateAlertProvider } from '../../context';
import AlertThreshold from '../AlertThreshold';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock: any = jest.fn(() => ({
paths,
}));
uplotMock.paths = paths;
return uplotMock;
});
// Mock the ThresholdItem component
jest.mock('../ThresholdItem', () => ({
__esModule: true,
default: function MockThresholdItem({
threshold,
removeThreshold,
showRemoveButton,
}: {
threshold: Record<string, unknown>;
removeThreshold: (id: string) => void;
showRemoveButton: boolean;
}): JSX.Element {
return (
<div data-testid={`threshold-item-${threshold.id}`}>
<span>{threshold.label as string}</span>
{showRemoveButton && (
<button
type="button"
data-testid={`remove-threshold-${threshold.id}`}
onClick={(): void => removeThreshold(threshold.id as string)}
>
Remove
</button>
)}
</div>
);
},
}));
// Mock useQueryBuilder hook
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): {
currentQuery: {
dataSource: string;
queryName: string;
builder: {
queryData: Array<{ queryName: string }>;
queryFormulas: Array<{ queryName: string }>;
};
unit: string;
};
} => ({
currentQuery: {
dataSource: 'METRICS',
queryName: 'A',
builder: {
queryData: [{ queryName: 'Query A' }, { queryName: 'Query B' }],
queryFormulas: [{ queryName: 'Formula 1' }],
},
unit: 'bytes',
},
}),
}));
// Mock getAllChannels API
jest.mock('api/channels/getAll', () => ({
__esModule: true,
default: jest.fn(() =>
Promise.resolve({
data: [
{ id: '1', name: 'Email Channel' },
{ id: '2', name: 'Slack Channel' },
] as Channels[],
}),
),
}));
// Mock alert format categories
jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
getCategoryByOptionId: jest.fn(() => ({ name: 'bytes' })),
getCategorySelectOptionByName: jest.fn(() => [
{ label: 'Bytes', value: 'bytes' },
{ label: 'KB', value: 'kb' },
]),
}));
const TEST_STRINGS = {
ADD_THRESHOLD: 'Add Threshold',
AT_LEAST_ONCE: 'AT LEAST ONCE',
IS_ABOVE: 'IS ABOVE',
} as const;
const createTestQueryClient = (): QueryClient =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const renderAlertThreshold = (): ReturnType<typeof render> => {
const queryClient = createTestQueryClient();
return render(
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<CreateAlertProvider>
<AlertThreshold />
</CreateAlertProvider>
</QueryClientProvider>
</MemoryRouter>,
);
};
const verifySelectRenders = (title: string): void => {
const select = screen.getByTitle(title);
expect(select).toBeInTheDocument();
};
describe('AlertThreshold', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders the main condition sentence', () => {
renderAlertThreshold();
expect(screen.getByText('Send a notification when')).toBeInTheDocument();
expect(screen.getByText('the threshold(s)')).toBeInTheDocument();
expect(screen.getByText('during the')).toBeInTheDocument();
expect(screen.getByText('Evaluation Window.')).toBeInTheDocument();
});
it('renders query selection dropdown', async () => {
renderAlertThreshold();
await waitFor(() => {
const querySelect = screen.getByTitle('A');
expect(querySelect).toBeInTheDocument();
});
});
it('renders operator selection dropdown', () => {
renderAlertThreshold();
verifySelectRenders(TEST_STRINGS.IS_ABOVE);
});
it('renders match type selection dropdown', () => {
renderAlertThreshold();
verifySelectRenders(TEST_STRINGS.AT_LEAST_ONCE);
});
it('renders threshold items', () => {
renderAlertThreshold();
expect(screen.getByTestId(/threshold-item-/)).toBeInTheDocument();
});
it('renders add threshold button', () => {
renderAlertThreshold();
expect(screen.getByText(TEST_STRINGS.ADD_THRESHOLD)).toBeInTheDocument();
});
it('adds a new threshold when add button is clicked', () => {
renderAlertThreshold();
const addButton = screen.getByText(TEST_STRINGS.ADD_THRESHOLD);
fireEvent.click(addButton);
// Should now have multiple threshold items
const thresholdItems = screen.getAllByTestId(/threshold-item-/);
expect(thresholdItems).toHaveLength(2);
});
it('adds correct threshold types based on count', () => {
renderAlertThreshold();
const addButton = screen.getByText(TEST_STRINGS.ADD_THRESHOLD);
// First addition should add WARNING threshold
fireEvent.click(addButton);
expect(screen.getByText('WARNING')).toBeInTheDocument();
// Second addition should add INFO threshold
fireEvent.click(addButton);
expect(screen.getByText('INFO')).toBeInTheDocument();
// Third addition should add random threshold
fireEvent.click(addButton);
expect(screen.getAllByTestId(/threshold-item-/)).toHaveLength(4);
});
it('updates operator when operator dropdown changes', () => {
renderAlertThreshold();
verifySelectRenders(TEST_STRINGS.IS_ABOVE);
});
it('updates match type when match type dropdown changes', () => {
renderAlertThreshold();
verifySelectRenders(TEST_STRINGS.AT_LEAST_ONCE);
});
it('shows remove button for non-first thresholds', () => {
renderAlertThreshold();
// Add a threshold
const addButton = screen.getByText(TEST_STRINGS.ADD_THRESHOLD);
fireEvent.click(addButton);
// Second threshold should have remove button
expect(screen.getByTestId(/remove-threshold-/)).toBeInTheDocument();
});
it('does not show remove button for first threshold', () => {
renderAlertThreshold();
// First threshold should not have remove button
expect(screen.queryByTestId(/remove-threshold-/)).not.toBeInTheDocument();
});
it('removes threshold when remove button is clicked', () => {
renderAlertThreshold();
// Add a threshold first
const addButton = screen.getByText(TEST_STRINGS.ADD_THRESHOLD);
fireEvent.click(addButton);
// Get the remove button and click it
const removeButton = screen.getByTestId(/remove-threshold-/);
fireEvent.click(removeButton);
// Should be back to one threshold
expect(screen.getAllByTestId(/threshold-item-/)).toHaveLength(1);
});
it('does not remove threshold if only one remains', () => {
renderAlertThreshold();
// Should only have one threshold initially
expect(screen.getAllByTestId(/threshold-item-/)).toHaveLength(1);
// Try to remove (should not work)
const thresholdItems = screen.getAllByTestId(/threshold-item-/);
expect(thresholdItems).toHaveLength(1);
});
it('handles loading state for channels', () => {
renderAlertThreshold();
// Component should render even while channels are loading
expect(screen.getByText('Send a notification when')).toBeInTheDocument();
});
it('renders with correct initial state', () => {
renderAlertThreshold();
// Should have initial critical threshold
expect(screen.getByText('CRITICAL')).toBeInTheDocument();
verifySelectRenders(TEST_STRINGS.IS_ABOVE);
verifySelectRenders(TEST_STRINGS.AT_LEAST_ONCE);
});
});

View File

@ -0,0 +1,89 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { render, screen } from '@testing-library/react';
import {
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
} from 'container/CreateAlertV2/context/constants';
import * as context from '../../context';
import AnomalyThreshold from '../AnomalyThreshold';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock: any = jest.fn(() => ({
paths,
}));
uplotMock.paths = paths;
return uplotMock;
});
const mockSetAlertState = jest.fn();
const mockSetThresholdState = jest.fn();
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
alertState: INITIAL_ALERT_STATE,
setAlertState: mockSetAlertState,
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
setThresholdState: mockSetThresholdState,
} as any);
// Mock useQueryBuilder hook
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): {
currentQuery: {
dataSource: string;
queryName: string;
builder: {
queryData: Array<{ queryName: string }>;
queryFormulas: Array<{ queryName: string }>;
};
};
} => ({
currentQuery: {
dataSource: 'METRICS',
queryName: 'A',
builder: {
queryData: [{ queryName: 'Query A' }, { queryName: 'Query B' }],
queryFormulas: [{ queryName: 'Formula 1' }],
},
},
}),
}));
const renderAnomalyThreshold = (): ReturnType<typeof render> =>
render(<AnomalyThreshold />);
describe('AnomalyThreshold', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders the first condition sentence', () => {
renderAnomalyThreshold();
expect(screen.getByTestId('notification-text')).toBeInTheDocument();
expect(screen.getByTestId('evaluation-window-text')).toBeInTheDocument();
expect(screen.getByTestId('evaluation-window-select')).toBeInTheDocument();
});
it('renders the second condition sentence', () => {
renderAnomalyThreshold();
expect(screen.getByTestId('threshold-text')).toBeInTheDocument();
expect(screen.getByTestId('threshold-value-select')).toBeInTheDocument();
expect(screen.getByTestId('deviations-text')).toBeInTheDocument();
expect(screen.getByTestId('operator-select')).toBeInTheDocument();
expect(screen.getByTestId('predicted-data-text')).toBeInTheDocument();
expect(screen.getByTestId('match-type-select')).toBeInTheDocument();
});
it('renders the third condition sentence', () => {
renderAnomalyThreshold();
expect(screen.getByTestId('using-the-text')).toBeInTheDocument();
expect(screen.getByTestId('algorithm-select')).toBeInTheDocument();
expect(screen.getByTestId('algorithm-with-text')).toBeInTheDocument();
expect(screen.getByTestId('seasonality-select')).toBeInTheDocument();
expect(screen.getByTestId('seasonality-text')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,398 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/jsx-props-no-spreading */
import { fireEvent, render, screen } from '@testing-library/react';
import { DefaultOptionType } from 'antd/es/select';
import { Channels } from 'types/api/channels/getAll';
import ThresholdItem from '../ThresholdItem';
import { ThresholdItemProps } from '../types';
// Mock the enableRecoveryThreshold utility
jest.mock('../../utils', () => ({
enableRecoveryThreshold: jest.fn(() => true),
}));
const TEST_CONSTANTS = {
THRESHOLD_ID: 'test-threshold-1',
CRITICAL_LABEL: 'CRITICAL',
WARNING_LABEL: 'WARNING',
INFO_LABEL: 'INFO',
CHANNEL_1: 'channel-1',
CHANNEL_2: 'channel-2',
CHANNEL_3: 'channel-3',
EMAIL_CHANNEL_NAME: 'Email Channel',
ENTER_THRESHOLD_NAME: 'Enter threshold name',
ENTER_THRESHOLD_VALUE: 'Enter threshold value',
ENTER_RECOVERY_THRESHOLD_VALUE: 'Enter recovery threshold value',
} as const;
const mockThreshold = {
id: TEST_CONSTANTS.THRESHOLD_ID,
label: TEST_CONSTANTS.CRITICAL_LABEL,
thresholdValue: 100,
recoveryThresholdValue: 80,
unit: 'bytes',
channels: [TEST_CONSTANTS.CHANNEL_1],
color: '#ff0000',
};
const mockChannels: Channels[] = [
{
id: TEST_CONSTANTS.CHANNEL_1,
name: TEST_CONSTANTS.EMAIL_CHANNEL_NAME,
} as any,
{ id: TEST_CONSTANTS.CHANNEL_2, name: 'Slack Channel' } as any,
{ id: TEST_CONSTANTS.CHANNEL_3, name: 'PagerDuty Channel' } as any,
];
const mockUnits: DefaultOptionType[] = [
{ label: 'Bytes', value: 'bytes' },
{ label: 'KB', value: 'kb' },
{ label: 'MB', value: 'mb' },
];
const defaultProps: ThresholdItemProps = {
threshold: mockThreshold,
updateThreshold: jest.fn(),
removeThreshold: jest.fn(),
showRemoveButton: false,
channels: mockChannels,
isLoadingChannels: false,
units: mockUnits,
};
const renderThresholdItem = (
props: Partial<ThresholdItemProps> = {},
): ReturnType<typeof render> => {
const mergedProps = { ...defaultProps, ...props };
return render(<ThresholdItem {...mergedProps} />);
};
const verifySelectorWidth = (
selectorIndex: number,
expectedWidth: string,
): void => {
const selectors = screen.getAllByRole('combobox');
const selector = selectors[selectorIndex];
expect(selector.closest('.ant-select')).toHaveStyle(`width: ${expectedWidth}`);
};
const showRecoveryThreshold = (): void => {
const recoveryButton = screen.getByRole('button', { name: '' });
fireEvent.click(recoveryButton);
};
const verifyComponentRendersWithLoading = (): void => {
expect(
screen.getByPlaceholderText(TEST_CONSTANTS.ENTER_THRESHOLD_NAME),
).toBeInTheDocument();
};
const verifyUnitSelectorDisabled = (): void => {
const unitSelectors = screen.getAllByRole('combobox');
const unitSelector = unitSelectors[0]; // First combobox is the unit selector
expect(unitSelector).toBeDisabled();
};
describe('ThresholdItem', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders threshold indicator with correct color', () => {
renderThresholdItem();
// Find the threshold dot by its class
const thresholdDot = document.querySelector('.threshold-dot');
expect(thresholdDot).toHaveStyle('background-color: #ff0000');
});
it('renders threshold label input with correct value', () => {
renderThresholdItem();
const labelInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_THRESHOLD_NAME,
);
expect(labelInput).toHaveValue(TEST_CONSTANTS.CRITICAL_LABEL);
});
it('renders threshold value input with correct value', () => {
renderThresholdItem();
const valueInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
);
expect(valueInput).toHaveValue('100');
});
it('renders unit selector with correct value', () => {
renderThresholdItem();
// Check for the unit selector by looking for the displayed text
expect(screen.getByText('Bytes')).toBeInTheDocument();
});
it('renders channels selector with correct value', () => {
renderThresholdItem();
// Check for the channels selector by looking for the displayed text
expect(
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
).toBeInTheDocument();
});
it('updates threshold label when label input changes', () => {
const updateThreshold = jest.fn();
renderThresholdItem({ updateThreshold });
const labelInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_THRESHOLD_NAME,
);
fireEvent.change(labelInput, {
target: { value: TEST_CONSTANTS.WARNING_LABEL },
});
expect(updateThreshold).toHaveBeenCalledWith(
TEST_CONSTANTS.THRESHOLD_ID,
'label',
TEST_CONSTANTS.WARNING_LABEL,
);
});
it('updates threshold value when value input changes', () => {
const updateThreshold = jest.fn();
renderThresholdItem({ updateThreshold });
const valueInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
);
fireEvent.change(valueInput, { target: { value: '200' } });
expect(updateThreshold).toHaveBeenCalledWith(
TEST_CONSTANTS.THRESHOLD_ID,
'thresholdValue',
'200',
);
});
it('updates threshold unit when unit selector changes', () => {
const updateThreshold = jest.fn();
renderThresholdItem({ updateThreshold });
// Find the unit selector by its role and simulate change
const unitSelectors = screen.getAllByRole('combobox');
const unitSelector = unitSelectors[0]; // First combobox is the unit selector
// Simulate clicking to open the dropdown and selecting a value
fireEvent.click(unitSelector);
// The actual change event might not work the same way with Ant Design Select
// So we'll just verify the selector is present and can be interacted with
expect(unitSelector).toBeInTheDocument();
});
it('updates threshold channels when channels selector changes', () => {
const updateThreshold = jest.fn();
renderThresholdItem({ updateThreshold });
// Find the channels selector by its role and simulate change
const channelSelectors = screen.getAllByRole('combobox');
const channelSelector = channelSelectors[1]; // Second combobox is the channels selector
// Simulate clicking to open the dropdown
fireEvent.click(channelSelector);
// The actual change event might not work the same way with Ant Design Select
// So we'll just verify the selector is present and can be interacted with
expect(channelSelector).toBeInTheDocument();
});
it('shows remove button when showRemoveButton is true', () => {
renderThresholdItem({ showRemoveButton: true });
// The remove button is the second button (with circle-x icon)
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(2); // Recovery button + remove button
});
it('does not show remove button when showRemoveButton is false', () => {
renderThresholdItem({ showRemoveButton: false });
// Only the recovery button should be present
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(1); // Only recovery button
});
it('calls removeThreshold when remove button is clicked', () => {
const removeThreshold = jest.fn();
renderThresholdItem({ showRemoveButton: true, removeThreshold });
// The remove button is the second button (with circle-x icon)
const buttons = screen.getAllByRole('button');
const removeButton = buttons[1]; // Second button is the remove button
fireEvent.click(removeButton);
expect(removeThreshold).toHaveBeenCalledWith(TEST_CONSTANTS.THRESHOLD_ID);
});
it('shows recovery threshold button when recovery threshold is enabled', () => {
renderThresholdItem();
// The recovery button is the first button (with chart-line icon)
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(1); // Recovery button
});
it('shows recovery threshold inputs when recovery button is clicked', () => {
renderThresholdItem();
// The recovery button is the first button (with chart-line icon)
const buttons = screen.getAllByRole('button');
const recoveryButton = buttons[0]; // First button is the recovery button
fireEvent.click(recoveryButton);
expect(screen.getByPlaceholderText('Recovery threshold')).toBeInTheDocument();
expect(
screen.getByPlaceholderText(TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE),
).toBeInTheDocument();
});
it('updates recovery threshold value when input changes', () => {
const updateThreshold = jest.fn();
renderThresholdItem({ updateThreshold });
// Show recovery threshold first
const buttons = screen.getAllByRole('button');
const recoveryButton = buttons[0]; // First button is the recovery button
fireEvent.click(recoveryButton);
const recoveryValueInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE,
);
fireEvent.change(recoveryValueInput, { target: { value: '90' } });
expect(updateThreshold).toHaveBeenCalledWith(
TEST_CONSTANTS.THRESHOLD_ID,
'recoveryThresholdValue',
'90',
);
});
it('disables unit selector when no units are available', () => {
renderThresholdItem({ units: [] });
verifyUnitSelectorDisabled();
});
it('shows tooltip when no units are available', () => {
renderThresholdItem({ units: [] });
// The tooltip should be present when hovering over disabled unit selector
verifyUnitSelectorDisabled();
});
it('renders channels as multiple select options', () => {
renderThresholdItem();
// Check that channels are rendered as multiple select
expect(
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
).toBeInTheDocument();
// Should be able to select multiple channels
const channelSelectors = screen.getAllByRole('combobox');
const channelSelector = channelSelectors[1]; // Second combobox is the channels selector
fireEvent.change(channelSelector, {
target: { value: [TEST_CONSTANTS.CHANNEL_1, TEST_CONSTANTS.CHANNEL_2] },
});
});
it('handles empty threshold values correctly', () => {
const emptyThreshold = {
...mockThreshold,
label: '',
thresholdValue: 0,
unit: '',
channels: [],
};
renderThresholdItem({ threshold: emptyThreshold });
expect(screen.getByPlaceholderText('Enter threshold name')).toHaveValue('');
expect(screen.getByPlaceholderText('Enter threshold value')).toHaveValue('0');
});
it('renders with correct input widths', () => {
renderThresholdItem();
const labelInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_THRESHOLD_NAME,
);
const valueInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
);
expect(labelInput).toHaveStyle('width: 260px');
expect(valueInput).toHaveStyle('width: 210px');
});
it('renders channels selector with correct width', () => {
renderThresholdItem();
verifySelectorWidth(1, '260px');
});
it('renders unit selector with correct width', () => {
renderThresholdItem();
verifySelectorWidth(0, '150px');
});
it('handles loading channels state', () => {
renderThresholdItem({ isLoadingChannels: true });
verifyComponentRendersWithLoading();
});
it('renders recovery threshold with correct initial value', () => {
renderThresholdItem();
showRecoveryThreshold();
const recoveryValueInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE,
);
expect(recoveryValueInput).toHaveValue('80');
});
it('renders recovery threshold label as disabled', () => {
renderThresholdItem();
showRecoveryThreshold();
const recoveryLabelInput = screen.getByPlaceholderText('Recovery threshold');
expect(recoveryLabelInput).toBeDisabled();
});
it('renders correct channel options', () => {
renderThresholdItem();
// Check that channels are rendered
expect(
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
).toBeInTheDocument();
// Should be able to select different channels
const channelSelectors = screen.getAllByRole('combobox');
const channelSelector = channelSelectors[1]; // Second combobox is the channels selector
fireEvent.change(channelSelector, { target: { value: 'channel-2' } });
expect(screen.getByText('Slack Channel')).toBeInTheDocument();
});
it('handles threshold without channels', () => {
const thresholdWithoutChannels = {
...mockThreshold,
channels: [],
};
renderThresholdItem({ threshold: thresholdWithoutChannels });
// Should render channels selector without selected values
const channelSelectors = screen.getAllByRole('combobox');
expect(channelSelectors).toHaveLength(2); // Should have both unit and channel selectors
});
});

View File

@ -0,0 +1,5 @@
export const THRESHOLD_TAB_TOOLTIP =
'An alert is triggered when the metric crosses a threshold.';
export const ANOMALY_TAB_TOOLTIP =
'An alert is triggered whenever the metric deviates from an expected pattern.';

View File

@ -0,0 +1,3 @@
import AlertCondition from './AlertCondition';
export default AlertCondition;

View File

@ -0,0 +1,277 @@
.alert-condition-container {
margin: 0 16px;
margin-top: 24px;
.alert-condition {
display: flex;
align-items: center;
margin-left: 12px;
margin-top: 24px;
.alert-condition-tabs {
display: flex;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
flex-direction: row;
border-bottom: none;
margin-bottom: -1px;
.explorer-view-option {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
border: none;
padding: 9px;
box-shadow: none;
border-radius: 0px;
border-left: 0.5px solid var(--bg-slate-400);
border-bottom: 0.5px solid var(--bg-slate-400);
width: 120px;
height: 36px;
gap: 8px;
&.active-tab {
background-color: var(--bg-ink-500);
border-bottom: none;
&:hover {
background-color: var(--bg-ink-500) !important;
}
}
&:disabled {
background-color: var(--bg-ink-300);
opacity: 0.6;
}
&:first-child {
border-left: 1px solid transparent;
}
&:hover {
background-color: transparent !important;
border-left: 1px solid transparent !important;
color: var(--bg-vanilla-100);
}
}
}
}
}
.alert-threshold-container,
.anomaly-threshold-container {
padding: 24px;
padding-right: 72px;
background-color: var(--bg-ink-500);
border: 1px solid var(--bg-slate-400);
width: fit-content;
.alert-condition-sentences {
display: flex;
flex-direction: column;
gap: 12px;
.alert-condition-sentence {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
.sentence-text {
color: var(--text-vanilla-400);
font-size: 14px;
line-height: 1.5;
}
.ant-select {
width: 240px !important;
.ant-select-selector {
background-color: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
color: var(--text-vanilla-400);
font-family: 'Space Mono';
&:hover {
border-color: var(--bg-vanilla-300);
}
&:focus {
border-color: var(--bg-vanilla-300);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
.ant-select-selection-item {
color: var(--bg-vanilla-100);
}
.ant-select-arrow {
color: var(--bg-vanilla-400);
}
}
}
}
.thresholds-section {
margin-top: 16px;
margin-left: 24px;
.threshold-item {
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: 16px;
.threshold-row {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 2px;
.threshold-indicator {
.threshold-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
}
.threshold-controls {
display: flex;
align-items: center;
gap: 8px;
.ant-input {
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-100);
height: 32px;
&::placeholder {
font-family: 'Space Mono';
}
&:hover {
border-color: var(--bg-vanilla-300);
}
&:focus {
border-color: var(--bg-vanilla-300);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
.ant-select {
.ant-select-selector {
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-100);
height: 32px;
&:hover {
border-color: var(--bg-vanilla-300);
}
&:focus {
border-color: var(--bg-vanilla-300);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
.ant-select-selection-placeholder {
font-family: 'Space Mono';
}
}
.ant-select-selection-item {
color: var(--bg-vanilla-100);
}
.ant-select-arrow {
color: var(--bg-vanilla-400);
}
}
.icon-btn {
color: var(--bg-vanilla-400);
border: 1px solid var(--bg-slate-400);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.recovery-threshold-input-group {
display: flex;
align-items: center;
gap: 0;
margin-left: 28px;
.recovery-threshold-label {
pointer-events: none;
cursor: default;
}
.recovery-threshold-btn {
pointer-events: none;
cursor: default;
color: var(--bg-vanilla-400);
background-color: var(--bg-ink-400) !important;
border: 1px solid var(--bg-slate-400);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.ant-input {
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-100);
height: 32px;
&::placeholder {
font-family: 'Space Mono';
}
&:hover {
border-color: var(--bg-vanilla-300);
}
&:focus {
border-color: var(--bg-vanilla-300);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
}
}
.add-threshold-btn {
margin-top: 8px;
border: 1px dashed var(--bg-slate-400);
color: var(--bg-vanilla-300);
background-color: transparent;
border-radius: 4px;
height: 32px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
border-color: var(--bg-vanilla-300);
color: var(--bg-vanilla-100);
}
.anticon {
margin-right: 8px;
}
}
}
}

View File

@ -0,0 +1,23 @@
import { DefaultOptionType } from 'antd/es/select';
import { Channels } from 'types/api/channels/getAll';
import { Threshold } from '../context/types';
export type UpdateThreshold = {
(thresholdId: string, field: 'channels', value: string[]): void;
(
thresholdId: string,
field: Exclude<keyof Threshold, 'channels'>,
value: string,
): void;
};
export interface ThresholdItemProps {
threshold: Threshold;
updateThreshold: UpdateThreshold;
removeThreshold: (thresholdId: string) => void;
showRemoveButton: boolean;
channels: Channels[];
isLoadingChannels: boolean;
units: DefaultOptionType[];
}

View File

@ -0,0 +1,46 @@
import { BaseOptionType, DefaultOptionType, SelectProps } from 'antd/es/select';
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
import { getSelectedQueryOptions } from 'container/FormAlertRules/utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
export function getQueryNames(currentQuery: Query): BaseOptionType[] {
const involvedQueriesInTraceOperator = getInvolvedQueriesInTraceOperator(
currentQuery.builder.queryTraceOperator,
);
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
[EQueryType.QUERY_BUILDER]: () => [
...(getSelectedQueryOptions(currentQuery.builder.queryData)?.filter(
(option) =>
!involvedQueriesInTraceOperator.includes(option.value as string),
) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryTraceOperator) || []),
],
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
[EQueryType.CLICKHOUSE]: () =>
getSelectedQueryOptions(currentQuery.clickhouse_sql),
};
return queryConfig[currentQuery.queryType]?.() || [];
}
export function getCategoryByOptionId(id: string): string | undefined {
return Y_AXIS_CATEGORIES.find((category) =>
category.units.some((unit) => unit.id === id),
)?.name;
}
export function getCategorySelectOptionByName(
name: string,
): DefaultOptionType[] {
return (
Y_AXIS_CATEGORIES.find((category) => category.name === name)?.units.map(
(unit) => ({
label: unit.name,
value: unit.id,
}),
) || []
);
}

View File

@ -1,5 +1,7 @@
import './styles.scss';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useCallback, useMemo } from 'react';
import { Labels } from 'types/api/alerts/def';
import { useCreateAlertState } from '../context';
@ -8,6 +10,29 @@ import LabelsInput from './LabelsInput';
function CreateAlertHeader(): JSX.Element {
const { alertState, setAlertState } = useCreateAlertState();
const { currentQuery } = useQueryBuilder();
const groupByLabels = useMemo(() => {
const labels = new Array<string>();
currentQuery.builder.queryData.forEach((query) => {
query.groupBy.forEach((groupBy) => {
labels.push(groupBy.key);
});
});
return labels;
}, [currentQuery]);
// If the label key is a group by label, then it is not allowed to be used as a label key
const validateLabelsKey = useCallback(
(key: string): string | null => {
if (groupByLabels.includes(key)) {
return `Cannot use ${key} as a key`;
}
return null;
},
[groupByLabels],
);
return (
<div className="alert-header">
<div className="alert-header__tab-bar">
@ -38,6 +63,7 @@ function CreateAlertHeader(): JSX.Element {
onLabelsChange={(labels: Labels): void =>
setAlertState({ type: 'SET_ALERT_LABELS', payload: labels })
}
validateLabelsKey={validateLabelsKey}
/>
</div>
</div>

View File

@ -7,6 +7,7 @@ import { LabelInputState, LabelsInputProps } from './types';
function LabelsInput({
labels,
onLabelsChange,
validateLabelsKey,
}: LabelsInputProps): JSX.Element {
const { notifications } = useNotifications();
const [inputState, setInputState] = useState<LabelInputState>({
@ -38,6 +39,13 @@ function LabelsInput({
});
return;
}
const error = validateLabelsKey(key.trim());
if (error) {
notifications.error({
message: error,
});
return;
}
// Add the label immediately
const newLabels = {
...labels,
@ -55,6 +63,13 @@ function LabelsInput({
});
return;
}
const error = validateLabelsKey(inputState.key.trim());
if (error) {
notifications.error({
message: error,
});
return;
}
setInputState((prev) => ({ ...prev, isKeyInput: false }));
}
} else if (inputState.value.trim()) {
@ -74,7 +89,7 @@ function LabelsInput({
setInputState({ key: '', value: '', isKeyInput: true });
}
},
[inputState, labels, notifications, onLabelsChange],
[inputState, labels, notifications, onLabelsChange, validateLabelsKey],
);
const handleInputChange = useCallback(

View File

@ -10,10 +10,12 @@ jest.mock('@ant-design/icons', () => ({
}));
const mockOnLabelsChange = jest.fn();
const mockValidateLabelsKey = jest.fn().mockReturnValue(null);
const defaultProps: LabelsInputProps = {
labels: {},
onLabelsChange: mockOnLabelsChange,
validateLabelsKey: mockValidateLabelsKey,
};
const ADD_LABELS_TEXT = '+ Add labels';
@ -33,6 +35,7 @@ const renderLabelsInput = (
describe('LabelsInput', () => {
beforeEach(() => {
jest.clearAllMocks();
mockValidateLabelsKey.mockReturnValue(null); // Reset validation to always pass
});
describe('Initial Rendering', () => {
@ -483,7 +486,11 @@ describe('LabelsInput', () => {
// Simulate parent component updating labels
const firstLabels = { severity: 'high' };
rerender(
<LabelsInput labels={firstLabels} onLabelsChange={mockOnLabelsChange} />,
<LabelsInput
labels={firstLabels}
onLabelsChange={mockOnLabelsChange}
validateLabelsKey={mockValidateLabelsKey}
/>,
);
// Add second label

View File

@ -3,6 +3,7 @@ import { Labels } from 'types/api/alerts/def';
export interface LabelsInputProps {
labels: Labels;
onLabelsChange: (labels: Labels) => void;
validateLabelsKey: (key: string) => string | null;
}
export interface LabelInputState {

View File

@ -4,6 +4,7 @@ import { initialQueriesMap } from 'constants/queryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import AlertCondition from './AlertCondition';
import { CreateAlertProvider } from './context';
import CreateAlertHeader from './CreateAlertHeader';
import QuerySection from './QuerySection';
@ -20,6 +21,7 @@ function CreateAlertV2({
<CreateAlertProvider>
<CreateAlertHeader />
<QuerySection />
<AlertCondition />
</CreateAlertProvider>
</div>
);

View File

@ -6,19 +6,24 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertDef } from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
function ChartPreview(): JSX.Element {
export interface ChartPreviewProps {
alertDef: AlertDef;
}
function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
const { currentQuery, panelType, stagedQuery } = useQueryBuilder();
const { thresholdState, alertState } = useCreateAlertState();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [, setQueryStatus] = useState<string>('');
const { alertDef } = useCreateAlertState();
const yAxisUnit = currentQuery.unit || '';
const yAxisUnit = alertState.yAxisUnit || '';
const renderQBChartPreview = (): JSX.Element => (
<ChartPreviewComponent
@ -36,6 +41,7 @@ function ChartPreview(): JSX.Element {
graphType={panelType || PANEL_TYPES.TIME_SERIES}
setQueryStatus={setQueryStatus}
showSideLegend
additionalThresholds={thresholdState.thresholds}
/>
);
@ -55,6 +61,7 @@ function ChartPreview(): JSX.Element {
graphType={panelType || PANEL_TYPES.TIME_SERIES}
setQueryStatus={setQueryStatus}
showSideLegend
additionalThresholds={thresholdState.thresholds}
/>
);

View File

@ -2,6 +2,7 @@ import './styles.scss';
import { Button } from 'antd';
import classNames from 'classnames';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { PANEL_TYPES } from 'constants/queryBuilder';
import QuerySectionComponent from 'container/FormAlertRules/QuerySection';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@ -11,10 +12,19 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { useCreateAlertState } from '../context';
import Stepper from '../Stepper';
import ChartPreview from './ChartPreview';
import { buildAlertDefForChartPreview } from './utils';
function QuerySection(): JSX.Element {
const { currentQuery, handleRunQuery } = useQueryBuilder();
const { alertType, setAlertType, alertDef } = useCreateAlertState();
const {
alertState,
setAlertState,
alertType,
setAlertType,
thresholdState,
} = useCreateAlertState();
const alertDef = buildAlertDefForChartPreview({ alertType, thresholdState });
const tabs = [
{
@ -45,7 +55,13 @@ function QuerySection(): JSX.Element {
stepNumber={1}
label="Define the query you want to set an alert on"
/>
<ChartPreview />
<ChartPreview alertDef={alertDef} />
<YAxisUnitSelector
value={alertState.yAxisUnit}
onChange={(value): void => {
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
}}
/>
<div className="query-section-tabs">
<div className="query-section-query-actions">
{tabs.map((tab) => (

View File

@ -2,16 +2,21 @@
/* eslint-disable react/destructuring-assignment */
import { render, screen } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
} from 'container/CreateAlertV2/context/constants';
import { buildInitialAlertDef } from 'container/CreateAlertV2/context/utils';
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 { CreateAlertProvider } from '../../context';
import ChartPreview from '../ChartPreview/ChartPreview';
// Constants for duplicate strings
const REQUESTS_PER_SEC = 'requests/sec';
const CHART_PREVIEW_NAME = 'Chart Preview';
const QUERY_TYPE_TEST_ID = 'query-type';
@ -109,13 +114,28 @@ const mockUseQueryBuilder = {
},
};
const mockAlertDef = buildInitialAlertDef(AlertTypes.METRICS_BASED_ALERT);
jest.mock('../../context', () => ({
...jest.requireActual('../../context'),
useCreateAlertState: (): any => ({
alertState: {
...INITIAL_ALERT_STATE,
yAxisUnit: REQUESTS_PER_SEC,
},
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
setAlertState: jest.fn(),
setThresholdState: jest.fn(),
}),
}));
const renderChartPreview = (): ReturnType<typeof render> =>
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<CreateAlertProvider>
<ChartPreview />
<ChartPreview alertDef={mockAlertDef} />
</CreateAlertProvider>
</MemoryRouter>
</QueryClientProvider>

View File

@ -70,6 +70,17 @@
}
}
.y-axis-unit-selector-component {
margin-top: 16px;
.ant-select {
.ant-select-selector {
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
}
.chart-preview-container {
margin-right: 4px;
.alert-chart-container {

View File

@ -0,0 +1,28 @@
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
import { AlertThresholdState } from '../context/types';
import { buildInitialAlertDef } from '../context/utils';
export function buildAlertDefForChartPreview({
alertType,
thresholdState,
}: {
alertType: AlertTypes;
thresholdState: AlertThresholdState;
}): AlertDef {
const initialAlertDef = buildInitialAlertDef(alertType);
return {
...initialAlertDef,
ruleType:
alertType === AlertTypes.ANOMALY_BASED_ALERT
? AlertDetectionTypes.ANOMALY_DETECTION_ALERT
: AlertDetectionTypes.THRESHOLD_ALERT,
condition: {
...initialAlertDef.condition,
targetUnit: thresholdState.thresholds?.[0].unit,
},
};
}

View File

@ -1,7 +1,117 @@
import { AlertState } from './types';
import { Color } from '@signozhq/design-tokens';
import getRandomColor from 'lib/getRandomColor';
import { v4 } from 'uuid';
import {
AlertState,
AlertThresholdMatchType,
AlertThresholdOperator,
AlertThresholdState,
Algorithm,
Seasonality,
Threshold,
TimeDuration,
} from './types';
export const INITIAL_ALERT_STATE: AlertState = {
name: '',
description: '',
labels: {},
yAxisUnit: undefined,
};
export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
id: v4(),
label: 'CRITICAL',
thresholdValue: 0,
recoveryThresholdValue: 0,
unit: '',
channels: [],
color: Color.BG_SAKURA_500,
};
export const INITIAL_WARNING_THRESHOLD: Threshold = {
id: v4(),
label: 'WARNING',
thresholdValue: 0,
recoveryThresholdValue: 0,
unit: '',
channels: [],
color: Color.BG_AMBER_500,
};
export const INITIAL_INFO_THRESHOLD: Threshold = {
id: v4(),
label: 'INFO',
thresholdValue: 0,
recoveryThresholdValue: 0,
unit: '',
channels: [],
color: Color.BG_ROBIN_500,
};
export const INITIAL_RANDOM_THRESHOLD: Threshold = {
id: v4(),
label: '',
thresholdValue: 0,
recoveryThresholdValue: 0,
unit: '',
channels: [],
color: getRandomColor(),
};
export const INITIAL_ALERT_THRESHOLD_STATE: AlertThresholdState = {
selectedQuery: 'A',
operator: AlertThresholdOperator.IS_ABOVE,
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
evaluationWindow: TimeDuration.FIVE_MINUTES,
algorithm: Algorithm.STANDARD,
seasonality: Seasonality.HOURLY,
thresholds: [INITIAL_CRITICAL_THRESHOLD],
};
export const THRESHOLD_OPERATOR_OPTIONS = [
{ value: AlertThresholdOperator.IS_ABOVE, label: 'IS ABOVE' },
{ value: AlertThresholdOperator.IS_BELOW, label: 'IS BELOW' },
{ value: AlertThresholdOperator.IS_EQUAL_TO, label: 'IS EQUAL TO' },
{ value: AlertThresholdOperator.IS_NOT_EQUAL_TO, label: 'IS NOT EQUAL TO' },
];
export const ANOMALY_THRESHOLD_OPERATOR_OPTIONS = [
{ value: AlertThresholdOperator.IS_ABOVE, label: 'IS ABOVE' },
{ value: AlertThresholdOperator.IS_BELOW, label: 'IS BELOW' },
{ value: AlertThresholdOperator.ABOVE_BELOW, label: 'ABOVE/BELOW' },
];
export const THRESHOLD_MATCH_TYPE_OPTIONS = [
{ value: AlertThresholdMatchType.AT_LEAST_ONCE, label: 'AT LEAST ONCE' },
{ value: AlertThresholdMatchType.ALL_THE_TIME, label: 'ALL THE TIME' },
{ value: AlertThresholdMatchType.ON_AVERAGE, label: 'ON AVERAGE' },
{ value: AlertThresholdMatchType.IN_TOTAL, label: 'IN TOTAL' },
{ value: AlertThresholdMatchType.LAST, label: 'LAST' },
];
export const ANOMALY_THRESHOLD_MATCH_TYPE_OPTIONS = [
{ value: AlertThresholdMatchType.AT_LEAST_ONCE, label: 'AT LEAST ONCE' },
{ value: AlertThresholdMatchType.ALL_THE_TIME, label: 'ALL THE TIME' },
];
export const ANOMALY_TIME_DURATION_OPTIONS = [
{ value: TimeDuration.FIVE_MINUTES, label: '5 minutes' },
{ value: TimeDuration.TEN_MINUTES, label: '10 minutes' },
{ value: TimeDuration.FIFTEEN_MINUTES, label: '15 minutes' },
{ value: TimeDuration.ONE_HOUR, label: '1 hour' },
{ value: TimeDuration.THREE_HOURS, label: '3 hours' },
{ value: TimeDuration.FOUR_HOURS, label: '4 hours' },
{ value: TimeDuration.TWENTY_FOUR_HOURS, label: '24 hours' },
];
export const ANOMALY_ALGORITHM_OPTIONS = [
{ value: Algorithm.STANDARD, label: 'Standard' },
];
export const ANOMALY_SEASONALITY_OPTIONS = [
{ value: Seasonality.HOURLY, label: 'Hourly' },
{ value: Seasonality.DAILY, label: 'Daily' },
{ value: Seasonality.WEEKLY, label: 'Weekly' },
];

View File

@ -5,18 +5,22 @@ import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
import { INITIAL_ALERT_STATE } from './constants';
import {
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
} from './constants';
import { ICreateAlertContextProps, ICreateAlertProviderProps } from './types';
import {
alertCreationReducer,
alertThresholdReducer,
buildInitialAlertDef,
getInitialAlertTypeFromURL,
} from './utils';
@ -51,7 +55,6 @@ export function CreateAlertProvider(
const [alertType, setAlertType] = useState<AlertTypes>(() =>
getInitialAlertTypeFromURL(queryParams, currentQuery),
);
const [alertDef] = useState<AlertDef>(buildInitialAlertDef(alertType));
const handleAlertTypeChange = useCallback(
(value: AlertTypes): void => {
@ -72,15 +75,27 @@ export function CreateAlertProvider(
[redirectWithQueryBuilderData],
);
const [thresholdState, setThresholdState] = useReducer(
alertThresholdReducer,
INITIAL_ALERT_THRESHOLD_STATE,
);
useEffect(() => {
setThresholdState({
type: 'RESET',
});
}, [alertType]);
const contextValue: ICreateAlertContextProps = useMemo(
() => ({
alertState,
setAlertState,
alertType,
setAlertType: handleAlertTypeChange,
alertDef,
thresholdState,
setThresholdState,
}),
[alertState, alertType, handleAlertTypeChange, alertDef],
[alertState, alertType, handleAlertTypeChange, thresholdState],
);
return (

View File

@ -1,13 +1,14 @@
import { Dispatch } from 'react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef, Labels } from 'types/api/alerts/def';
import { Labels } from 'types/api/alerts/def';
export interface ICreateAlertContextProps {
alertState: AlertState;
setAlertState: Dispatch<CreateAlertAction>;
alertType: AlertTypes;
setAlertType: Dispatch<AlertTypes>;
alertDef: AlertDef;
thresholdState: AlertThresholdState;
setThresholdState: Dispatch<AlertThresholdAction>;
}
export interface ICreateAlertProviderProps {
@ -25,9 +26,78 @@ export interface AlertState {
name: string;
description: string;
labels: Labels;
yAxisUnit: string | undefined;
}
export type CreateAlertAction =
| { type: 'SET_ALERT_NAME'; payload: string }
| { type: 'SET_ALERT_DESCRIPTION'; payload: string }
| { type: 'SET_ALERT_LABELS'; payload: Labels };
| { type: 'SET_ALERT_LABELS'; payload: Labels }
| { type: 'SET_Y_AXIS_UNIT'; payload: string | undefined };
export interface Threshold {
id: string;
label: string;
thresholdValue: number;
recoveryThresholdValue: number;
unit: string;
channels: string[];
color: string;
}
export enum AlertThresholdOperator {
IS_ABOVE = '1',
IS_BELOW = '2',
IS_EQUAL_TO = '3',
IS_NOT_EQUAL_TO = '4',
ABOVE_BELOW = '7',
}
export enum AlertThresholdMatchType {
AT_LEAST_ONCE = '1',
ALL_THE_TIME = '2',
ON_AVERAGE = '3',
IN_TOTAL = '4',
LAST = '5',
}
export interface AlertThresholdState {
selectedQuery: string;
operator: AlertThresholdOperator;
matchType: AlertThresholdMatchType;
evaluationWindow: string;
algorithm: string;
seasonality: string;
thresholds: Threshold[];
}
export enum TimeDuration {
ONE_MINUTE = '1m0s',
FIVE_MINUTES = '5m0s',
TEN_MINUTES = '10m0s',
FIFTEEN_MINUTES = '15m0s',
ONE_HOUR = '1h0m0s',
THREE_HOURS = '3h0m0s',
FOUR_HOURS = '4h0m0s',
TWENTY_FOUR_HOURS = '24h0m0s',
}
export enum Algorithm {
STANDARD = 'standard',
}
export enum Seasonality {
HOURLY = 'hourly',
DAILY = 'daily',
WEEKLY = 'weekly',
}
export type AlertThresholdAction =
| { type: 'SET_SELECTED_QUERY'; payload: string }
| { type: 'SET_OPERATOR'; payload: AlertThresholdOperator }
| { type: 'SET_MATCH_TYPE'; payload: AlertThresholdMatchType }
| { type: 'SET_EVALUATION_WINDOW'; payload: string }
| { type: 'SET_ALGORITHM'; payload: string }
| { type: 'SET_SEASONALITY'; payload: string }
| { type: 'SET_THRESHOLDS'; payload: Threshold[] }
| { type: 'RESET' };

View File

@ -11,7 +11,13 @@ import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { AlertState, CreateAlertAction } from './types';
import { INITIAL_ALERT_THRESHOLD_STATE } from './constants';
import {
AlertState,
AlertThresholdAction,
AlertThresholdState,
CreateAlertAction,
} from './types';
export const alertCreationReducer = (
state: AlertState,
@ -33,6 +39,11 @@ export const alertCreationReducer = (
...state,
labels: action.payload,
};
case 'SET_Y_AXIS_UNIT':
return {
...state,
yAxisUnit: action.payload,
};
default:
return state;
}
@ -79,3 +90,23 @@ export function getInitialAlertTypeFromURL(
? (alertTypeFromURL as AlertTypes)
: getInitialAlertType(currentQuery);
}
export const alertThresholdReducer = (
state: AlertThresholdState,
action: AlertThresholdAction,
): AlertThresholdState => {
switch (action.type) {
case 'SET_SELECTED_QUERY':
return { ...state, selectedQuery: action.payload };
case 'SET_OPERATOR':
return { ...state, operator: action.payload };
case 'SET_MATCH_TYPE':
return { ...state, matchType: action.payload };
case 'SET_THRESHOLDS':
return { ...state, thresholds: action.payload };
case 'RESET':
return INITIAL_ALERT_THRESHOLD_STATE;
default:
return state;
}
};

View File

@ -8,6 +8,8 @@ import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
import { Threshold } from 'container/CreateAlertV2/context/types';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
@ -51,7 +53,7 @@ import { getTimeRange } from 'utils/getTimeRange';
import { AlertDetectionTypes } from '..';
import { ChartContainer } from './styles';
import { getThresholdLabel } from './utils';
import { getThresholds } from './utils';
export interface ChartPreviewProps {
name: string;
@ -66,6 +68,7 @@ export interface ChartPreviewProps {
yAxisUnit: string;
setQueryStatus?: (status: string) => void;
showSideLegend?: boolean;
additionalThresholds?: Threshold[];
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@ -82,10 +85,26 @@ function ChartPreview({
yAxisUnit,
setQueryStatus,
showSideLegend = false,
additionalThresholds,
}: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const dispatch = useDispatch();
const threshold = alertDef?.condition.target || 0;
const thresholds: Threshold[] = useMemo(
() =>
additionalThresholds || [
{
...INITIAL_CRITICAL_THRESHOLD,
thresholdValue: alertDef?.condition.target || 0,
unit: alertDef?.condition.targetUnit || '',
},
],
[
additionalThresholds,
alertDef?.condition.target,
alertDef?.condition.targetUnit,
],
);
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
@ -264,24 +283,7 @@ function ChartPreview({
maxTimeScale,
isDarkMode,
onDragSelect,
thresholds: [
{
index: '0', // no impact
keyIndex: 0,
moveThreshold: (): void => {},
selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact
thresholdValue: threshold,
thresholdLabel: `${t(
'preview_chart_threshold_label',
)} (y=${getThresholdLabel(
optionName,
threshold,
alertDef?.condition.targetUnit,
yAxisUnit,
)})`,
thresholdUnit: alertDef?.condition.targetUnit,
},
],
thresholds: getThresholds(thresholds, t, optionName, yAxisUnit),
softMax: null,
softMin: null,
panelType: graphType,
@ -303,10 +305,9 @@ function ChartPreview({
maxTimeScale,
isDarkMode,
onDragSelect,
threshold,
thresholds,
t,
optionName,
alertDef?.condition.targetUnit,
graphType,
timezone.value,
currentQuery,
@ -386,6 +387,7 @@ ChartPreview.defaultProps = {
alertDef: undefined,
setQueryStatus: (): void => {},
showSideLegend: false,
additionalThresholds: undefined,
};
export default ChartPreview;

View File

@ -1,3 +1,7 @@
import { Color } from '@signozhq/design-tokens';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Threshold } from 'container/CreateAlertV2/context/types';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import {
BooleanFormats,
DataFormats,
@ -6,6 +10,7 @@ import {
ThroughputFormats,
TimeFormats,
} from 'container/NewWidget/RightContainer/types';
import { TFunction } from 'i18next';
import {
dataFormatConfig,
@ -83,3 +88,57 @@ interface IUnit {
sourceUnit?: string;
targetUnit?: string;
}
export const getThresholds = (
thresholds: Threshold[],
t: TFunction,
optionName: string,
yAxisUnit: string,
): ThresholdProps[] => {
const thresholdsToReturn = new Array<ThresholdProps>();
thresholds.forEach((threshold, index) => {
// Push main threshold
const mainThreshold = {
index: index.toString(),
keyIndex: index,
moveThreshold: (): void => {},
selectedGraph: PANEL_TYPES.TIME_SERIES,
thresholdValue: threshold.thresholdValue,
thresholdLabel:
threshold.label ||
`${t('preview_chart_threshold_label')} (y=${getThresholdLabel(
optionName,
threshold.thresholdValue,
threshold.unit,
yAxisUnit,
)})`,
thresholdUnit: threshold.unit,
thresholdColor: threshold.color || Color.TEXT_SAKURA_500,
};
thresholdsToReturn.push(mainThreshold);
// Push recovery threshold
if (threshold.recoveryThresholdValue) {
const recoveryThreshold = {
index: (thresholds.length + index).toString(),
keyIndex: thresholds.length + index,
moveThreshold: (): void => {},
selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact
thresholdValue: threshold.recoveryThresholdValue,
thresholdLabel: threshold.label
? `${threshold.label} (Recovery)`
: `${t('preview_chart_threshold_label')} (y=${getThresholdLabel(
optionName,
threshold.thresholdValue,
threshold.unit,
yAxisUnit,
)})`,
thresholdUnit: threshold.unit,
thresholdColor: threshold.color || Color.TEXT_SAKURA_500,
};
thresholdsToReturn.push(recoveryThreshold);
}
});
return thresholdsToReturn;
};

View File

@ -98,7 +98,7 @@ export function AboutSigNozQuestions({
<TextArea
className="discover-signoz-input"
placeholder="e.g., Google Search, Hacker News, Reddit, a friend, ChatGPT, a blog post, a conference, etc."
placeholder={`e.g., googling "datadog alternative", a post on r/devops, from a friend/colleague, a LinkedIn post, ChatGPT, etc.`}
value={discoverSignoz}
autoFocus
rows={4}

View File

@ -192,7 +192,7 @@ function OrgQuestions({
return (
<div className="questions-container">
<Typography.Title level={3} className="title">
Welcome, {user?.displayName}!
{user?.displayName ? `Welcome, ${user.displayName}!` : 'Welcome!'}
</Typography.Title>
<Typography.Paragraph className="sub-title">
We&apos;ll help you get the most out of SigNoz, whether you&apos;re new to
@ -296,15 +296,15 @@ function OrgQuestions({
onChange={(e): void => setOtherTool(e.target.value)}
/>
) : (
<button
type="button"
<Button
type="primary"
className={`onboarding-questionaire-button ${
observabilityTool === 'Others' ? 'active' : ''
}`}
onClick={(): void => setObservabilityTool('Others')}
>
Others
</button>
</Button>
)}
</div>
</div>

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
// Generated from ../../../../grammar/FilterQuery.g4 by ANTLR 4.13.1
// Generated from FilterQuery.g4 by ANTLR 4.13.1
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
import {
ATN,
@ -100,7 +100,7 @@ export default class FilterQueryLexer extends Lexer {
public get modeNames(): string[] { return FilterQueryLexer.modeNames; }
public static readonly _serializedATN: number[] = [4,0,32,314,6,-1,2,0,
public static readonly _serializedATN: number[] = [4,0,32,320,6,-1,2,0,
7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,
7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,
16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,
@ -122,94 +122,97 @@ export default class FilterQueryLexer extends Lexer {
3,28,253,8,28,1,29,1,29,1,29,1,29,5,29,259,8,29,10,29,12,29,262,9,29,1,
29,1,29,1,29,1,29,1,29,5,29,269,8,29,10,29,12,29,272,9,29,1,29,3,29,275,
8,29,1,30,1,30,5,30,279,8,30,10,30,12,30,282,9,30,1,31,1,31,1,31,1,32,1,
32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,5,33,296,8,33,10,33,12,33,299,9,33,
1,34,4,34,302,8,34,11,34,12,34,303,1,34,1,34,1,35,1,35,1,36,4,36,311,8,
36,11,36,12,36,312,0,0,37,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,
21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,20,41,21,43,22,
45,23,47,24,49,25,51,26,53,27,55,0,57,28,59,29,61,0,63,0,65,0,67,30,69,
31,71,0,73,32,1,0,29,2,0,76,76,108,108,2,0,73,73,105,105,2,0,75,75,107,
107,2,0,69,69,101,101,2,0,66,66,98,98,2,0,84,84,116,116,2,0,87,87,119,119,
2,0,78,78,110,110,2,0,88,88,120,120,2,0,83,83,115,115,2,0,82,82,114,114,
2,0,71,71,103,103,2,0,80,80,112,112,2,0,67,67,99,99,2,0,79,79,111,111,2,
0,65,65,97,97,2,0,68,68,100,100,2,0,72,72,104,104,2,0,89,89,121,121,2,0,
85,85,117,117,2,0,70,70,102,102,2,0,43,43,45,45,2,0,34,34,92,92,2,0,39,
39,92,92,4,0,36,36,65,90,95,95,97,122,6,0,36,36,45,45,47,58,65,90,95,95,
97,122,3,0,9,10,13,13,32,32,1,0,48,57,8,0,9,10,13,13,32,34,39,41,44,44,
60,62,91,91,93,93,336,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,
9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,
0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,
31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,
0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,
53,1,0,0,0,0,57,1,0,0,0,0,59,1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,73,1,0,
0,0,1,75,1,0,0,0,3,77,1,0,0,0,5,79,1,0,0,0,7,81,1,0,0,0,9,83,1,0,0,0,11,
88,1,0,0,0,13,90,1,0,0,0,15,93,1,0,0,0,17,96,1,0,0,0,19,98,1,0,0,0,21,101,
1,0,0,0,23,103,1,0,0,0,25,106,1,0,0,0,27,111,1,0,0,0,29,117,1,0,0,0,31,
125,1,0,0,0,33,133,1,0,0,0,35,140,1,0,0,0,37,150,1,0,0,0,39,153,1,0,0,0,
41,157,1,0,0,0,43,161,1,0,0,0,45,164,1,0,0,0,47,173,1,0,0,0,49,177,1,0,
0,0,51,184,1,0,0,0,53,200,1,0,0,0,55,202,1,0,0,0,57,252,1,0,0,0,59,274,
1,0,0,0,61,276,1,0,0,0,63,283,1,0,0,0,65,286,1,0,0,0,67,290,1,0,0,0,69,
301,1,0,0,0,71,307,1,0,0,0,73,310,1,0,0,0,75,76,5,40,0,0,76,2,1,0,0,0,77,
78,5,41,0,0,78,4,1,0,0,0,79,80,5,91,0,0,80,6,1,0,0,0,81,82,5,93,0,0,82,
8,1,0,0,0,83,84,5,44,0,0,84,10,1,0,0,0,85,89,5,61,0,0,86,87,5,61,0,0,87,
89,5,61,0,0,88,85,1,0,0,0,88,86,1,0,0,0,89,12,1,0,0,0,90,91,5,33,0,0,91,
92,5,61,0,0,92,14,1,0,0,0,93,94,5,60,0,0,94,95,5,62,0,0,95,16,1,0,0,0,96,
97,5,60,0,0,97,18,1,0,0,0,98,99,5,60,0,0,99,100,5,61,0,0,100,20,1,0,0,0,
101,102,5,62,0,0,102,22,1,0,0,0,103,104,5,62,0,0,104,105,5,61,0,0,105,24,
1,0,0,0,106,107,7,0,0,0,107,108,7,1,0,0,108,109,7,2,0,0,109,110,7,3,0,0,
110,26,1,0,0,0,111,112,7,1,0,0,112,113,7,0,0,0,113,114,7,1,0,0,114,115,
7,2,0,0,115,116,7,3,0,0,116,28,1,0,0,0,117,118,7,4,0,0,118,119,7,3,0,0,
119,120,7,5,0,0,120,121,7,6,0,0,121,122,7,3,0,0,122,123,7,3,0,0,123,124,
7,7,0,0,124,30,1,0,0,0,125,126,7,3,0,0,126,127,7,8,0,0,127,128,7,1,0,0,
128,129,7,9,0,0,129,131,7,5,0,0,130,132,7,9,0,0,131,130,1,0,0,0,131,132,
1,0,0,0,132,32,1,0,0,0,133,134,7,10,0,0,134,135,7,3,0,0,135,136,7,11,0,
0,136,137,7,3,0,0,137,138,7,8,0,0,138,139,7,12,0,0,139,34,1,0,0,0,140,141,
7,13,0,0,141,142,7,14,0,0,142,143,7,7,0,0,143,144,7,5,0,0,144,145,7,15,
0,0,145,146,7,1,0,0,146,148,7,7,0,0,147,149,7,9,0,0,148,147,1,0,0,0,148,
149,1,0,0,0,149,36,1,0,0,0,150,151,7,1,0,0,151,152,7,7,0,0,152,38,1,0,0,
0,153,154,7,7,0,0,154,155,7,14,0,0,155,156,7,5,0,0,156,40,1,0,0,0,157,158,
7,15,0,0,158,159,7,7,0,0,159,160,7,16,0,0,160,42,1,0,0,0,161,162,7,14,0,
0,162,163,7,10,0,0,163,44,1,0,0,0,164,165,7,17,0,0,165,166,7,15,0,0,166,
167,7,9,0,0,167,168,7,5,0,0,168,169,7,14,0,0,169,170,7,2,0,0,170,171,7,
3,0,0,171,172,7,7,0,0,172,46,1,0,0,0,173,174,7,17,0,0,174,175,7,15,0,0,
175,176,7,9,0,0,176,48,1,0,0,0,177,178,7,17,0,0,178,179,7,15,0,0,179,180,
7,9,0,0,180,181,7,15,0,0,181,182,7,7,0,0,182,183,7,18,0,0,183,50,1,0,0,
0,184,185,7,17,0,0,185,186,7,15,0,0,186,187,7,9,0,0,187,188,7,15,0,0,188,
189,7,0,0,0,189,190,7,0,0,0,190,52,1,0,0,0,191,192,7,5,0,0,192,193,7,10,
0,0,193,194,7,19,0,0,194,201,7,3,0,0,195,196,7,20,0,0,196,197,7,15,0,0,
197,198,7,0,0,0,198,199,7,9,0,0,199,201,7,3,0,0,200,191,1,0,0,0,200,195,
1,0,0,0,201,54,1,0,0,0,202,203,7,21,0,0,203,56,1,0,0,0,204,206,3,55,27,
0,205,204,1,0,0,0,205,206,1,0,0,0,206,208,1,0,0,0,207,209,3,71,35,0,208,
207,1,0,0,0,209,210,1,0,0,0,210,208,1,0,0,0,210,211,1,0,0,0,211,219,1,0,
0,0,212,216,5,46,0,0,213,215,3,71,35,0,214,213,1,0,0,0,215,218,1,0,0,0,
216,214,1,0,0,0,216,217,1,0,0,0,217,220,1,0,0,0,218,216,1,0,0,0,219,212,
1,0,0,0,219,220,1,0,0,0,220,230,1,0,0,0,221,223,7,3,0,0,222,224,3,55,27,
0,223,222,1,0,0,0,223,224,1,0,0,0,224,226,1,0,0,0,225,227,3,71,35,0,226,
225,1,0,0,0,227,228,1,0,0,0,228,226,1,0,0,0,228,229,1,0,0,0,229,231,1,0,
0,0,230,221,1,0,0,0,230,231,1,0,0,0,231,253,1,0,0,0,232,234,3,55,27,0,233,
232,1,0,0,0,233,234,1,0,0,0,234,235,1,0,0,0,235,237,5,46,0,0,236,238,3,
71,35,0,237,236,1,0,0,0,238,239,1,0,0,0,239,237,1,0,0,0,239,240,1,0,0,0,
240,250,1,0,0,0,241,243,7,3,0,0,242,244,3,55,27,0,243,242,1,0,0,0,243,244,
1,0,0,0,244,246,1,0,0,0,245,247,3,71,35,0,246,245,1,0,0,0,247,248,1,0,0,
0,248,246,1,0,0,0,248,249,1,0,0,0,249,251,1,0,0,0,250,241,1,0,0,0,250,251,
1,0,0,0,251,253,1,0,0,0,252,205,1,0,0,0,252,233,1,0,0,0,253,58,1,0,0,0,
254,260,5,34,0,0,255,259,8,22,0,0,256,257,5,92,0,0,257,259,9,0,0,0,258,
255,1,0,0,0,258,256,1,0,0,0,259,262,1,0,0,0,260,258,1,0,0,0,260,261,1,0,
0,0,261,263,1,0,0,0,262,260,1,0,0,0,263,275,5,34,0,0,264,270,5,39,0,0,265,
269,8,23,0,0,266,267,5,92,0,0,267,269,9,0,0,0,268,265,1,0,0,0,268,266,1,
0,0,0,269,272,1,0,0,0,270,268,1,0,0,0,270,271,1,0,0,0,271,273,1,0,0,0,272,
270,1,0,0,0,273,275,5,39,0,0,274,254,1,0,0,0,274,264,1,0,0,0,275,60,1,0,
0,0,276,280,7,24,0,0,277,279,7,25,0,0,278,277,1,0,0,0,279,282,1,0,0,0,280,
278,1,0,0,0,280,281,1,0,0,0,281,62,1,0,0,0,282,280,1,0,0,0,283,284,5,91,
0,0,284,285,5,93,0,0,285,64,1,0,0,0,286,287,5,91,0,0,287,288,5,42,0,0,288,
289,5,93,0,0,289,66,1,0,0,0,290,297,3,61,30,0,291,292,5,46,0,0,292,296,
3,61,30,0,293,296,3,63,31,0,294,296,3,65,32,0,295,291,1,0,0,0,295,293,1,
0,0,0,295,294,1,0,0,0,296,299,1,0,0,0,297,295,1,0,0,0,297,298,1,0,0,0,298,
68,1,0,0,0,299,297,1,0,0,0,300,302,7,26,0,0,301,300,1,0,0,0,302,303,1,0,
0,0,303,301,1,0,0,0,303,304,1,0,0,0,304,305,1,0,0,0,305,306,6,34,0,0,306,
70,1,0,0,0,307,308,7,27,0,0,308,72,1,0,0,0,309,311,8,28,0,0,310,309,1,0,
0,0,311,312,1,0,0,0,312,310,1,0,0,0,312,313,1,0,0,0,313,74,1,0,0,0,28,0,
88,131,148,200,205,210,216,219,223,228,230,233,239,243,248,250,252,258,
260,268,270,274,280,295,297,303,312,1,6,0,0];
32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,1,33,4,33,298,8,33,11,33,12,
33,299,5,33,302,8,33,10,33,12,33,305,9,33,1,34,4,34,308,8,34,11,34,12,34,
309,1,34,1,34,1,35,1,35,1,36,4,36,317,8,36,11,36,12,36,318,0,0,37,1,1,3,
2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,
16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,
0,57,28,59,29,61,0,63,0,65,0,67,30,69,31,71,0,73,32,1,0,29,2,0,76,76,108,
108,2,0,73,73,105,105,2,0,75,75,107,107,2,0,69,69,101,101,2,0,66,66,98,
98,2,0,84,84,116,116,2,0,87,87,119,119,2,0,78,78,110,110,2,0,88,88,120,
120,2,0,83,83,115,115,2,0,82,82,114,114,2,0,71,71,103,103,2,0,80,80,112,
112,2,0,67,67,99,99,2,0,79,79,111,111,2,0,65,65,97,97,2,0,68,68,100,100,
2,0,72,72,104,104,2,0,89,89,121,121,2,0,85,85,117,117,2,0,70,70,102,102,
2,0,43,43,45,45,2,0,34,34,92,92,2,0,39,39,92,92,4,0,35,36,64,90,95,95,97,
123,7,0,35,36,45,45,47,58,64,90,95,95,97,123,125,125,3,0,9,10,13,13,32,
32,1,0,48,57,8,0,9,10,13,13,32,34,39,41,44,44,60,62,91,91,93,93,344,0,1,
1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,
13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,
0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,
35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,
0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,57,1,0,0,0,0,
59,1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,73,1,0,0,0,1,75,1,0,0,0,3,77,1,0,
0,0,5,79,1,0,0,0,7,81,1,0,0,0,9,83,1,0,0,0,11,88,1,0,0,0,13,90,1,0,0,0,
15,93,1,0,0,0,17,96,1,0,0,0,19,98,1,0,0,0,21,101,1,0,0,0,23,103,1,0,0,0,
25,106,1,0,0,0,27,111,1,0,0,0,29,117,1,0,0,0,31,125,1,0,0,0,33,133,1,0,
0,0,35,140,1,0,0,0,37,150,1,0,0,0,39,153,1,0,0,0,41,157,1,0,0,0,43,161,
1,0,0,0,45,164,1,0,0,0,47,173,1,0,0,0,49,177,1,0,0,0,51,184,1,0,0,0,53,
200,1,0,0,0,55,202,1,0,0,0,57,252,1,0,0,0,59,274,1,0,0,0,61,276,1,0,0,0,
63,283,1,0,0,0,65,286,1,0,0,0,67,290,1,0,0,0,69,307,1,0,0,0,71,313,1,0,
0,0,73,316,1,0,0,0,75,76,5,40,0,0,76,2,1,0,0,0,77,78,5,41,0,0,78,4,1,0,
0,0,79,80,5,91,0,0,80,6,1,0,0,0,81,82,5,93,0,0,82,8,1,0,0,0,83,84,5,44,
0,0,84,10,1,0,0,0,85,89,5,61,0,0,86,87,5,61,0,0,87,89,5,61,0,0,88,85,1,
0,0,0,88,86,1,0,0,0,89,12,1,0,0,0,90,91,5,33,0,0,91,92,5,61,0,0,92,14,1,
0,0,0,93,94,5,60,0,0,94,95,5,62,0,0,95,16,1,0,0,0,96,97,5,60,0,0,97,18,
1,0,0,0,98,99,5,60,0,0,99,100,5,61,0,0,100,20,1,0,0,0,101,102,5,62,0,0,
102,22,1,0,0,0,103,104,5,62,0,0,104,105,5,61,0,0,105,24,1,0,0,0,106,107,
7,0,0,0,107,108,7,1,0,0,108,109,7,2,0,0,109,110,7,3,0,0,110,26,1,0,0,0,
111,112,7,1,0,0,112,113,7,0,0,0,113,114,7,1,0,0,114,115,7,2,0,0,115,116,
7,3,0,0,116,28,1,0,0,0,117,118,7,4,0,0,118,119,7,3,0,0,119,120,7,5,0,0,
120,121,7,6,0,0,121,122,7,3,0,0,122,123,7,3,0,0,123,124,7,7,0,0,124,30,
1,0,0,0,125,126,7,3,0,0,126,127,7,8,0,0,127,128,7,1,0,0,128,129,7,9,0,0,
129,131,7,5,0,0,130,132,7,9,0,0,131,130,1,0,0,0,131,132,1,0,0,0,132,32,
1,0,0,0,133,134,7,10,0,0,134,135,7,3,0,0,135,136,7,11,0,0,136,137,7,3,0,
0,137,138,7,8,0,0,138,139,7,12,0,0,139,34,1,0,0,0,140,141,7,13,0,0,141,
142,7,14,0,0,142,143,7,7,0,0,143,144,7,5,0,0,144,145,7,15,0,0,145,146,7,
1,0,0,146,148,7,7,0,0,147,149,7,9,0,0,148,147,1,0,0,0,148,149,1,0,0,0,149,
36,1,0,0,0,150,151,7,1,0,0,151,152,7,7,0,0,152,38,1,0,0,0,153,154,7,7,0,
0,154,155,7,14,0,0,155,156,7,5,0,0,156,40,1,0,0,0,157,158,7,15,0,0,158,
159,7,7,0,0,159,160,7,16,0,0,160,42,1,0,0,0,161,162,7,14,0,0,162,163,7,
10,0,0,163,44,1,0,0,0,164,165,7,17,0,0,165,166,7,15,0,0,166,167,7,9,0,0,
167,168,7,5,0,0,168,169,7,14,0,0,169,170,7,2,0,0,170,171,7,3,0,0,171,172,
7,7,0,0,172,46,1,0,0,0,173,174,7,17,0,0,174,175,7,15,0,0,175,176,7,9,0,
0,176,48,1,0,0,0,177,178,7,17,0,0,178,179,7,15,0,0,179,180,7,9,0,0,180,
181,7,15,0,0,181,182,7,7,0,0,182,183,7,18,0,0,183,50,1,0,0,0,184,185,7,
17,0,0,185,186,7,15,0,0,186,187,7,9,0,0,187,188,7,15,0,0,188,189,7,0,0,
0,189,190,7,0,0,0,190,52,1,0,0,0,191,192,7,5,0,0,192,193,7,10,0,0,193,194,
7,19,0,0,194,201,7,3,0,0,195,196,7,20,0,0,196,197,7,15,0,0,197,198,7,0,
0,0,198,199,7,9,0,0,199,201,7,3,0,0,200,191,1,0,0,0,200,195,1,0,0,0,201,
54,1,0,0,0,202,203,7,21,0,0,203,56,1,0,0,0,204,206,3,55,27,0,205,204,1,
0,0,0,205,206,1,0,0,0,206,208,1,0,0,0,207,209,3,71,35,0,208,207,1,0,0,0,
209,210,1,0,0,0,210,208,1,0,0,0,210,211,1,0,0,0,211,219,1,0,0,0,212,216,
5,46,0,0,213,215,3,71,35,0,214,213,1,0,0,0,215,218,1,0,0,0,216,214,1,0,
0,0,216,217,1,0,0,0,217,220,1,0,0,0,218,216,1,0,0,0,219,212,1,0,0,0,219,
220,1,0,0,0,220,230,1,0,0,0,221,223,7,3,0,0,222,224,3,55,27,0,223,222,1,
0,0,0,223,224,1,0,0,0,224,226,1,0,0,0,225,227,3,71,35,0,226,225,1,0,0,0,
227,228,1,0,0,0,228,226,1,0,0,0,228,229,1,0,0,0,229,231,1,0,0,0,230,221,
1,0,0,0,230,231,1,0,0,0,231,253,1,0,0,0,232,234,3,55,27,0,233,232,1,0,0,
0,233,234,1,0,0,0,234,235,1,0,0,0,235,237,5,46,0,0,236,238,3,71,35,0,237,
236,1,0,0,0,238,239,1,0,0,0,239,237,1,0,0,0,239,240,1,0,0,0,240,250,1,0,
0,0,241,243,7,3,0,0,242,244,3,55,27,0,243,242,1,0,0,0,243,244,1,0,0,0,244,
246,1,0,0,0,245,247,3,71,35,0,246,245,1,0,0,0,247,248,1,0,0,0,248,246,1,
0,0,0,248,249,1,0,0,0,249,251,1,0,0,0,250,241,1,0,0,0,250,251,1,0,0,0,251,
253,1,0,0,0,252,205,1,0,0,0,252,233,1,0,0,0,253,58,1,0,0,0,254,260,5,34,
0,0,255,259,8,22,0,0,256,257,5,92,0,0,257,259,9,0,0,0,258,255,1,0,0,0,258,
256,1,0,0,0,259,262,1,0,0,0,260,258,1,0,0,0,260,261,1,0,0,0,261,263,1,0,
0,0,262,260,1,0,0,0,263,275,5,34,0,0,264,270,5,39,0,0,265,269,8,23,0,0,
266,267,5,92,0,0,267,269,9,0,0,0,268,265,1,0,0,0,268,266,1,0,0,0,269,272,
1,0,0,0,270,268,1,0,0,0,270,271,1,0,0,0,271,273,1,0,0,0,272,270,1,0,0,0,
273,275,5,39,0,0,274,254,1,0,0,0,274,264,1,0,0,0,275,60,1,0,0,0,276,280,
7,24,0,0,277,279,7,25,0,0,278,277,1,0,0,0,279,282,1,0,0,0,280,278,1,0,0,
0,280,281,1,0,0,0,281,62,1,0,0,0,282,280,1,0,0,0,283,284,5,91,0,0,284,285,
5,93,0,0,285,64,1,0,0,0,286,287,5,91,0,0,287,288,5,42,0,0,288,289,5,93,
0,0,289,66,1,0,0,0,290,303,3,61,30,0,291,292,5,46,0,0,292,302,3,61,30,0,
293,302,3,63,31,0,294,302,3,65,32,0,295,297,5,46,0,0,296,298,3,71,35,0,
297,296,1,0,0,0,298,299,1,0,0,0,299,297,1,0,0,0,299,300,1,0,0,0,300,302,
1,0,0,0,301,291,1,0,0,0,301,293,1,0,0,0,301,294,1,0,0,0,301,295,1,0,0,0,
302,305,1,0,0,0,303,301,1,0,0,0,303,304,1,0,0,0,304,68,1,0,0,0,305,303,
1,0,0,0,306,308,7,26,0,0,307,306,1,0,0,0,308,309,1,0,0,0,309,307,1,0,0,
0,309,310,1,0,0,0,310,311,1,0,0,0,311,312,6,34,0,0,312,70,1,0,0,0,313,314,
7,27,0,0,314,72,1,0,0,0,315,317,8,28,0,0,316,315,1,0,0,0,317,318,1,0,0,
0,318,316,1,0,0,0,318,319,1,0,0,0,319,74,1,0,0,0,29,0,88,131,148,200,205,
210,216,219,223,228,230,233,239,243,248,250,252,258,260,268,270,274,280,
299,301,303,309,318,1,6,0,0];
private static __ATN: ATN;
public static get _ATN(): ATN {

View File

@ -1,4 +1,4 @@
// Generated from ../../../../grammar/FilterQuery.g4 by ANTLR 4.13.1
// Generated from FilterQuery.g4 by ANTLR 4.13.1
import {ParseTreeListener} from "antlr4";

View File

@ -1,4 +1,4 @@
// Generated from ../../../../grammar/FilterQuery.g4 by ANTLR 4.13.1
// Generated from FilterQuery.g4 by ANTLR 4.13.1
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
import {

View File

@ -1,4 +1,4 @@
// Generated from ../../../../grammar/FilterQuery.g4 by ANTLR 4.13.1
// Generated from FilterQuery.g4 by ANTLR 4.13.1
import {ParseTreeVisitor} from 'antlr4';

2
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.36.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.128.1
github.com/SigNoz/signoz-otel-collector v0.129.4
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3
github.com/cespare/xxhash/v2 v2.3.0

4
go.sum
View File

@ -104,8 +104,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8=
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
github.com/SigNoz/signoz-otel-collector v0.128.1 h1:D0bKMrRNgcKreKKYoakCr5jTWj1srupbNwGIvpHMihw=
github.com/SigNoz/signoz-otel-collector v0.128.1/go.mod h1:vFQLsJFzQwVkO1ltIMH+z9KKuTZTn/P0lKu2mNYDBpE=
github.com/SigNoz/signoz-otel-collector v0.129.4 h1:DGDu9y1I1FU+HX4eECPGmfhnXE4ys4yr7LL6znbf6to=
github.com/SigNoz/signoz-otel-collector v0.129.4/go.mod h1:xyR+coBzzO04p6Eu+ql2RVYUl/jFD+8hD9lArcc9U7g=
github.com/Yiling-J/theine-go v0.6.1 h1:njE/rBBviU/Sq2G7PJKdLdwXg8j1azvZQulIjmshD+o=
github.com/Yiling-J/theine-go v0.6.1/go.mod h1:08QpMa5JZ2pKN+UJCRrCasWYO1IKCdl54Xa836rpmDU=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=

View File

@ -207,12 +207,12 @@ QUOTED_TEXT
)
;
fragment SEGMENT : [a-zA-Z$_] [a-zA-Z0-9$_:\-/]* ;
fragment SEGMENT : [a-zA-Z$_@{#] [a-zA-Z0-9$_@#{}:\-/]* ;
fragment EMPTY_BRACKS : '[' ']' ;
fragment OLD_JSON_BRACKS: '[' '*' ']';
KEY
: SEGMENT ( '.' SEGMENT | EMPTY_BRACKS | OLD_JSON_BRACKS)*
: SEGMENT ( '.' SEGMENT | EMPTY_BRACKS | OLD_JSON_BRACKS | '.' DIGIT+)*
;
// Ignore whitespace

View File

@ -10,7 +10,6 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/transition"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
@ -51,11 +50,9 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
return
}
if querybuilder.QBV5Enabled {
dashboardMigrator := transition.NewDashboardMigrateV5(handler.providerSettings.Logger, nil, nil)
if req["version"] != "v5" {
dashboardMigrator.Migrate(ctx, req)
}
dashboardMigrator := transition.NewDashboardMigrateV5(handler.providerSettings.Logger, nil, nil)
if req["version"] != "v5" {
dashboardMigrator.Migrate(ctx, req)
}
dashboard, err := handler.module.Create(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.UserID), req)

File diff suppressed because one or more lines are too long

View File

@ -61,7 +61,7 @@ func filterquerylexerLexerInit() {
}
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
staticData.serializedATN = []int32{
4, 0, 32, 314, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 0, 32, 320, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
@ -90,127 +90,130 @@ func filterquerylexerLexerInit() {
8, 29, 10, 29, 12, 29, 262, 9, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 5,
29, 269, 8, 29, 10, 29, 12, 29, 272, 9, 29, 1, 29, 3, 29, 275, 8, 29, 1,
30, 1, 30, 5, 30, 279, 8, 30, 10, 30, 12, 30, 282, 9, 30, 1, 31, 1, 31,
1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 5,
33, 296, 8, 33, 10, 33, 12, 33, 299, 9, 33, 1, 34, 4, 34, 302, 8, 34, 11,
34, 12, 34, 303, 1, 34, 1, 34, 1, 35, 1, 35, 1, 36, 4, 36, 311, 8, 36,
11, 36, 12, 36, 312, 0, 0, 37, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13,
7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16,
33, 17, 35, 18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25,
51, 26, 53, 27, 55, 0, 57, 28, 59, 29, 61, 0, 63, 0, 65, 0, 67, 30, 69,
31, 71, 0, 73, 32, 1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105,
105, 2, 0, 75, 75, 107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98,
98, 2, 0, 84, 84, 116, 116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110,
110, 2, 0, 88, 88, 120, 120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114,
114, 2, 0, 71, 71, 103, 103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99,
99, 2, 0, 79, 79, 111, 111, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100,
2, 0, 72, 72, 104, 104, 2, 0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117,
2, 0, 70, 70, 102, 102, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2,
0, 39, 39, 92, 92, 4, 0, 36, 36, 65, 90, 95, 95, 97, 122, 6, 0, 36, 36,
45, 45, 47, 58, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1,
0, 48, 57, 8, 0, 9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91,
93, 93, 336, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7,
1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0,
15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0,
0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0,
0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0,
0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1,
0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53,
1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0,
69, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 1, 75, 1, 0, 0, 0, 3, 77, 1, 0, 0, 0,
5, 79, 1, 0, 0, 0, 7, 81, 1, 0, 0, 0, 9, 83, 1, 0, 0, 0, 11, 88, 1, 0,
0, 0, 13, 90, 1, 0, 0, 0, 15, 93, 1, 0, 0, 0, 17, 96, 1, 0, 0, 0, 19, 98,
1, 0, 0, 0, 21, 101, 1, 0, 0, 0, 23, 103, 1, 0, 0, 0, 25, 106, 1, 0, 0,
0, 27, 111, 1, 0, 0, 0, 29, 117, 1, 0, 0, 0, 31, 125, 1, 0, 0, 0, 33, 133,
1, 0, 0, 0, 35, 140, 1, 0, 0, 0, 37, 150, 1, 0, 0, 0, 39, 153, 1, 0, 0,
0, 41, 157, 1, 0, 0, 0, 43, 161, 1, 0, 0, 0, 45, 164, 1, 0, 0, 0, 47, 173,
1, 0, 0, 0, 49, 177, 1, 0, 0, 0, 51, 184, 1, 0, 0, 0, 53, 200, 1, 0, 0,
0, 55, 202, 1, 0, 0, 0, 57, 252, 1, 0, 0, 0, 59, 274, 1, 0, 0, 0, 61, 276,
1, 0, 0, 0, 63, 283, 1, 0, 0, 0, 65, 286, 1, 0, 0, 0, 67, 290, 1, 0, 0,
0, 69, 301, 1, 0, 0, 0, 71, 307, 1, 0, 0, 0, 73, 310, 1, 0, 0, 0, 75, 76,
5, 40, 0, 0, 76, 2, 1, 0, 0, 0, 77, 78, 5, 41, 0, 0, 78, 4, 1, 0, 0, 0,
79, 80, 5, 91, 0, 0, 80, 6, 1, 0, 0, 0, 81, 82, 5, 93, 0, 0, 82, 8, 1,
0, 0, 0, 83, 84, 5, 44, 0, 0, 84, 10, 1, 0, 0, 0, 85, 89, 5, 61, 0, 0,
86, 87, 5, 61, 0, 0, 87, 89, 5, 61, 0, 0, 88, 85, 1, 0, 0, 0, 88, 86, 1,
0, 0, 0, 89, 12, 1, 0, 0, 0, 90, 91, 5, 33, 0, 0, 91, 92, 5, 61, 0, 0,
92, 14, 1, 0, 0, 0, 93, 94, 5, 60, 0, 0, 94, 95, 5, 62, 0, 0, 95, 16, 1,
0, 0, 0, 96, 97, 5, 60, 0, 0, 97, 18, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0,
99, 100, 5, 61, 0, 0, 100, 20, 1, 0, 0, 0, 101, 102, 5, 62, 0, 0, 102,
22, 1, 0, 0, 0, 103, 104, 5, 62, 0, 0, 104, 105, 5, 61, 0, 0, 105, 24,
1, 0, 0, 0, 106, 107, 7, 0, 0, 0, 107, 108, 7, 1, 0, 0, 108, 109, 7, 2,
0, 0, 109, 110, 7, 3, 0, 0, 110, 26, 1, 0, 0, 0, 111, 112, 7, 1, 0, 0,
112, 113, 7, 0, 0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 2, 0, 0, 115,
116, 7, 3, 0, 0, 116, 28, 1, 0, 0, 0, 117, 118, 7, 4, 0, 0, 118, 119, 7,
3, 0, 0, 119, 120, 7, 5, 0, 0, 120, 121, 7, 6, 0, 0, 121, 122, 7, 3, 0,
0, 122, 123, 7, 3, 0, 0, 123, 124, 7, 7, 0, 0, 124, 30, 1, 0, 0, 0, 125,
126, 7, 3, 0, 0, 126, 127, 7, 8, 0, 0, 127, 128, 7, 1, 0, 0, 128, 129,
7, 9, 0, 0, 129, 131, 7, 5, 0, 0, 130, 132, 7, 9, 0, 0, 131, 130, 1, 0,
0, 0, 131, 132, 1, 0, 0, 0, 132, 32, 1, 0, 0, 0, 133, 134, 7, 10, 0, 0,
134, 135, 7, 3, 0, 0, 135, 136, 7, 11, 0, 0, 136, 137, 7, 3, 0, 0, 137,
138, 7, 8, 0, 0, 138, 139, 7, 12, 0, 0, 139, 34, 1, 0, 0, 0, 140, 141,
7, 13, 0, 0, 141, 142, 7, 14, 0, 0, 142, 143, 7, 7, 0, 0, 143, 144, 7,
5, 0, 0, 144, 145, 7, 15, 0, 0, 145, 146, 7, 1, 0, 0, 146, 148, 7, 7, 0,
0, 147, 149, 7, 9, 0, 0, 148, 147, 1, 0, 0, 0, 148, 149, 1, 0, 0, 0, 149,
36, 1, 0, 0, 0, 150, 151, 7, 1, 0, 0, 151, 152, 7, 7, 0, 0, 152, 38, 1,
0, 0, 0, 153, 154, 7, 7, 0, 0, 154, 155, 7, 14, 0, 0, 155, 156, 7, 5, 0,
0, 156, 40, 1, 0, 0, 0, 157, 158, 7, 15, 0, 0, 158, 159, 7, 7, 0, 0, 159,
160, 7, 16, 0, 0, 160, 42, 1, 0, 0, 0, 161, 162, 7, 14, 0, 0, 162, 163,
7, 10, 0, 0, 163, 44, 1, 0, 0, 0, 164, 165, 7, 17, 0, 0, 165, 166, 7, 15,
0, 0, 166, 167, 7, 9, 0, 0, 167, 168, 7, 5, 0, 0, 168, 169, 7, 14, 0, 0,
169, 170, 7, 2, 0, 0, 170, 171, 7, 3, 0, 0, 171, 172, 7, 7, 0, 0, 172,
46, 1, 0, 0, 0, 173, 174, 7, 17, 0, 0, 174, 175, 7, 15, 0, 0, 175, 176,
7, 9, 0, 0, 176, 48, 1, 0, 0, 0, 177, 178, 7, 17, 0, 0, 178, 179, 7, 15,
0, 0, 179, 180, 7, 9, 0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 7, 0, 0,
182, 183, 7, 18, 0, 0, 183, 50, 1, 0, 0, 0, 184, 185, 7, 17, 0, 0, 185,
186, 7, 15, 0, 0, 186, 187, 7, 9, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189,
7, 0, 0, 0, 189, 190, 7, 0, 0, 0, 190, 52, 1, 0, 0, 0, 191, 192, 7, 5,
0, 0, 192, 193, 7, 10, 0, 0, 193, 194, 7, 19, 0, 0, 194, 201, 7, 3, 0,
0, 195, 196, 7, 20, 0, 0, 196, 197, 7, 15, 0, 0, 197, 198, 7, 0, 0, 0,
198, 199, 7, 9, 0, 0, 199, 201, 7, 3, 0, 0, 200, 191, 1, 0, 0, 0, 200,
195, 1, 0, 0, 0, 201, 54, 1, 0, 0, 0, 202, 203, 7, 21, 0, 0, 203, 56, 1,
0, 0, 0, 204, 206, 3, 55, 27, 0, 205, 204, 1, 0, 0, 0, 205, 206, 1, 0,
0, 0, 206, 208, 1, 0, 0, 0, 207, 209, 3, 71, 35, 0, 208, 207, 1, 0, 0,
0, 209, 210, 1, 0, 0, 0, 210, 208, 1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211,
219, 1, 0, 0, 0, 212, 216, 5, 46, 0, 0, 213, 215, 3, 71, 35, 0, 214, 213,
1, 0, 0, 0, 215, 218, 1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0,
0, 0, 217, 220, 1, 0, 0, 0, 218, 216, 1, 0, 0, 0, 219, 212, 1, 0, 0, 0,
219, 220, 1, 0, 0, 0, 220, 230, 1, 0, 0, 0, 221, 223, 7, 3, 0, 0, 222,
224, 3, 55, 27, 0, 223, 222, 1, 0, 0, 0, 223, 224, 1, 0, 0, 0, 224, 226,
1, 0, 0, 0, 225, 227, 3, 71, 35, 0, 226, 225, 1, 0, 0, 0, 227, 228, 1,
0, 0, 0, 228, 226, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 231, 1, 0, 0,
0, 230, 221, 1, 0, 0, 0, 230, 231, 1, 0, 0, 0, 231, 253, 1, 0, 0, 0, 232,
234, 3, 55, 27, 0, 233, 232, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235,
1, 0, 0, 0, 235, 237, 5, 46, 0, 0, 236, 238, 3, 71, 35, 0, 237, 236, 1,
0, 0, 0, 238, 239, 1, 0, 0, 0, 239, 237, 1, 0, 0, 0, 239, 240, 1, 0, 0,
0, 240, 250, 1, 0, 0, 0, 241, 243, 7, 3, 0, 0, 242, 244, 3, 55, 27, 0,
243, 242, 1, 0, 0, 0, 243, 244, 1, 0, 0, 0, 244, 246, 1, 0, 0, 0, 245,
247, 3, 71, 35, 0, 246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246,
1, 0, 0, 0, 248, 249, 1, 0, 0, 0, 249, 251, 1, 0, 0, 0, 250, 241, 1, 0,
0, 0, 250, 251, 1, 0, 0, 0, 251, 253, 1, 0, 0, 0, 252, 205, 1, 0, 0, 0,
252, 233, 1, 0, 0, 0, 253, 58, 1, 0, 0, 0, 254, 260, 5, 34, 0, 0, 255,
259, 8, 22, 0, 0, 256, 257, 5, 92, 0, 0, 257, 259, 9, 0, 0, 0, 258, 255,
1, 0, 0, 0, 258, 256, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0,
0, 0, 260, 261, 1, 0, 0, 0, 261, 263, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0,
263, 275, 5, 34, 0, 0, 264, 270, 5, 39, 0, 0, 265, 269, 8, 23, 0, 0, 266,
267, 5, 92, 0, 0, 267, 269, 9, 0, 0, 0, 268, 265, 1, 0, 0, 0, 268, 266,
1, 0, 0, 0, 269, 272, 1, 0, 0, 0, 270, 268, 1, 0, 0, 0, 270, 271, 1, 0,
0, 0, 271, 273, 1, 0, 0, 0, 272, 270, 1, 0, 0, 0, 273, 275, 5, 39, 0, 0,
274, 254, 1, 0, 0, 0, 274, 264, 1, 0, 0, 0, 275, 60, 1, 0, 0, 0, 276, 280,
7, 24, 0, 0, 277, 279, 7, 25, 0, 0, 278, 277, 1, 0, 0, 0, 279, 282, 1,
0, 0, 0, 280, 278, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 62, 1, 0, 0,
0, 282, 280, 1, 0, 0, 0, 283, 284, 5, 91, 0, 0, 284, 285, 5, 93, 0, 0,
285, 64, 1, 0, 0, 0, 286, 287, 5, 91, 0, 0, 287, 288, 5, 42, 0, 0, 288,
289, 5, 93, 0, 0, 289, 66, 1, 0, 0, 0, 290, 297, 3, 61, 30, 0, 291, 292,
5, 46, 0, 0, 292, 296, 3, 61, 30, 0, 293, 296, 3, 63, 31, 0, 294, 296,
3, 65, 32, 0, 295, 291, 1, 0, 0, 0, 295, 293, 1, 0, 0, 0, 295, 294, 1,
0, 0, 0, 296, 299, 1, 0, 0, 0, 297, 295, 1, 0, 0, 0, 297, 298, 1, 0, 0,
0, 298, 68, 1, 0, 0, 0, 299, 297, 1, 0, 0, 0, 300, 302, 7, 26, 0, 0, 301,
300, 1, 0, 0, 0, 302, 303, 1, 0, 0, 0, 303, 301, 1, 0, 0, 0, 303, 304,
1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 306, 6, 34, 0, 0, 306, 70, 1, 0,
0, 0, 307, 308, 7, 27, 0, 0, 308, 72, 1, 0, 0, 0, 309, 311, 8, 28, 0, 0,
310, 309, 1, 0, 0, 0, 311, 312, 1, 0, 0, 0, 312, 310, 1, 0, 0, 0, 312,
313, 1, 0, 0, 0, 313, 74, 1, 0, 0, 0, 28, 0, 88, 131, 148, 200, 205, 210,
216, 219, 223, 228, 230, 233, 239, 243, 248, 250, 252, 258, 260, 268, 270,
274, 280, 295, 297, 303, 312, 1, 6, 0, 0,
1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1,
33, 1, 33, 4, 33, 298, 8, 33, 11, 33, 12, 33, 299, 5, 33, 302, 8, 33, 10,
33, 12, 33, 305, 9, 33, 1, 34, 4, 34, 308, 8, 34, 11, 34, 12, 34, 309,
1, 34, 1, 34, 1, 35, 1, 35, 1, 36, 4, 36, 317, 8, 36, 11, 36, 12, 36, 318,
0, 0, 37, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37,
19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55,
0, 57, 28, 59, 29, 61, 0, 63, 0, 65, 0, 67, 30, 69, 31, 71, 0, 73, 32,
1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0, 75, 75,
107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84, 84, 116,
116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88, 88, 120,
120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71, 71, 103,
103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79, 111, 111,
2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104, 104, 2,
0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102, 102, 2,
0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92, 4, 0, 35,
36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64, 90, 95,
95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 8, 0,
9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 344, 0,
1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0,
9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0,
0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0,
0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0,
0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1,
0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47,
1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0,
57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0,
0, 73, 1, 0, 0, 0, 1, 75, 1, 0, 0, 0, 3, 77, 1, 0, 0, 0, 5, 79, 1, 0, 0,
0, 7, 81, 1, 0, 0, 0, 9, 83, 1, 0, 0, 0, 11, 88, 1, 0, 0, 0, 13, 90, 1,
0, 0, 0, 15, 93, 1, 0, 0, 0, 17, 96, 1, 0, 0, 0, 19, 98, 1, 0, 0, 0, 21,
101, 1, 0, 0, 0, 23, 103, 1, 0, 0, 0, 25, 106, 1, 0, 0, 0, 27, 111, 1,
0, 0, 0, 29, 117, 1, 0, 0, 0, 31, 125, 1, 0, 0, 0, 33, 133, 1, 0, 0, 0,
35, 140, 1, 0, 0, 0, 37, 150, 1, 0, 0, 0, 39, 153, 1, 0, 0, 0, 41, 157,
1, 0, 0, 0, 43, 161, 1, 0, 0, 0, 45, 164, 1, 0, 0, 0, 47, 173, 1, 0, 0,
0, 49, 177, 1, 0, 0, 0, 51, 184, 1, 0, 0, 0, 53, 200, 1, 0, 0, 0, 55, 202,
1, 0, 0, 0, 57, 252, 1, 0, 0, 0, 59, 274, 1, 0, 0, 0, 61, 276, 1, 0, 0,
0, 63, 283, 1, 0, 0, 0, 65, 286, 1, 0, 0, 0, 67, 290, 1, 0, 0, 0, 69, 307,
1, 0, 0, 0, 71, 313, 1, 0, 0, 0, 73, 316, 1, 0, 0, 0, 75, 76, 5, 40, 0,
0, 76, 2, 1, 0, 0, 0, 77, 78, 5, 41, 0, 0, 78, 4, 1, 0, 0, 0, 79, 80, 5,
91, 0, 0, 80, 6, 1, 0, 0, 0, 81, 82, 5, 93, 0, 0, 82, 8, 1, 0, 0, 0, 83,
84, 5, 44, 0, 0, 84, 10, 1, 0, 0, 0, 85, 89, 5, 61, 0, 0, 86, 87, 5, 61,
0, 0, 87, 89, 5, 61, 0, 0, 88, 85, 1, 0, 0, 0, 88, 86, 1, 0, 0, 0, 89,
12, 1, 0, 0, 0, 90, 91, 5, 33, 0, 0, 91, 92, 5, 61, 0, 0, 92, 14, 1, 0,
0, 0, 93, 94, 5, 60, 0, 0, 94, 95, 5, 62, 0, 0, 95, 16, 1, 0, 0, 0, 96,
97, 5, 60, 0, 0, 97, 18, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 100, 5, 61,
0, 0, 100, 20, 1, 0, 0, 0, 101, 102, 5, 62, 0, 0, 102, 22, 1, 0, 0, 0,
103, 104, 5, 62, 0, 0, 104, 105, 5, 61, 0, 0, 105, 24, 1, 0, 0, 0, 106,
107, 7, 0, 0, 0, 107, 108, 7, 1, 0, 0, 108, 109, 7, 2, 0, 0, 109, 110,
7, 3, 0, 0, 110, 26, 1, 0, 0, 0, 111, 112, 7, 1, 0, 0, 112, 113, 7, 0,
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 2, 0, 0, 115, 116, 7, 3, 0, 0,
116, 28, 1, 0, 0, 0, 117, 118, 7, 4, 0, 0, 118, 119, 7, 3, 0, 0, 119, 120,
7, 5, 0, 0, 120, 121, 7, 6, 0, 0, 121, 122, 7, 3, 0, 0, 122, 123, 7, 3,
0, 0, 123, 124, 7, 7, 0, 0, 124, 30, 1, 0, 0, 0, 125, 126, 7, 3, 0, 0,
126, 127, 7, 8, 0, 0, 127, 128, 7, 1, 0, 0, 128, 129, 7, 9, 0, 0, 129,
131, 7, 5, 0, 0, 130, 132, 7, 9, 0, 0, 131, 130, 1, 0, 0, 0, 131, 132,
1, 0, 0, 0, 132, 32, 1, 0, 0, 0, 133, 134, 7, 10, 0, 0, 134, 135, 7, 3,
0, 0, 135, 136, 7, 11, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 8, 0, 0,
138, 139, 7, 12, 0, 0, 139, 34, 1, 0, 0, 0, 140, 141, 7, 13, 0, 0, 141,
142, 7, 14, 0, 0, 142, 143, 7, 7, 0, 0, 143, 144, 7, 5, 0, 0, 144, 145,
7, 15, 0, 0, 145, 146, 7, 1, 0, 0, 146, 148, 7, 7, 0, 0, 147, 149, 7, 9,
0, 0, 148, 147, 1, 0, 0, 0, 148, 149, 1, 0, 0, 0, 149, 36, 1, 0, 0, 0,
150, 151, 7, 1, 0, 0, 151, 152, 7, 7, 0, 0, 152, 38, 1, 0, 0, 0, 153, 154,
7, 7, 0, 0, 154, 155, 7, 14, 0, 0, 155, 156, 7, 5, 0, 0, 156, 40, 1, 0,
0, 0, 157, 158, 7, 15, 0, 0, 158, 159, 7, 7, 0, 0, 159, 160, 7, 16, 0,
0, 160, 42, 1, 0, 0, 0, 161, 162, 7, 14, 0, 0, 162, 163, 7, 10, 0, 0, 163,
44, 1, 0, 0, 0, 164, 165, 7, 17, 0, 0, 165, 166, 7, 15, 0, 0, 166, 167,
7, 9, 0, 0, 167, 168, 7, 5, 0, 0, 168, 169, 7, 14, 0, 0, 169, 170, 7, 2,
0, 0, 170, 171, 7, 3, 0, 0, 171, 172, 7, 7, 0, 0, 172, 46, 1, 0, 0, 0,
173, 174, 7, 17, 0, 0, 174, 175, 7, 15, 0, 0, 175, 176, 7, 9, 0, 0, 176,
48, 1, 0, 0, 0, 177, 178, 7, 17, 0, 0, 178, 179, 7, 15, 0, 0, 179, 180,
7, 9, 0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 7, 0, 0, 182, 183, 7, 18,
0, 0, 183, 50, 1, 0, 0, 0, 184, 185, 7, 17, 0, 0, 185, 186, 7, 15, 0, 0,
186, 187, 7, 9, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 0, 0, 0, 189,
190, 7, 0, 0, 0, 190, 52, 1, 0, 0, 0, 191, 192, 7, 5, 0, 0, 192, 193, 7,
10, 0, 0, 193, 194, 7, 19, 0, 0, 194, 201, 7, 3, 0, 0, 195, 196, 7, 20,
0, 0, 196, 197, 7, 15, 0, 0, 197, 198, 7, 0, 0, 0, 198, 199, 7, 9, 0, 0,
199, 201, 7, 3, 0, 0, 200, 191, 1, 0, 0, 0, 200, 195, 1, 0, 0, 0, 201,
54, 1, 0, 0, 0, 202, 203, 7, 21, 0, 0, 203, 56, 1, 0, 0, 0, 204, 206, 3,
55, 27, 0, 205, 204, 1, 0, 0, 0, 205, 206, 1, 0, 0, 0, 206, 208, 1, 0,
0, 0, 207, 209, 3, 71, 35, 0, 208, 207, 1, 0, 0, 0, 209, 210, 1, 0, 0,
0, 210, 208, 1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211, 219, 1, 0, 0, 0, 212,
216, 5, 46, 0, 0, 213, 215, 3, 71, 35, 0, 214, 213, 1, 0, 0, 0, 215, 218,
1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 220, 1, 0,
0, 0, 218, 216, 1, 0, 0, 0, 219, 212, 1, 0, 0, 0, 219, 220, 1, 0, 0, 0,
220, 230, 1, 0, 0, 0, 221, 223, 7, 3, 0, 0, 222, 224, 3, 55, 27, 0, 223,
222, 1, 0, 0, 0, 223, 224, 1, 0, 0, 0, 224, 226, 1, 0, 0, 0, 225, 227,
3, 71, 35, 0, 226, 225, 1, 0, 0, 0, 227, 228, 1, 0, 0, 0, 228, 226, 1,
0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 231, 1, 0, 0, 0, 230, 221, 1, 0, 0,
0, 230, 231, 1, 0, 0, 0, 231, 253, 1, 0, 0, 0, 232, 234, 3, 55, 27, 0,
233, 232, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235, 1, 0, 0, 0, 235,
237, 5, 46, 0, 0, 236, 238, 3, 71, 35, 0, 237, 236, 1, 0, 0, 0, 238, 239,
1, 0, 0, 0, 239, 237, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0, 240, 250, 1, 0,
0, 0, 241, 243, 7, 3, 0, 0, 242, 244, 3, 55, 27, 0, 243, 242, 1, 0, 0,
0, 243, 244, 1, 0, 0, 0, 244, 246, 1, 0, 0, 0, 245, 247, 3, 71, 35, 0,
246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1, 0, 0, 0, 248,
249, 1, 0, 0, 0, 249, 251, 1, 0, 0, 0, 250, 241, 1, 0, 0, 0, 250, 251,
1, 0, 0, 0, 251, 253, 1, 0, 0, 0, 252, 205, 1, 0, 0, 0, 252, 233, 1, 0,
0, 0, 253, 58, 1, 0, 0, 0, 254, 260, 5, 34, 0, 0, 255, 259, 8, 22, 0, 0,
256, 257, 5, 92, 0, 0, 257, 259, 9, 0, 0, 0, 258, 255, 1, 0, 0, 0, 258,
256, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 260, 261,
1, 0, 0, 0, 261, 263, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0, 263, 275, 5, 34,
0, 0, 264, 270, 5, 39, 0, 0, 265, 269, 8, 23, 0, 0, 266, 267, 5, 92, 0,
0, 267, 269, 9, 0, 0, 0, 268, 265, 1, 0, 0, 0, 268, 266, 1, 0, 0, 0, 269,
272, 1, 0, 0, 0, 270, 268, 1, 0, 0, 0, 270, 271, 1, 0, 0, 0, 271, 273,
1, 0, 0, 0, 272, 270, 1, 0, 0, 0, 273, 275, 5, 39, 0, 0, 274, 254, 1, 0,
0, 0, 274, 264, 1, 0, 0, 0, 275, 60, 1, 0, 0, 0, 276, 280, 7, 24, 0, 0,
277, 279, 7, 25, 0, 0, 278, 277, 1, 0, 0, 0, 279, 282, 1, 0, 0, 0, 280,
278, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 62, 1, 0, 0, 0, 282, 280, 1,
0, 0, 0, 283, 284, 5, 91, 0, 0, 284, 285, 5, 93, 0, 0, 285, 64, 1, 0, 0,
0, 286, 287, 5, 91, 0, 0, 287, 288, 5, 42, 0, 0, 288, 289, 5, 93, 0, 0,
289, 66, 1, 0, 0, 0, 290, 303, 3, 61, 30, 0, 291, 292, 5, 46, 0, 0, 292,
302, 3, 61, 30, 0, 293, 302, 3, 63, 31, 0, 294, 302, 3, 65, 32, 0, 295,
297, 5, 46, 0, 0, 296, 298, 3, 71, 35, 0, 297, 296, 1, 0, 0, 0, 298, 299,
1, 0, 0, 0, 299, 297, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 302, 1, 0,
0, 0, 301, 291, 1, 0, 0, 0, 301, 293, 1, 0, 0, 0, 301, 294, 1, 0, 0, 0,
301, 295, 1, 0, 0, 0, 302, 305, 1, 0, 0, 0, 303, 301, 1, 0, 0, 0, 303,
304, 1, 0, 0, 0, 304, 68, 1, 0, 0, 0, 305, 303, 1, 0, 0, 0, 306, 308, 7,
26, 0, 0, 307, 306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 307, 1, 0, 0,
0, 309, 310, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 312, 6, 34, 0, 0, 312,
70, 1, 0, 0, 0, 313, 314, 7, 27, 0, 0, 314, 72, 1, 0, 0, 0, 315, 317, 8,
28, 0, 0, 316, 315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0,
0, 318, 319, 1, 0, 0, 0, 319, 74, 1, 0, 0, 0, 29, 0, 88, 131, 148, 200,
205, 210, 216, 219, 223, 228, 230, 233, 239, 243, 248, 250, 252, 258, 260,
268, 270, 274, 280, 299, 301, 303, 309, 318, 1, 6, 0, 0,
}
deserializer := antlr.NewATNDeserializer(nil)
staticData.atn = deserializer.Deserialize(staticData.serializedATN)

View File

@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/model"
@ -87,6 +88,8 @@ type BaseRule struct {
TemporalityMap map[string]map[v3.Temporality]bool
sqlstore sqlstore.SQLStore
evaluation ruletypes.Evaluation
}
type RuleOption func(*BaseRule)
@ -129,6 +132,10 @@ func NewBaseRule(id string, orgID valuer.UUID, p *ruletypes.PostableRule, reader
if err != nil {
return nil, err
}
evaluation, err := p.Evaluation.GetEvaluation()
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to get evaluation: %v", err)
}
baseRule := &BaseRule{
id: id,
@ -146,6 +153,7 @@ func NewBaseRule(id string, orgID valuer.UUID, p *ruletypes.PostableRule, reader
reader: reader,
TemporalityMap: make(map[string]map[v3.Temporality]bool),
Threshold: threshold,
evaluation: evaluation,
}
if baseRule.evalWindow == 0 {
@ -248,8 +256,10 @@ func (r *BaseRule) Unit() string {
}
func (r *BaseRule) Timestamps(ts time.Time) (time.Time, time.Time) {
start := ts.Add(-time.Duration(r.evalWindow)).UnixMilli()
end := ts.UnixMilli()
st, en := r.evaluation.NextWindowFor(ts)
start := st.UnixMilli()
end := en.UnixMilli()
if r.evalDelay > 0 {
start = start - int64(r.evalDelay.Milliseconds())

View File

@ -12,12 +12,11 @@ import (
"go.uber.org/zap"
"errors"
"github.com/go-openapi/strfmt"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/prometheus"
querierV5 "github.com/SigNoz/signoz/pkg/querier"
@ -147,6 +146,12 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
var task Task
ruleId := RuleIdFromTaskName(opts.TaskName)
evaluation, err := opts.Rule.Evaluation.GetEvaluation()
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := NewThresholdRule(
@ -167,7 +172,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
rules = append(rules, tr)
// create ch rule task for evalution
task = newTask(TaskTypeCh, opts.TaskName, taskNamesuffix, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(TaskTypeCh, opts.TaskName, taskNamesuffix, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
@ -189,7 +194,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
rules = append(rules, pr)
// create promql rule task for evalution
task = newTask(TaskTypeProm, opts.TaskName, taskNamesuffix, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(TaskTypeProm, opts.TaskName, taskNamesuffix, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else {
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
@ -400,7 +405,7 @@ func (m *Manager) editTask(_ context.Context, orgID valuer.UUID, rule *ruletypes
if err != nil {
zap.L().Error("loading tasks failed", zap.Error(err))
return errors.New("error preparing rule with given parameters, previous rule set restored")
return errors.NewInvalidInputf(errors.CodeInvalidInput, "error preparing rule with given parameters, previous rule set restored")
}
for _, r := range newTask.Rules() {
@ -593,7 +598,7 @@ func (m *Manager) addTask(_ context.Context, orgID valuer.UUID, rule *ruletypes.
if err != nil {
zap.L().Error("creating rule task failed", zap.String("name", taskName), zap.Error(err))
return errors.New("error loading rules, previous rule set restored")
return errors.NewInvalidInputf(errors.CodeInvalidInput, "error loading rules, previous rule set restored")
}
for _, r := range newTask.Rules() {

View File

@ -123,8 +123,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
prevState := r.State()
start := ts.Add(-r.evalWindow)
end := ts
start, end := r.Timestamps(ts)
interval := 60 * time.Second // TODO(srikanthccv): this should be configurable
valueFormatter := formatter.FromUnit(r.Unit())

View File

@ -25,11 +25,13 @@ func getVectorValues(vectors []ruletypes.Sample) []float64 {
func TestPromRuleShouldAlert(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Test Rule",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeProm,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Test Rule",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeProm,
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypePromQL,

View File

@ -31,11 +31,13 @@ import (
func TestThresholdRuleShouldAlert(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -886,11 +888,13 @@ func TestNormalizeLabelName(t *testing.T) {
func TestPrepareLinksToLogs(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -938,11 +942,13 @@ func TestPrepareLinksToLogs(t *testing.T) {
func TestPrepareLinksToLogsV5(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -997,11 +1003,13 @@ func TestPrepareLinksToLogsV5(t *testing.T) {
func TestPrepareLinksToTracesV5(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeTraces,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeTraces,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -1056,11 +1064,13 @@ func TestPrepareLinksToTracesV5(t *testing.T) {
func TestPrepareLinksToTraces(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Links to traces test",
AlertType: ruletypes.AlertTypeTraces,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Links to traces test",
AlertType: ruletypes.AlertTypeTraces,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -1108,11 +1118,13 @@ func TestPrepareLinksToTraces(t *testing.T) {
func TestThresholdRuleLabelNormalization(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -1214,11 +1226,13 @@ func TestThresholdRuleLabelNormalization(t *testing.T) {
func TestThresholdRuleEvalDelay(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Test Eval Delay",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Test Eval Delay",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeClickHouseSQL,
@ -1275,11 +1289,13 @@ func TestThresholdRuleEvalDelay(t *testing.T) {
func TestThresholdRuleClickHouseTmpl(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeClickHouseSQL,
@ -1342,11 +1358,13 @@ func (m *queryMatcherAny) Match(x string, y string) error {
func TestThresholdRuleUnitCombinations(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Units test",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Units test",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -1535,11 +1553,13 @@ func TestThresholdRuleUnitCombinations(t *testing.T) {
func TestThresholdRuleNoData(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "No data test",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "No data test",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -1638,11 +1658,13 @@ func TestThresholdRuleNoData(t *testing.T) {
func TestThresholdRuleTracesLink(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Traces link test",
AlertType: ruletypes.AlertTypeTraces,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Traces link test",
AlertType: ruletypes.AlertTypeTraces,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -1763,11 +1785,13 @@ func TestThresholdRuleTracesLink(t *testing.T) {
func TestThresholdRuleLogsLink(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Logs link test",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Logs link test",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
@ -1901,11 +1925,13 @@ func TestThresholdRuleLogsLink(t *testing.T) {
func TestThresholdRuleShiftBy(t *testing.T) {
target := float64(10)
postableRule := ruletypes.PostableRule{
AlertName: "Logs link test",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Logs link test",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
Thresholds: &ruletypes.RuleThresholdData{
Kind: ruletypes.BasicThresholdKind,
@ -1973,11 +1999,13 @@ func TestThresholdRuleShiftBy(t *testing.T) {
func TestMultipleThresholdRule(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Mulitple threshold test",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
AlertName: "Mulitple threshold test",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,

View File

@ -1,15 +1 @@
package querybuilder
import (
"os"
"strings"
)
var QBV5Enabled = false
func init() {
v := os.Getenv("ENABLE_QB_V5")
if strings.ToLower(v) == "true" || strings.ToLower(v) == "1" {
QBV5Enabled = true
}
}

View File

@ -22,21 +22,20 @@ func NewConditionBuilder(fm qbtypes.FieldMapper) *defaultConditionBuilder {
func valueForIndexFilter(op qbtypes.FilterOperator, key *telemetrytypes.TelemetryFieldKey, value any) any {
switch v := value.(type) {
case string:
if op == qbtypes.FilterOperatorEqual || op == qbtypes.FilterOperatorNotEqual {
return fmt.Sprintf(`%%%s":"%s%%`, key.Name, v)
}
return fmt.Sprintf(`%%%s%%%s%%`, key.Name, v)
case []any:
// assuming array will always be for in and not in
values := make([]string, 0, len(v))
for _, v := range v {
values = append(values, fmt.Sprintf(`%%%s":"%s%%`, key.Name, v))
values = append(values, fmt.Sprintf(`%%%s":"%s%%`, key.Name, querybuilder.FormatValueForContains(v)))
}
return values
default:
// format to string for anything else as we store resource values as string
if op == qbtypes.FilterOperatorEqual || op == qbtypes.FilterOperatorNotEqual {
return fmt.Sprintf(`%%%s":"%s%%`, key.Name, querybuilder.FormatValueForContains(v))
}
return fmt.Sprintf(`%%%s%%%s%%`, key.Name, querybuilder.FormatValueForContains(v))
}
// resource table expects string value
return fmt.Sprintf(`%%%v%%`, value)
}
func keyIndexFilter(key *telemetrytypes.TelemetryFieldKey) any {
@ -55,15 +54,9 @@ func (b *defaultConditionBuilder) ConditionFor(
return "true", nil
}
switch op {
case qbtypes.FilterOperatorContains,
qbtypes.FilterOperatorNotContains,
qbtypes.FilterOperatorILike,
qbtypes.FilterOperatorNotILike,
qbtypes.FilterOperatorLike,
qbtypes.FilterOperatorNotLike:
value = querybuilder.FormatValueForContains(value)
}
// except for in, not in, between, not between all other operators should have formatted value
// as we store resource values as string
formattedValue := querybuilder.FormatValueForContains(value)
column, err := b.fm.ColumnFor(ctx, key)
if err != nil {
@ -81,34 +74,34 @@ func (b *defaultConditionBuilder) ConditionFor(
switch op {
case qbtypes.FilterOperatorEqual:
return sb.And(
sb.E(fieldName, value),
sb.E(fieldName, formattedValue),
keyIdxFilter,
sb.Like(column.Name, valueForIndexFilter),
), nil
case qbtypes.FilterOperatorNotEqual:
return sb.And(
sb.NE(fieldName, value),
sb.NE(fieldName, formattedValue),
sb.NotLike(column.Name, valueForIndexFilter),
), nil
case qbtypes.FilterOperatorGreaterThan:
return sb.And(sb.GT(fieldName, value), keyIdxFilter), nil
return sb.And(sb.GT(fieldName, formattedValue), keyIdxFilter), nil
case qbtypes.FilterOperatorGreaterThanOrEq:
return sb.And(sb.GE(fieldName, value), keyIdxFilter), nil
return sb.And(sb.GE(fieldName, formattedValue), keyIdxFilter), nil
case qbtypes.FilterOperatorLessThan:
return sb.And(sb.LT(fieldName, value), keyIdxFilter), nil
return sb.And(sb.LT(fieldName, formattedValue), keyIdxFilter), nil
case qbtypes.FilterOperatorLessThanOrEq:
return sb.And(sb.LE(fieldName, value), keyIdxFilter), nil
return sb.And(sb.LE(fieldName, formattedValue), keyIdxFilter), nil
case qbtypes.FilterOperatorLike, qbtypes.FilterOperatorILike:
return sb.And(
sb.ILike(fieldName, value),
sb.ILike(fieldName, formattedValue),
keyIdxFilter,
sb.ILike(column.Name, valueForIndexFilter),
), nil
case qbtypes.FilterOperatorNotLike, qbtypes.FilterOperatorNotILike:
// no index filter: as cannot apply `not contains x%y` as y can be somewhere else
return sb.And(
sb.NotILike(fieldName, value),
sb.NotILike(fieldName, formattedValue),
), nil
case qbtypes.FilterOperatorBetween:
@ -119,7 +112,7 @@ func (b *defaultConditionBuilder) ConditionFor(
if len(values) != 2 {
return "", qbtypes.ErrBetweenValues
}
return sb.And(keyIdxFilter, sb.Between(fieldName, values[0], values[1])), nil
return sb.And(keyIdxFilter, sb.Between(fieldName, querybuilder.FormatValueForContains(values[0]), querybuilder.FormatValueForContains(values[1]))), nil
case qbtypes.FilterOperatorNotBetween:
values, ok := value.([]any)
if !ok {
@ -128,7 +121,7 @@ func (b *defaultConditionBuilder) ConditionFor(
if len(values) != 2 {
return "", qbtypes.ErrBetweenValues
}
return sb.And(sb.NotBetween(fieldName, values[0], values[1])), nil
return sb.And(sb.NotBetween(fieldName, querybuilder.FormatValueForContains(values[0]), querybuilder.FormatValueForContains(values[1]))), nil
case qbtypes.FilterOperatorIn:
values, ok := value.([]any)
@ -137,7 +130,7 @@ func (b *defaultConditionBuilder) ConditionFor(
}
inConditions := make([]string, 0, len(values))
for _, v := range values {
inConditions = append(inConditions, sb.E(fieldName, v))
inConditions = append(inConditions, sb.E(fieldName, querybuilder.FormatValueForContains(v)))
}
mainCondition := sb.Or(inConditions...)
valConditions := make([]string, 0, len(values))
@ -156,7 +149,7 @@ func (b *defaultConditionBuilder) ConditionFor(
}
notInConditions := make([]string, 0, len(values))
for _, v := range values {
notInConditions = append(notInConditions, sb.NE(fieldName, v))
notInConditions = append(notInConditions, sb.NE(fieldName, querybuilder.FormatValueForContains(v)))
}
mainCondition := sb.And(notInConditions...)
valConditions := make([]string, 0, len(values))
@ -180,24 +173,24 @@ func (b *defaultConditionBuilder) ConditionFor(
case qbtypes.FilterOperatorRegexp:
return sb.And(
fmt.Sprintf("match(%s, %s)", fieldName, sb.Var(value)),
fmt.Sprintf("match(%s, %s)", fieldName, sb.Var(formattedValue)),
keyIdxFilter,
), nil
case qbtypes.FilterOperatorNotRegexp:
return sb.And(
fmt.Sprintf("NOT match(%s, %s)", fieldName, sb.Var(value)),
fmt.Sprintf("NOT match(%s, %s)", fieldName, sb.Var(formattedValue)),
), nil
case qbtypes.FilterOperatorContains:
return sb.And(
sb.ILike(fieldName, fmt.Sprintf(`%%%s%%`, value)),
sb.ILike(fieldName, fmt.Sprintf(`%%%s%%`, formattedValue)),
keyIdxFilter,
sb.ILike(column.Name, valueForIndexFilter),
), nil
case qbtypes.FilterOperatorNotContains:
// no index filter: as cannot apply `not contains x%y` as y can be somewhere else
return sb.And(
sb.NotILike(fieldName, fmt.Sprintf(`%%%s%%`, value)),
sb.NotILike(fieldName, fmt.Sprintf(`%%%s%%`, formattedValue)),
), nil
}
return "", qbtypes.ErrUnsupportedOperator

View File

@ -143,6 +143,61 @@ func TestConditionBuilder(t *testing.T) {
expected: "simpleJSONHas(labels, 'k8s.namespace.name') <> ?",
expectedArgs: []any{true},
},
{
name: "number_equals",
key: &telemetrytypes.TelemetryFieldKey{
Name: "test_num",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorEqual,
value: 1,
expected: "simpleJSONExtractString(labels, 'test_num') = ? AND labels LIKE ? AND labels LIKE ?",
expectedArgs: []any{"1", "%test_num%", "%test_num\":\"1%"},
},
{
name: "number_gt",
key: &telemetrytypes.TelemetryFieldKey{
Name: "test_num",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorGreaterThan,
value: 1,
expected: "simpleJSONExtractString(labels, 'test_num') > ? AND labels LIKE ?",
expectedArgs: []any{"1", "%test_num%"},
},
{
name: "number_in",
key: &telemetrytypes.TelemetryFieldKey{
Name: "test_num",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorIn,
value: []any{1, 2},
expected: "(simpleJSONExtractString(labels, 'test_num') = ? OR simpleJSONExtractString(labels, 'test_num') = ?) AND labels LIKE ? AND (labels LIKE ? OR labels LIKE ?)",
expectedArgs: []any{"1", "2", "%test_num%", "%test_num\":\"1%", "%test_num\":\"2%"},
},
{
name: "number_between",
key: &telemetrytypes.TelemetryFieldKey{
Name: "test_num",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorBetween,
value: []any{1, 2},
expected: "labels LIKE ? AND simpleJSONExtractString(labels, 'test_num') BETWEEN ? AND ?",
expectedArgs: []any{"%test_num%", "1", "2"},
},
{
name: "string_regexp",
key: &telemetrytypes.TelemetryFieldKey{
Name: "k8s.namespace.name",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorRegexp,
value: "ban.*",
expected: "match(simpleJSONExtractString(labels, 'k8s.namespace.name'), ?) AND labels LIKE ?",
expectedArgs: []any{"ban.*", "%k8s.namespace.name%"},
},
}
fm := NewFieldMapper()

View File

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

View File

@ -44,6 +44,7 @@ var (
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
ValueType: schema.ColumnTypeString,
}},
"resource": {Name: "resource", Type: schema.JSONColumnType{}},
"scope_name": {Name: "scope_name", Type: schema.ColumnTypeString},
"scope_version": {Name: "scope_version", Type: schema.ColumnTypeString},
"scope_string": {Name: "scope_string", Type: schema.MapColumnType{
@ -53,7 +54,8 @@ var (
}
)
type fieldMapper struct{}
type fieldMapper struct {
}
func NewFieldMapper() qbtypes.FieldMapper {
return &fieldMapper{}
@ -62,7 +64,7 @@ func NewFieldMapper() qbtypes.FieldMapper {
func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
switch key.FieldContext {
case telemetrytypes.FieldContextResource:
return logsV2Columns["resources_string"], nil
return logsV2Columns["resource"], nil
case telemetrytypes.FieldContextScope:
switch key.Name {
case "name", "scope.name", "scope_name":
@ -102,6 +104,24 @@ func (m *fieldMapper) FieldFor(ctx context.Context, key *telemetrytypes.Telemetr
}
switch column.Type {
case schema.JSONColumnType{}:
// json is only supported for resource context as of now
if key.FieldContext != telemetrytypes.FieldContextResource {
return "", errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "only resource context fields are supported for json columns, got %s", key.FieldContext.String)
}
oldColumn := logsV2Columns["resources_string"]
oldKeyName := fmt.Sprintf("%s['%s']", oldColumn.Name, key.Name)
// have to add ::string as clickHouse throws an error :- data types Variant/Dynamic are not allowed in GROUP BY
// once clickHouse dependency is updated, we need to check if we can remove it.
if key.Materialized {
oldKeyName = telemetrytypes.FieldKeyToMaterializedColumnName(key)
oldKeyNameExists := telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, %s==true, %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldKeyNameExists, oldKeyName), nil
} else {
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
}
case schema.ColumnTypeString,
schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
schema.ColumnTypeUInt64,

View File

@ -26,7 +26,7 @@ func TestGetColumn(t *testing.T) {
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
expectedCol: logsV2Columns["resources_string"],
expectedCol: logsV2Columns["resource"],
expectedError: nil,
},
{
@ -234,7 +234,18 @@ func TestGetFieldKeyName(t *testing.T) {
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
expectedResult: "resources_string['service.name']",
expectedResult: "multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL)",
expectedError: nil,
},
{
name: "Map column type - resource attribute - Materialized",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
Materialized: true,
},
expectedResult: "multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, `resource_string_service$$name_exists`==true, `resource_string_service$$name`, NULL)",
expectedError: nil,
},
{
@ -248,10 +259,9 @@ func TestGetFieldKeyName(t *testing.T) {
},
}
fm := NewFieldMapper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fm := NewFieldMapper()
result, err := fm.FieldFor(ctx, &tc.key)
if tc.expectedError != nil {

View File

@ -121,6 +121,38 @@ func TestFilterExprLogs(t *testing.T) {
expectedErrorContains: "",
},
{
category: "Key with curly brace",
query: `{UserId} = "U101"`,
shouldPass: true,
expectedQuery: "WHERE (attributes_string['{UserId}'] = ? AND mapContains(attributes_string, '{UserId}') = ?)",
expectedArgs: []any{"U101", true},
expectedErrorContains: "",
},
{
category: "Key with @symbol",
query: `user@email = "u@example.com"`,
shouldPass: true,
expectedQuery: "WHERE (attributes_string['user@email'] = ? AND mapContains(attributes_string, 'user@email') = ?)",
expectedArgs: []any{"u@example.com", true},
expectedErrorContains: "",
},
{
category: "Key with @symbol",
query: `#user_name = "anon42069"`,
shouldPass: true,
expectedQuery: "WHERE (attributes_string['#user_name'] = ? AND mapContains(attributes_string, '#user_name') = ?)",
expectedArgs: []any{"anon42069", true},
expectedErrorContains: "",
},
{
category: "Key with @symbol",
query: `gen_ai.completion.0.content = "जब तक इस देश में सिनेमा है"`,
shouldPass: true,
expectedQuery: "WHERE (attributes_string['gen_ai.completion.0.content'] = ? AND mapContains(attributes_string, 'gen_ai.completion.0.content') = ?)",
expectedArgs: []any{"जब तक इस देश में सिनेमा है", true},
expectedErrorContains: "",
},
// Searches with special characters
{
category: "Special characters",
@ -420,8 +452,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with conditions",
query: "error service.name=authentication",
shouldPass: true,
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{"error", "authentication", true},
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{"error", "authentication", "NULL"},
expectedErrorContains: "",
},
@ -778,8 +810,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Basic equality",
query: "service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)",
expectedArgs: []any{"api", true},
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@ -844,7 +876,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Not equals",
query: "service.name!=\"api\"",
shouldPass: true,
expectedQuery: "WHERE resources_string['service.name'] <> ?",
expectedQuery: "WHERE multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?",
expectedArgs: []any{"api"},
expectedErrorContains: "",
},
@ -1138,16 +1170,16 @@ func TestFilterExprLogs(t *testing.T) {
category: "IN operator (parentheses)",
query: "service.name IN (\"api\", \"web\", \"auth\")",
shouldPass: true,
expectedQuery: "WHERE ((resources_string['service.name'] = ? OR resources_string['service.name'] = ? OR resources_string['service.name'] = ?) AND mapContains(resources_string, 'service.name') = ?)",
expectedArgs: []any{"api", "web", "auth", true},
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ?) AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "web", "auth", "NULL"},
expectedErrorContains: "",
},
{
category: "IN operator (parentheses)",
query: "environment IN (\"dev\", \"test\", \"staging\", \"prod\")",
shouldPass: true,
expectedQuery: "WHERE ((resources_string['environment'] = ? OR resources_string['environment'] = ? OR resources_string['environment'] = ? OR resources_string['environment'] = ?) AND mapContains(resources_string, 'environment') = ?)",
expectedArgs: []any{"dev", "test", "staging", "prod", true},
expectedQuery: "WHERE ((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ?) AND multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ?)",
expectedArgs: []any{"dev", "test", "staging", "prod", "NULL"},
expectedErrorContains: "",
},
@ -1172,16 +1204,16 @@ func TestFilterExprLogs(t *testing.T) {
category: "IN operator (brackets)",
query: "service.name IN [\"api\", \"web\", \"auth\"]",
shouldPass: true,
expectedQuery: "WHERE ((resources_string['service.name'] = ? OR resources_string['service.name'] = ? OR resources_string['service.name'] = ?) AND mapContains(resources_string, 'service.name') = ?)",
expectedArgs: []any{"api", "web", "auth", true},
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ?) AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "web", "auth", "NULL"},
expectedErrorContains: "",
},
{
category: "IN operator (brackets)",
query: "environment IN [\"dev\", \"test\", \"staging\", \"prod\"]",
shouldPass: true,
expectedQuery: "WHERE ((resources_string['environment'] = ? OR resources_string['environment'] = ? OR resources_string['environment'] = ? OR resources_string['environment'] = ?) AND mapContains(resources_string, 'environment') = ?)",
expectedArgs: []any{"dev", "test", "staging", "prod", true},
expectedQuery: "WHERE ((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? OR multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ?) AND multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ?)",
expectedArgs: []any{"dev", "test", "staging", "prod", "NULL"},
expectedErrorContains: "",
},
@ -1206,7 +1238,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "NOT IN operator (parentheses)",
query: "service.name NOT IN (\"database\", \"cache\")",
shouldPass: true,
expectedQuery: "WHERE (resources_string['service.name'] <> ? AND resources_string['service.name'] <> ?)",
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"database", "cache"},
expectedErrorContains: "",
},
@ -1214,7 +1246,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "NOT IN operator (parentheses)",
query: "environment NOT IN (\"prod\")",
shouldPass: true,
expectedQuery: "WHERE (resources_string['environment'] <> ?)",
expectedQuery: "WHERE (multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ?)",
expectedArgs: []any{"prod"},
expectedErrorContains: "",
},
@ -1240,7 +1272,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "NOT IN operator (brackets)",
query: "service.name NOT IN [\"database\", \"cache\"]",
shouldPass: true,
expectedQuery: "WHERE (resources_string['service.name'] <> ? AND resources_string['service.name'] <> ?)",
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"database", "cache"},
expectedErrorContains: "",
},
@ -1248,7 +1280,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "NOT IN operator (brackets)",
query: "environment NOT IN [\"prod\"]",
shouldPass: true,
expectedQuery: "WHERE (resources_string['environment'] <> ?)",
expectedQuery: "WHERE (multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ?)",
expectedArgs: []any{"prod"},
expectedErrorContains: "",
},
@ -1498,8 +1530,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Explicit AND",
query: "status=200 AND service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{float64(200), true, "api", true},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
expectedErrorContains: "",
},
{
@ -1532,8 +1564,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Explicit OR",
query: "service.name=\"api\" OR service.name=\"web\"",
shouldPass: true,
expectedQuery: "WHERE ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{"api", true, "web", true},
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{"api", "NULL", "web", "NULL"},
expectedErrorContains: "",
},
{
@ -1558,8 +1590,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "NOT with expressions",
query: "NOT service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE NOT ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{"api", true},
expectedQuery: "WHERE NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@ -1576,8 +1608,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "AND + OR combinations",
query: "status=200 AND (service.name=\"api\" OR service.name=\"web\")",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))))",
expectedArgs: []any{float64(200), true, "api", true, "web", true},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))))",
expectedArgs: []any{float64(200), true, "api", "NULL", "web", "NULL"},
expectedErrorContains: "",
},
{
@ -1602,8 +1634,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "AND + NOT combinations",
query: "status=200 AND NOT service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND NOT ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)))",
expectedArgs: []any{float64(200), true, "api", true},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
expectedErrorContains: "",
},
{
@ -1620,8 +1652,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "OR + NOT combinations",
query: "NOT status=200 OR NOT service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) OR NOT ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)))",
expectedArgs: []any{float64(200), true, "api", true},
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) OR NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
expectedErrorContains: "",
},
{
@ -1638,8 +1670,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "AND + OR + NOT combinations",
query: "status=200 AND (service.name=\"api\" OR NOT duration>1000)",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR NOT ((toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?)))))",
expectedArgs: []any{float64(200), true, "api", true, float64(1000), true},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR NOT ((toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?)))))",
expectedArgs: []any{float64(200), true, "api", "NULL", float64(1000), true},
expectedErrorContains: "",
},
{
@ -1654,8 +1686,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "AND + OR + NOT combinations",
query: "NOT (status=200 AND service.name=\"api\") OR count>0",
shouldPass: true,
expectedQuery: "WHERE (NOT ((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)))) OR (toFloat64(attributes_number['count']) > ? AND mapContains(attributes_number, 'count') = ?))",
expectedArgs: []any{float64(200), true, "api", true, float64(0), true},
expectedQuery: "WHERE (NOT ((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))) OR (toFloat64(attributes_number['count']) > ? AND mapContains(attributes_number, 'count') = ?))",
expectedArgs: []any{float64(200), true, "api", "NULL", float64(0), true},
expectedErrorContains: "",
},
@ -1664,8 +1696,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Implicit AND",
query: "status=200 service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{float64(200), true, "api", true},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
expectedErrorContains: "",
},
{
@ -1690,8 +1722,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Mixed implicit/explicit AND",
query: "status=200 AND service.name=\"api\" duration<1000",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) AND (toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?))",
expectedArgs: []any{float64(200), true, "api", true, float64(1000), true},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) AND (toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?))",
expectedArgs: []any{float64(200), true, "api", "NULL", float64(1000), true},
expectedErrorContains: "",
},
{
@ -1716,8 +1748,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Simple grouping",
query: "service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)",
expectedArgs: []any{"api", true},
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@ -1742,8 +1774,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Nested grouping",
query: "(((service.name=\"api\")))",
shouldPass: true,
expectedQuery: "WHERE ((((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))))",
expectedArgs: []any{"api", true},
expectedQuery: "WHERE ((((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))))",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@ -1760,8 +1792,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Complex nested grouping",
query: "(status=200 AND (service.name=\"api\" OR service.name=\"web\"))",
shouldPass: true,
expectedQuery: "WHERE (((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)))))",
expectedArgs: []any{float64(200), true, "api", true, "web", true},
expectedQuery: "WHERE (((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))))",
expectedArgs: []any{float64(200), true, "api", "NULL", "web", "NULL"},
expectedErrorContains: "",
},
{
@ -1786,16 +1818,16 @@ func TestFilterExprLogs(t *testing.T) {
category: "Deep nesting",
query: "(((status=200 OR status=201) AND service.name=\"api\") OR ((status=202 OR status=203) AND service.name=\"web\"))",
shouldPass: true,
expectedQuery: "WHERE (((((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))) OR (((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)))))",
expectedArgs: []any{float64(200), true, float64(201), true, "api", true, float64(202), true, float64(203), true, "web", true},
expectedQuery: "WHERE (((((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))) OR (((((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))))",
expectedArgs: []any{float64(200), true, float64(201), true, "api", "NULL", float64(202), true, float64(203), true, "web", "NULL"},
expectedErrorContains: "",
},
{
category: "Deep nesting",
query: "(count>0 AND ((duration<1000 AND service.name=\"api\") OR (duration<500 AND service.name=\"web\")))",
shouldPass: true,
expectedQuery: "WHERE (((toFloat64(attributes_number['count']) > ? AND mapContains(attributes_number, 'count') = ?) AND (((((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))) OR (((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)))))))",
expectedArgs: []any{float64(0), true, float64(1000), true, "api", true, float64(500), true, "web", true},
expectedQuery: "WHERE (((toFloat64(attributes_number['count']) > ? AND mapContains(attributes_number, 'count') = ?) AND (((((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))) OR (((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))))))",
expectedArgs: []any{float64(0), true, float64(1000), true, "api", "NULL", float64(500), true, "web", "NULL"},
expectedErrorContains: "",
},
@ -1804,16 +1836,16 @@ func TestFilterExprLogs(t *testing.T) {
category: "String quote styles",
query: "service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)",
expectedArgs: []any{"api", true},
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
category: "String quote styles",
query: "service.name='api'",
shouldPass: true,
expectedQuery: "WHERE (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)",
expectedArgs: []any{"api", true},
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)",
expectedArgs: []any{"api", "NULL"},
expectedErrorContains: "",
},
{
@ -1972,29 +2004,29 @@ func TestFilterExprLogs(t *testing.T) {
category: "Operator precedence",
query: "NOT status=200 AND service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{float64(200), true, "api", true}, // Should be (NOT status=200) AND service.name="api"
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"}, // Should be (NOT status=200) AND service.name="api"
},
{
category: "Operator precedence",
query: "status=200 AND service.name=\"api\" OR service.name=\"web\"",
shouldPass: true,
expectedQuery: "WHERE (((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)) OR (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{float64(200), true, "api", true, "web", true}, // Should be (status=200 AND service.name="api") OR service.name="web"
expectedQuery: "WHERE (((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL", "web", "NULL"}, // Should be (status=200 AND service.name="api") OR service.name="web"
},
{
category: "Operator precedence",
query: "NOT status=200 OR NOT service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) OR NOT ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)))",
expectedArgs: []any{float64(200), true, "api", true}, // Should be (NOT status=200) OR (NOT service.name="api")
expectedQuery: "WHERE (NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)) OR NOT ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?)))",
expectedArgs: []any{float64(200), true, "api", "NULL"}, // Should be (NOT status=200) OR (NOT service.name="api")
},
{
category: "Operator precedence",
query: "status=200 OR service.name=\"api\" AND level=\"ERROR\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) AND (attributes_string['level'] = ? AND mapContains(attributes_string, 'level') = ?)))",
expectedArgs: []any{float64(200), true, "api", true, "ERROR", true}, // Should be status=200 OR (service.name="api" AND level="ERROR")
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) OR ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) AND (attributes_string['level'] = ? AND mapContains(attributes_string, 'level') = ?)))",
expectedArgs: []any{float64(200), true, "api", "NULL", "ERROR", true}, // Should be status=200 OR (service.name="api" AND level="ERROR")
},
// Different whitespace patterns
@ -2018,8 +2050,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Whitespace patterns",
query: "status=200 AND service.name=\"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{float64(200), true, "api", true}, // Multiple spaces
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"}, // Multiple spaces
},
// More Unicode characters
@ -2188,8 +2220,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "More common filters",
query: "service.name=\"api\" AND (status>=500 OR duration>1000) AND NOT message CONTAINS \"expected\"",
shouldPass: true,
expectedQuery: "WHERE ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) AND (((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?))) AND NOT ((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?)))",
expectedArgs: []any{"api", true, float64(500), true, float64(1000), true, "%expected%", true},
expectedQuery: "WHERE ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) AND (((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) OR (toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?))) AND NOT ((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?)))",
expectedArgs: []any{"api", "NULL", float64(500), true, float64(1000), true, "%expected%", true},
},
// Edge cases
@ -2254,8 +2286,8 @@ func TestFilterExprLogs(t *testing.T) {
category: "Unusual whitespace",
query: "status = 200 AND service.name = \"api\"",
shouldPass: true,
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{float64(200), true, "api", true},
expectedQuery: "WHERE ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?) AND (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?))",
expectedArgs: []any{float64(200), true, "api", "NULL"},
},
{
category: "Unusual whitespace",
@ -2315,13 +2347,13 @@ func TestFilterExprLogs(t *testing.T) {
)
`,
shouldPass: true,
expectedQuery: "WHERE ((((((((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) AND (toFloat64(attributes_number['status']) < ? AND mapContains(attributes_number, 'status') = ?))) OR (((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) AND (toFloat64(attributes_number['status']) < ? AND mapContains(attributes_number, 'status') = ?) AND NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)))))) AND ((((resources_string['service.name'] = ? OR resources_string['service.name'] = ? OR resources_string['service.name'] = ?) AND mapContains(resources_string, 'service.name') = ?) OR (((resources_string['service.type'] = ? AND mapContains(resources_string, 'service.type') = ?) AND NOT ((resources_string['service.deprecated'] = ? AND mapContains(resources_string, 'service.deprecated') = ?)))))))) AND (((((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) OR ((toFloat64(attributes_number['duration']) BETWEEN ? AND ? AND mapContains(attributes_number, 'duration') = ?)))) AND ((resources_string['environment'] <> ? OR (((resources_string['environment'] = ? AND mapContains(resources_string, 'environment') = ?) AND (attributes_bool['is_automated_test'] = ? AND mapContains(attributes_bool, 'is_automated_test') = ?))))))) AND NOT ((((((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?) OR (LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?))) AND (attributes_string['severity'] = ? AND mapContains(attributes_string, 'severity') = ?)))))",
expectedQuery: "WHERE ((((((((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) AND (toFloat64(attributes_number['status']) < ? AND mapContains(attributes_number, 'status') = ?))) OR (((toFloat64(attributes_number['status']) >= ? AND mapContains(attributes_number, 'status') = ?) AND (toFloat64(attributes_number['status']) < ? AND mapContains(attributes_number, 'status') = ?) AND NOT ((toFloat64(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?)))))) AND ((((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? OR multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ?) AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (((multiIf(resource.`service.type` IS NOT NULL, resource.`service.type`::String, mapContains(resources_string, 'service.type'), resources_string['service.type'], NULL) = ? AND multiIf(resource.`service.type` IS NOT NULL, resource.`service.type`::String, mapContains(resources_string, 'service.type'), resources_string['service.type'], NULL) <> ?) AND NOT ((multiIf(resource.`service.deprecated` IS NOT NULL, resource.`service.deprecated`::String, mapContains(resources_string, 'service.deprecated'), resources_string['service.deprecated'], NULL) = ? AND multiIf(resource.`service.deprecated` IS NOT NULL, resource.`service.deprecated`::String, mapContains(resources_string, 'service.deprecated'), resources_string['service.deprecated'], NULL) <> ?)))))))) AND (((((toFloat64(attributes_number['duration']) < ? AND mapContains(attributes_number, 'duration') = ?) OR ((toFloat64(attributes_number['duration']) BETWEEN ? AND ? AND mapContains(attributes_number, 'duration') = ?)))) AND ((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ? OR (((multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) = ? AND multiIf(resource.`environment` IS NOT NULL, resource.`environment`::String, mapContains(resources_string, 'environment'), resources_string['environment'], NULL) <> ?) AND (attributes_bool['is_automated_test'] = ? AND mapContains(attributes_bool, 'is_automated_test') = ?))))))) AND NOT ((((((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?) OR (LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?))) AND (attributes_string['severity'] = ? AND mapContains(attributes_string, 'severity') = ?)))))",
expectedArgs: []any{
float64(200), true, float64(300), true, float64(400), true, float64(500), true, float64(404), true,
"api", "web", "auth", true,
"internal", true, true, true,
"api", "web", "auth", "NULL",
"internal", "NULL", true, "NULL",
float64(1000), true, float64(1000), float64(5000), true,
"test", "test", true, true, true,
"test", "test", "NULL", true, true,
"%warning%", true, "%deprecated%", true,
"low", true,
},
@ -2437,6 +2469,14 @@ func TestFilterExprLogsConflictNegation(t *testing.T) {
expectedArgs: []any{"done", "done"},
expectedErrorContains: "",
},
{
category: "exists",
query: "body EXISTS",
shouldPass: true,
expectedQuery: "WHERE (body <> ? OR mapContains(attributes_string, 'body') = ?)",
expectedArgs: []any{"", true},
expectedErrorContains: "",
},
}
for _, tc := range testCases {

View File

@ -69,8 +69,8 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "NULL", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, "NULL", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
@ -98,8 +98,8 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "redis-manual", true, "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, true, "redis-manual", true, "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", "redis-manual", "NULL", "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, "NULL", "redis-manual", "NULL", "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
@ -137,8 +137,8 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "NULL", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, "NULL", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},

View File

@ -862,6 +862,34 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
Materialized: true,
},
},
"{UserId}": {
{
Name: "{UserId}",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"user@email": {
{
Name: "user@email",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"#user_name": {
{
Name: "#user_name",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"gen_ai.completion.0.content": {
{
Name: "gen_ai.completion.0.content",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
}
for _, keys := range keysMap {

View File

@ -163,6 +163,13 @@ func (c *conditionBuilder) conditionFor(
var value any
switch column.Type {
case schema.JSONColumnType{}:
value = "NULL"
if operator == qbtypes.FilterOperatorExists {
return sb.NE(tblFieldName, value), nil
} else {
return sb.E(tblFieldName, value), nil
}
case schema.ColumnTypeString,
schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
schema.FixedStringColumnType{Length: 32},

View File

@ -50,6 +50,7 @@ var (
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
ValueType: schema.ColumnTypeString,
}},
"resource": {Name: "resource", Type: schema.JSONColumnType{}},
"events": {Name: "events", Type: schema.ArrayColumnType{
ElementType: schema.ColumnTypeString,
@ -157,7 +158,8 @@ var (
}
)
type defaultFieldMapper struct{}
type defaultFieldMapper struct {
}
var _ qbtypes.FieldMapper = (*defaultFieldMapper)(nil)
@ -171,7 +173,7 @@ func (m *defaultFieldMapper) getColumn(
) (*schema.Column, error) {
switch key.FieldContext {
case telemetrytypes.FieldContextResource:
return indexV3Columns["resources_string"], nil
return indexV3Columns["resource"], nil
case telemetrytypes.FieldContextScope:
return nil, qbtypes.ErrColumnNotFound
case telemetrytypes.FieldContextAttribute:
@ -235,6 +237,23 @@ func (m *defaultFieldMapper) FieldFor(
}
switch column.Type {
case schema.JSONColumnType{}:
// json is only supported for resource context as of now
if key.FieldContext != telemetrytypes.FieldContextResource {
return "", errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "only resource context fields are supported for json columns, got %s", key.FieldContext.String)
}
oldColumn := indexV3Columns["resources_string"]
oldKeyName := fmt.Sprintf("%s['%s']", oldColumn.Name, key.Name)
// have to add ::string as clickHouse throws an error :- data types Variant/Dynamic are not allowed in GROUP BY
// once clickHouse dependency is updated, we need to check if we can remove it.
if key.Materialized {
oldKeyName = telemetrytypes.FieldKeyToMaterializedColumnName(key)
oldKeyNameExists := telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, %s==true, %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldKeyNameExists, oldKeyName), nil
} else {
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
}
case schema.ColumnTypeString,
schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
schema.ColumnTypeUInt64,

View File

@ -64,7 +64,16 @@ func TestGetFieldKeyName(t *testing.T) {
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
expectedResult: "resources_string['service.name']",
expectedResult: "multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL)",
expectedError: nil,
},
{
name: "Map column type - resource attribute - legacy",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
expectedResult: "multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL)",
expectedError: nil,
},
{
@ -78,10 +87,9 @@ func TestGetFieldKeyName(t *testing.T) {
},
}
fm := NewFieldMapper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fm := NewFieldMapper()
result, err := fm.FieldFor(ctx, &tc.key)
if tc.expectedError != nil {

View File

@ -390,6 +390,7 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
innerSB.Select("trace_id", "duration_nano", sqlbuilder.Escape("resource_string_service$$name as `service.name`"), "name")
innerSB.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
innerSB.Where("parent_span_id = ''")
innerSB.Where("trace_id GLOBAL IN __toe")
// Add time filter to inner query
innerSB.Where(

View File

@ -60,8 +60,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
@ -89,8 +89,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "redis-manual", true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "redis-manual", true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", "redis-manual", "NULL", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", "redis-manual", "NULL", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
@ -187,8 +187,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
@ -216,8 +216,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
@ -255,8 +255,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) <> ?, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "NULL", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
@ -412,7 +412,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT name AS `name`, resources_string['service.name'] AS `service.name`, duration_nano AS `duration_nano`, `attribute_number_cart$$items_count` AS `cart.items_count`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT name AS `name`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, duration_nano AS `duration_nano`, `attribute_number_cart$$items_count` AS `cart.items_count`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@ -441,7 +441,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, `resource_string_service$$name` AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY attributes_string['user.id'] AS `user.id` desc LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, `resource_string_service$$name_exists`==true, `resource_string_service$$name`, NULL) AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY attributes_string['user.id'] AS `user.id` desc LIMIT ?",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@ -547,7 +547,7 @@ func TestStatementBuilderTraceQuery(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND trace_id GLOBAL IN __toe AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,

View File

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

View File

@ -129,6 +129,7 @@ func (f FilterOperator) IsNegativeOperator() bool {
FilterOperatorILike,
FilterOperatorBetween,
FilterOperatorIn,
FilterOperatorExists,
FilterOperatorRegexp,
FilterOperatorContains:
return false

View File

@ -50,6 +50,8 @@ type PostableRule struct {
PreferredChannels []string `json:"preferredChannels,omitempty"`
Version string `json:"version,omitempty"`
Evaluation *EvaluationEnvelope `yaml:"evaluation,omitempty" json:"evaluation,omitempty"`
}
func (r *PostableRule) processRuleDefaults() error {
@ -98,6 +100,9 @@ func (r *PostableRule) processRuleDefaults() error {
r.RuleCondition.Thresholds = &thresholdData
}
}
if r.Evaluation == nil {
r.Evaluation = &EvaluationEnvelope{RollingEvaluation, RollingWindow{EvalWindow: r.EvalWindow, Frequency: r.Frequency}}
}
return r.Validate()
}

View File

@ -0,0 +1,287 @@
package ruletypes
import (
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
type EvaluationKind struct {
valuer.String
}
var (
RollingEvaluation = EvaluationKind{valuer.NewString("rolling")}
CumulativeEvaluation = EvaluationKind{valuer.NewString("cumulative")}
)
type Evaluation interface {
NextWindowFor(curr time.Time) (time.Time, time.Time)
GetFrequency() Duration
}
type RollingWindow struct {
EvalWindow Duration `json:"evalWindow"`
Frequency Duration `json:"frequency"`
}
func (rollingWindow RollingWindow) Validate() error {
if rollingWindow.EvalWindow <= 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "evalWindow must be greater than zero")
}
if rollingWindow.Frequency <= 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "frequency must be greater than zero")
}
return nil
}
func (rollingWindow RollingWindow) NextWindowFor(curr time.Time) (time.Time, time.Time) {
return curr.Add(time.Duration(-rollingWindow.EvalWindow)), curr
}
func (rollingWindow RollingWindow) GetFrequency() Duration {
return rollingWindow.Frequency
}
type CumulativeWindow struct {
Schedule CumulativeSchedule `json:"schedule"`
Frequency Duration `json:"frequency"`
Timezone string `json:"timezone"`
}
type CumulativeSchedule struct {
Type ScheduleType `json:"type"`
Minute *int `json:"minute,omitempty"` // 0-59, for all types
Hour *int `json:"hour,omitempty"` // 0-23, for daily/weekly/monthly
Day *int `json:"day,omitempty"` // 1-31, for monthly
Weekday *int `json:"weekday,omitempty"` // 0-6 (Sunday=0), for weekly
}
type ScheduleType struct {
valuer.String
}
var (
ScheduleTypeHourly = ScheduleType{valuer.NewString("hourly")}
ScheduleTypeDaily = ScheduleType{valuer.NewString("daily")}
ScheduleTypeWeekly = ScheduleType{valuer.NewString("weekly")}
ScheduleTypeMonthly = ScheduleType{valuer.NewString("monthly")}
)
func (cumulativeWindow CumulativeWindow) Validate() error {
// Validate schedule
if err := cumulativeWindow.Schedule.Validate(); err != nil {
return err
}
if _, err := time.LoadLocation(cumulativeWindow.Timezone); err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "timezone is invalid")
}
if cumulativeWindow.Frequency <= 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "frequency must be greater than zero")
}
return nil
}
func (cs CumulativeSchedule) Validate() error {
switch cs.Type {
case ScheduleTypeHourly:
if cs.Minute == nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "minute must be specified for hourly schedule")
}
if *cs.Minute < 0 || *cs.Minute > 59 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "minute must be between 0 and 59")
}
case ScheduleTypeDaily:
if cs.Hour == nil || cs.Minute == nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "hour and minute must be specified for daily schedule")
}
if *cs.Hour < 0 || *cs.Hour > 23 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "hour must be between 0 and 23")
}
if *cs.Minute < 0 || *cs.Minute > 59 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "minute must be between 0 and 59")
}
case ScheduleTypeWeekly:
if cs.Weekday == nil || cs.Hour == nil || cs.Minute == nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "weekday, hour and minute must be specified for weekly schedule")
}
if *cs.Weekday < 0 || *cs.Weekday > 6 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "weekday must be between 0 and 6 (Sunday=0)")
}
if *cs.Hour < 0 || *cs.Hour > 23 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "hour must be between 0 and 23")
}
if *cs.Minute < 0 || *cs.Minute > 59 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "minute must be between 0 and 59")
}
case ScheduleTypeMonthly:
if cs.Day == nil || cs.Hour == nil || cs.Minute == nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "day, hour and minute must be specified for monthly schedule")
}
if *cs.Day < 1 || *cs.Day > 31 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "day must be between 1 and 31")
}
if *cs.Hour < 0 || *cs.Hour > 23 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "hour must be between 0 and 23")
}
if *cs.Minute < 0 || *cs.Minute > 59 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "minute must be between 0 and 59")
}
default:
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid schedule type")
}
return nil
}
func (cumulativeWindow CumulativeWindow) NextWindowFor(curr time.Time) (time.Time, time.Time) {
loc := time.UTC
if cumulativeWindow.Timezone != "" {
if tz, err := time.LoadLocation(cumulativeWindow.Timezone); err == nil {
loc = tz
}
}
currInTZ := curr.In(loc)
windowStart := cumulativeWindow.getLastScheduleTime(currInTZ, loc)
return windowStart.In(time.UTC), currInTZ.In(time.UTC)
}
func (cw CumulativeWindow) getLastScheduleTime(curr time.Time, loc *time.Location) time.Time {
schedule := cw.Schedule
switch schedule.Type {
case ScheduleTypeHourly:
// Find the most recent hour boundary with the specified minute
minute := *schedule.Minute
candidate := time.Date(curr.Year(), curr.Month(), curr.Day(), curr.Hour(), minute, 0, 0, loc)
if candidate.After(curr) {
candidate = candidate.Add(-time.Hour)
}
return candidate
case ScheduleTypeDaily:
// Find the most recent day boundary with the specified hour and minute
hour := *schedule.Hour
minute := *schedule.Minute
candidate := time.Date(curr.Year(), curr.Month(), curr.Day(), hour, minute, 0, 0, loc)
if candidate.After(curr) {
candidate = candidate.AddDate(0, 0, -1)
}
return candidate
case ScheduleTypeWeekly:
weekday := time.Weekday(*schedule.Weekday)
hour := *schedule.Hour
minute := *schedule.Minute
// Calculate days to subtract to reach the target weekday
daysBack := int(curr.Weekday() - weekday)
if daysBack < 0 {
daysBack += 7
}
candidate := time.Date(curr.Year(), curr.Month(), curr.Day(), hour, minute, 0, 0, loc).AddDate(0, 0, -daysBack)
if candidate.After(curr) {
candidate = candidate.AddDate(0, 0, -7)
}
return candidate
case ScheduleTypeMonthly:
// Find the most recent month boundary with the specified day, hour and minute
targetDay := *schedule.Day
hour := *schedule.Hour
minute := *schedule.Minute
// Try current month first
lastDayOfCurrentMonth := time.Date(curr.Year(), curr.Month()+1, 0, 0, 0, 0, 0, loc).Day()
dayInCurrentMonth := targetDay
if targetDay > lastDayOfCurrentMonth {
dayInCurrentMonth = lastDayOfCurrentMonth
}
candidate := time.Date(curr.Year(), curr.Month(), dayInCurrentMonth, hour, minute, 0, 0, loc)
if candidate.After(curr) {
prevMonth := curr.AddDate(0, -1, 0)
lastDayOfPrevMonth := time.Date(prevMonth.Year(), prevMonth.Month()+1, 0, 0, 0, 0, 0, loc).Day()
dayInPrevMonth := targetDay
if targetDay > lastDayOfPrevMonth {
dayInPrevMonth = lastDayOfPrevMonth
}
candidate = time.Date(prevMonth.Year(), prevMonth.Month(), dayInPrevMonth, hour, minute, 0, 0, loc)
}
return candidate
default:
return curr
}
}
func (cumulativeWindow CumulativeWindow) GetFrequency() Duration {
return cumulativeWindow.Frequency
}
type EvaluationEnvelope struct {
Kind EvaluationKind `json:"kind"`
Spec any `json:"spec"`
}
func (e *EvaluationEnvelope) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to unmarshal evaluation: %v", err)
}
if err := json.Unmarshal(raw["kind"], &e.Kind); err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to unmarshal evaluation kind: %v", err)
}
switch e.Kind {
case RollingEvaluation:
var rollingWindow RollingWindow
if err := json.Unmarshal(raw["spec"], &rollingWindow); err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to unmarshal rolling window: %v", err)
}
err := rollingWindow.Validate()
if err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to validate rolling window: %v", err)
}
e.Spec = rollingWindow
case CumulativeEvaluation:
var cumulativeWindow CumulativeWindow
if err := json.Unmarshal(raw["spec"], &cumulativeWindow); err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to unmarshal cumulative window: %v", err)
}
err := cumulativeWindow.Validate()
if err != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to validate cumulative window: %v", err)
}
e.Spec = cumulativeWindow
default:
return errors.NewInvalidInputf(errors.CodeUnsupported, "unknown evaluation kind")
}
return nil
}
func (e *EvaluationEnvelope) GetEvaluation() (Evaluation, error) {
if e.Kind.IsZero() {
e.Kind = RollingEvaluation
}
switch e.Kind {
case RollingEvaluation:
if rolling, ok := e.Spec.(RollingWindow); ok {
return rolling, nil
}
case CumulativeEvaluation:
if cumulative, ok := e.Spec.(CumulativeWindow); ok {
return cumulative, nil
}
default:
return nil, errors.NewInvalidInputf(errors.CodeUnsupported, "unknown evaluation kind")
}
return nil, errors.NewInvalidInputf(errors.CodeUnsupported, "unknown evaluation kind")
}

View File

@ -0,0 +1,878 @@
package ruletypes
import (
"encoding/json"
"testing"
"time"
)
func TestRollingWindow_EvaluationTime(t *testing.T) {
tests := []struct {
name string
evalWindow Duration
current time.Time
wantStart time.Time
wantEnd time.Time
}{
{
name: "5 minute rolling window",
evalWindow: Duration(5 * time.Minute),
current: time.Date(2023, 12, 1, 12, 30, 0, 0, time.UTC),
wantStart: time.Date(2023, 12, 1, 12, 25, 0, 0, time.UTC),
wantEnd: time.Date(2023, 12, 1, 12, 30, 0, 0, time.UTC),
},
{
name: "1 hour rolling window",
evalWindow: Duration(1 * time.Hour),
current: time.Date(2023, 12, 1, 15, 45, 30, 0, time.UTC),
wantStart: time.Date(2023, 12, 1, 14, 45, 30, 0, time.UTC),
wantEnd: time.Date(2023, 12, 1, 15, 45, 30, 0, time.UTC),
},
{
name: "30 second rolling window",
evalWindow: Duration(30 * time.Second),
current: time.Date(2023, 12, 1, 12, 30, 15, 0, time.UTC),
wantStart: time.Date(2023, 12, 1, 12, 29, 45, 0, time.UTC),
wantEnd: time.Date(2023, 12, 1, 12, 30, 15, 0, time.UTC),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rw := &RollingWindow{
EvalWindow: tt.evalWindow,
Frequency: Duration(1 * time.Minute),
}
gotStart, gotEnd := rw.NextWindowFor(tt.current)
if !gotStart.Equal(tt.wantStart) {
t.Errorf("RollingWindow.NextWindowFor() start time = %v, want %v", gotStart, tt.wantStart)
}
if !gotEnd.Equal(tt.wantEnd) {
t.Errorf("RollingWindow.NextWindowFor() end time = %v, want %v", gotEnd, tt.wantEnd)
}
})
}
}
func TestCumulativeWindow_NewScheduleSystem(t *testing.T) {
tests := []struct {
name string
window CumulativeWindow
current time.Time
wantErr bool
}{
{
name: "hourly schedule - minute 15",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(15),
},
Frequency: Duration(5 * time.Minute),
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 14, 30, 0, 0, time.UTC),
wantErr: false,
},
{
name: "daily schedule - 9:30 AM IST",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(9),
Minute: intPtr(30),
},
Frequency: Duration(1 * time.Hour),
Timezone: "Asia/Kolkata",
},
current: time.Date(2025, 3, 15, 15, 30, 0, 0, time.UTC),
wantErr: false,
},
{
name: "weekly schedule - Monday 2:00 PM",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeWeekly,
Weekday: intPtr(1), // Monday
Hour: intPtr(14),
Minute: intPtr(0),
},
Frequency: Duration(24 * time.Hour),
Timezone: "America/New_York",
},
current: time.Date(2025, 3, 18, 19, 0, 0, 0, time.UTC), // Tuesday
wantErr: false,
},
{
name: "monthly schedule - 1st at midnight",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(1),
Hour: intPtr(0),
Minute: intPtr(0),
},
Frequency: Duration(24 * time.Hour),
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 12, 0, 0, 0, time.UTC),
wantErr: false,
},
{
name: "invalid schedule - missing minute for hourly",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
},
Frequency: Duration(5 * time.Minute),
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 14, 30, 0, 0, time.UTC),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test validation
err := tt.window.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("CumulativeWindow.Validate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
// Test NextWindowFor
start, end := tt.window.NextWindowFor(tt.current)
// Basic validation
if start.After(end) {
t.Errorf("Window start should not be after end: start=%v, end=%v", start, end)
}
if end.After(tt.current) {
t.Errorf("Window end should not be after current time: end=%v, current=%v", end, tt.current)
}
}
})
}
}
func intPtr(i int) *int {
return &i
}
func TestCumulativeWindow_NextWindowFor(t *testing.T) {
tests := []struct {
name string
window CumulativeWindow
current time.Time
wantStart time.Time
wantEnd time.Time
}{
// Hourly schedule tests
{
name: "hourly - current at exact minute",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(30),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 14, 30, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 15, 14, 30, 0, 0, time.UTC),
wantEnd: time.Date(2025, 3, 15, 14, 30, 0, 0, time.UTC),
},
{
name: "hourly - current after scheduled minute",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(15),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 14, 45, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 15, 14, 15, 0, 0, time.UTC),
wantEnd: time.Date(2025, 3, 15, 14, 45, 0, 0, time.UTC),
},
{
name: "hourly - current before scheduled minute",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(30),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 14, 15, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 15, 13, 30, 0, 0, time.UTC), // Previous hour
wantEnd: time.Date(2025, 3, 15, 14, 15, 0, 0, time.UTC),
},
{
name: "hourly - current before scheduled minute",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(30),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 13, 14, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 15, 12, 30, 0, 0, time.UTC), // Previous hour
wantEnd: time.Date(2025, 3, 15, 13, 14, 0, 0, time.UTC),
},
{
name: "hourly - current before scheduled minute",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(30),
},
Timezone: "Asia/Kolkata",
},
current: time.Date(2025, 3, 15, 13, 14, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 15, 13, 00, 0, 0, time.UTC), // Previous hour
wantEnd: time.Date(2025, 3, 15, 13, 14, 0, 0, time.UTC),
},
// Daily schedule tests
{
name: "daily - current at exact time",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(9),
Minute: intPtr(30),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 9, 30, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 15, 9, 30, 0, 0, time.UTC),
wantEnd: time.Date(2025, 3, 15, 9, 30, 0, 0, time.UTC),
},
{
name: "daily - current after scheduled time",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(9),
Minute: intPtr(30),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 15, 45, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 15, 9, 30, 0, 0, time.UTC),
wantEnd: time.Date(2025, 3, 15, 15, 45, 0, 0, time.UTC),
},
{
name: "daily - current before scheduled time",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(9),
Minute: intPtr(30),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 8, 15, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 14, 9, 30, 0, 0, time.UTC), // Previous day
wantEnd: time.Date(2025, 3, 15, 8, 15, 0, 0, time.UTC),
},
{
name: "daily - with timezone IST",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(9),
Minute: intPtr(30),
},
Timezone: "Asia/Kolkata",
},
current: time.Date(2025, 3, 15, 15, 30, 0, 0, time.UTC), // 9:00 PM IST
wantStart: time.Date(2025, 3, 15, 4, 0, 0, 0, time.UTC), // 9:30 AM IST in UTC
wantEnd: time.Date(2025, 3, 15, 15, 30, 0, 0, time.UTC),
},
// Weekly schedule tests
{
name: "weekly - current on scheduled day at exact time",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeWeekly,
Weekday: intPtr(1), // Monday
Hour: intPtr(14),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 17, 14, 0, 0, 0, time.UTC), // Monday
wantStart: time.Date(2025, 3, 17, 14, 0, 0, 0, time.UTC),
wantEnd: time.Date(2025, 3, 17, 14, 0, 0, 0, time.UTC),
},
{
name: "weekly - current on different day",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeWeekly,
Weekday: intPtr(1), // Monday
Hour: intPtr(14),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 19, 10, 30, 0, 0, time.UTC), // Wednesday
wantStart: time.Date(2025, 3, 17, 14, 0, 0, 0, time.UTC), // Previous Monday
wantEnd: time.Date(2025, 3, 19, 10, 30, 0, 0, time.UTC),
},
{
name: "weekly - current before scheduled time on same day",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeWeekly,
Weekday: intPtr(2), // Tuesday
Hour: intPtr(14),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 18, 10, 0, 0, 0, time.UTC), // Tuesday before 2 PM
wantStart: time.Date(2025, 3, 11, 14, 0, 0, 0, time.UTC), // Previous Tuesday
wantEnd: time.Date(2025, 3, 18, 10, 0, 0, 0, time.UTC),
},
// Monthly schedule tests
{
name: "monthly - current on scheduled day at exact time",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(15),
Hour: intPtr(12),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 12, 0, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 15, 12, 0, 0, 0, time.UTC),
wantEnd: time.Date(2025, 3, 15, 12, 0, 0, 0, time.UTC),
},
{
name: "monthly - current after scheduled time",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(1),
Hour: intPtr(0),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 16, 30, 0, 0, time.UTC),
wantStart: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
wantEnd: time.Date(2025, 3, 15, 16, 30, 0, 0, time.UTC),
},
{
name: "monthly - current before scheduled day",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(15),
Hour: intPtr(12),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 10, 10, 0, 0, 0, time.UTC),
wantStart: time.Date(2025, 2, 15, 12, 0, 0, 0, time.UTC), // Previous month
wantEnd: time.Date(2025, 3, 10, 10, 0, 0, 0, time.UTC),
},
{
name: "monthly - day 31 in february (edge case)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(31),
Hour: intPtr(12),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2025, 3, 15, 10, 0, 0, 0, time.UTC),
wantStart: time.Date(2025, 2, 28, 12, 0, 0, 0, time.UTC), // Feb 28 (last day of Feb)
wantEnd: time.Date(2025, 3, 15, 10, 0, 0, 0, time.UTC),
},
// Comprehensive timezone-based test cases
{
name: "Asia/Tokyo timezone - hourly schedule",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(45),
},
Timezone: "Asia/Tokyo",
},
current: time.Date(2023, 12, 15, 2, 30, 0, 0, time.UTC), // 11:30 AM JST
wantStart: time.Date(2023, 12, 15, 1, 45, 0, 0, time.UTC), // 10:45 AM JST in UTC
wantEnd: time.Date(2023, 12, 15, 2, 30, 0, 0, time.UTC),
},
{
name: "America/New_York timezone - daily schedule (EST)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(8), // 8 AM EST
Minute: intPtr(0),
},
Timezone: "America/New_York",
},
current: time.Date(2023, 12, 15, 20, 30, 0, 0, time.UTC), // 3:30 PM EST
wantStart: time.Date(2023, 12, 15, 13, 0, 0, 0, time.UTC), // 8 AM EST in UTC
wantEnd: time.Date(2023, 12, 15, 20, 30, 0, 0, time.UTC),
},
{
name: "Europe/London timezone - weekly schedule (GMT)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeWeekly,
Weekday: intPtr(1), // Monday
Hour: intPtr(12),
Minute: intPtr(0),
},
Timezone: "Europe/London",
},
current: time.Date(2023, 12, 15, 15, 0, 0, 0, time.UTC), // Friday 3 PM GMT
wantStart: time.Date(2023, 12, 11, 12, 0, 0, 0, time.UTC), // Previous Monday 12 PM GMT
wantEnd: time.Date(2023, 12, 15, 15, 0, 0, 0, time.UTC),
},
{
name: "Australia/Sydney timezone - monthly schedule (AEDT)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(1),
Hour: intPtr(0), // Midnight AEDT
Minute: intPtr(0),
},
Timezone: "Australia/Sydney",
},
current: time.Date(2023, 12, 15, 5, 0, 0, 0, time.UTC), // 4 PM AEDT on 15th
wantStart: time.Date(2023, 11, 30, 13, 0, 0, 0, time.UTC), // Midnight AEDT on Dec 1st in UTC (Nov 30 13:00 UTC)
wantEnd: time.Date(2023, 12, 15, 5, 0, 0, 0, time.UTC),
},
{
name: "Pacific/Honolulu timezone - hourly schedule (HST)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(30),
},
Timezone: "Pacific/Honolulu",
},
current: time.Date(2023, 12, 15, 22, 45, 0, 0, time.UTC), // 12:45 PM HST
wantStart: time.Date(2023, 12, 15, 22, 30, 0, 0, time.UTC), // 12:30 PM HST in UTC
wantEnd: time.Date(2023, 12, 15, 22, 45, 0, 0, time.UTC),
},
{
name: "America/Los_Angeles timezone - DST transition daily",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(2), // 2 AM PST/PDT
Minute: intPtr(0),
},
Timezone: "America/Los_Angeles",
},
current: time.Date(2023, 3, 12, 15, 0, 0, 0, time.UTC), // Day after DST starts
wantStart: time.Date(2023, 3, 12, 9, 0, 0, 0, time.UTC), // 2 AM PDT in UTC (PDT = UTC-7)
wantEnd: time.Date(2023, 3, 12, 15, 0, 0, 0, time.UTC),
},
{
name: "Europe/Berlin timezone - weekly schedule (CET)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeWeekly,
Weekday: intPtr(5), // Friday
Hour: intPtr(16), // 4 PM CET
Minute: intPtr(30),
},
Timezone: "Europe/Berlin",
},
current: time.Date(2023, 12, 18, 10, 0, 0, 0, time.UTC), // Monday 11 AM CET
wantStart: time.Date(2023, 12, 15, 15, 30, 0, 0, time.UTC), // Previous Friday 4:30 PM CET
wantEnd: time.Date(2023, 12, 18, 10, 0, 0, 0, time.UTC),
},
{
name: "Asia/Kolkata timezone - monthly edge case (IST)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(31), // 31st (edge case for Feb)
Hour: intPtr(23),
Minute: intPtr(59),
},
Timezone: "Asia/Kolkata",
},
current: time.Date(2023, 3, 10, 12, 0, 0, 0, time.UTC), // March 10th 5:30 PM IST
wantStart: time.Date(2023, 2, 28, 18, 29, 0, 0, time.UTC), // Feb 28 11:59 PM IST (last day of Feb)
wantEnd: time.Date(2023, 3, 10, 12, 0, 0, 0, time.UTC),
},
{
name: "America/Chicago timezone - hourly across midnight (CST)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(0), // Top of hour
},
Timezone: "America/Chicago",
},
current: time.Date(2023, 12, 15, 6, 30, 0, 0, time.UTC), // 12:30 AM CST
wantStart: time.Date(2023, 12, 15, 6, 0, 0, 0, time.UTC), // Midnight CST in UTC
wantEnd: time.Date(2023, 12, 15, 6, 30, 0, 0, time.UTC),
},
// Boundary condition test cases
{
name: "boundary - end of year transition (Dec 31 to Jan 1)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(0),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), // Jan 1st noon
wantStart: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), // Jan 1st midnight
wantEnd: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
},
{
name: "boundary - leap year Feb 29th monthly schedule",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(29),
Hour: intPtr(15),
Minute: intPtr(30),
},
Timezone: "UTC",
},
current: time.Date(2024, 3, 10, 10, 0, 0, 0, time.UTC), // March 10th (leap year)
wantStart: time.Date(2024, 2, 29, 15, 30, 0, 0, time.UTC), // Feb 29th exists in leap year
wantEnd: time.Date(2024, 3, 10, 10, 0, 0, 0, time.UTC),
},
{
name: "boundary - non-leap year Feb 29th request (fallback to Feb 28th)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(29),
Hour: intPtr(15),
Minute: intPtr(30),
},
Timezone: "UTC",
},
current: time.Date(2023, 3, 10, 10, 0, 0, 0, time.UTC), // March 10th (non-leap year)
wantStart: time.Date(2023, 2, 28, 15, 30, 0, 0, time.UTC), // Feb 28th (fallback)
wantEnd: time.Date(2023, 3, 10, 10, 0, 0, 0, time.UTC),
},
{
name: "boundary - day 31 in April (30-day month fallback)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(31),
Hour: intPtr(12),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2023, 5, 15, 10, 0, 0, 0, time.UTC), // May 15th
wantStart: time.Date(2023, 4, 30, 12, 0, 0, 0, time.UTC), // April 30th (fallback from 31st)
wantEnd: time.Date(2023, 5, 15, 10, 0, 0, 0, time.UTC),
},
{
name: "boundary - weekly Sunday to Monday transition",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeWeekly,
Weekday: intPtr(0), // Sunday
Hour: intPtr(23),
Minute: intPtr(59),
},
Timezone: "UTC",
},
current: time.Date(2023, 12, 11, 1, 0, 0, 0, time.UTC), // Monday 1 AM
wantStart: time.Date(2023, 12, 10, 23, 59, 0, 0, time.UTC), // Previous Sunday 11:59 PM
wantEnd: time.Date(2023, 12, 11, 1, 0, 0, 0, time.UTC),
},
{
name: "boundary - hourly minute 59 to minute 0 transition",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(59),
},
Timezone: "UTC",
},
current: time.Date(2023, 12, 15, 14, 5, 0, 0, time.UTC), // 14:05
wantStart: time.Date(2023, 12, 15, 13, 59, 0, 0, time.UTC), // 13:59 (previous hour)
wantEnd: time.Date(2023, 12, 15, 14, 5, 0, 0, time.UTC),
},
{
name: "boundary - DST spring forward (2 AM doesn't exist)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(2), // 2 AM (skipped during DST)
Minute: intPtr(30),
},
Timezone: "America/New_York",
},
current: time.Date(2023, 3, 12, 15, 0, 0, 0, time.UTC), // Day DST starts
wantStart: time.Date(2023, 3, 12, 6, 30, 0, 0, time.UTC), // Same day 2:30 AM EDT (adjusted for DST)
wantEnd: time.Date(2023, 3, 12, 15, 0, 0, 0, time.UTC),
},
{
name: "boundary - DST fall back (2 AM occurs twice)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(2), // 2 AM (occurs twice)
Minute: intPtr(30),
},
Timezone: "America/New_York",
},
current: time.Date(2023, 11, 5, 15, 0, 0, 0, time.UTC), // Day DST ends
wantStart: time.Date(2023, 11, 5, 7, 30, 0, 0, time.UTC), // Same day 2:30 AM EST (after fall back)
wantEnd: time.Date(2023, 11, 5, 15, 0, 0, 0, time.UTC),
},
{
name: "boundary - month transition January to February",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeMonthly,
Day: intPtr(31),
Hour: intPtr(0),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2023, 2, 15, 12, 0, 0, 0, time.UTC), // February 15th
wantStart: time.Date(2023, 1, 31, 0, 0, 0, 0, time.UTC), // January 31st (exists)
wantEnd: time.Date(2023, 2, 15, 12, 0, 0, 0, time.UTC),
},
{
name: "boundary - extreme timezone offset (+14 hours)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(12),
Minute: intPtr(0),
},
Timezone: "Pacific/Kiritimati", // UTC+14
},
current: time.Date(2023, 12, 15, 5, 0, 0, 0, time.UTC), // 7 PM local time
wantStart: time.Date(2023, 12, 14, 22, 0, 0, 0, time.UTC), // 12 PM local time (previous day in UTC)
wantEnd: time.Date(2023, 12, 15, 5, 0, 0, 0, time.UTC),
},
{
name: "boundary - extreme timezone offset (-12 hours)",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeDaily,
Hour: intPtr(12),
Minute: intPtr(0),
},
Timezone: "Etc/GMT+12", // UTC-12 (use standard timezone name)
},
current: time.Date(2023, 12, 15, 5, 0, 0, 0, time.UTC), // 5 PM previous day local time
wantStart: time.Date(2023, 12, 15, 0, 0, 0, 0, time.UTC), // 12 PM local time (same day in UTC)
wantEnd: time.Date(2023, 12, 15, 5, 0, 0, 0, time.UTC),
},
{
name: "boundary - week boundary Saturday to Sunday",
window: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeWeekly,
Weekday: intPtr(6), // Saturday
Hour: intPtr(0),
Minute: intPtr(0),
},
Timezone: "UTC",
},
current: time.Date(2023, 12, 17, 12, 0, 0, 0, time.UTC), // Sunday noon
wantStart: time.Date(2023, 12, 16, 0, 0, 0, 0, time.UTC), // Saturday midnight
wantEnd: time.Date(2023, 12, 17, 12, 0, 0, 0, time.UTC),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotStart, gotEnd := tt.window.NextWindowFor(tt.current)
if !gotStart.Equal(tt.wantStart) {
t.Errorf("NextWindowFor() start = %v, want %v", gotStart, tt.wantStart)
}
if !gotEnd.Equal(tt.wantEnd) {
t.Errorf("NextWindowFor() end = %v, want %v", gotEnd, tt.wantEnd)
}
// Validate basic invariants
if gotStart.After(gotEnd) {
t.Errorf("Window start should not be after end: start=%v, end=%v", gotStart, gotEnd)
}
if gotEnd.After(tt.current) {
t.Errorf("Window end should not be after current time: end=%v, current=%v", gotEnd, tt.current)
}
duration := gotEnd.Sub(gotStart)
// Validate window length is reasonable
if duration < 0 {
t.Errorf("Window duration should not be negative: %v", duration)
}
if duration > 366*24*time.Hour {
t.Errorf("Window duration should not exceed 1 year: %v", duration)
}
})
}
}
func TestEvaluationEnvelope_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
jsonInput string
wantKind EvaluationKind
wantSpec interface{}
wantError bool
}{
{
name: "rolling evaluation with valid data",
jsonInput: `{"kind":"rolling","spec":{"evalWindow":"5m","frequency":"1m"}}`,
wantKind: RollingEvaluation,
wantSpec: RollingWindow{
EvalWindow: Duration(5 * time.Minute),
Frequency: Duration(1 * time.Minute),
},
},
{
name: "cumulative evaluation with valid data",
jsonInput: `{"kind":"cumulative","spec":{"schedule":{"type":"hourly","minute":30},"frequency":"2m","timezone":"UTC"}}`,
wantKind: CumulativeEvaluation,
wantSpec: CumulativeWindow{
Schedule: CumulativeSchedule{
Type: ScheduleTypeHourly,
Minute: intPtr(30),
},
Frequency: Duration(2 * time.Minute),
Timezone: "UTC",
},
},
{
name: "rolling evaluation with validation error - zero evalWindow",
jsonInput: `{"kind":"rolling","spec":{"evalWindow":"0s","frequency":"1m"}}`,
wantError: true,
},
{
name: "rolling evaluation with validation error - zero frequency",
jsonInput: `{"kind":"rolling","spec":{"evalWindow":"5m","frequency":"0s"}}`,
wantError: true,
},
{
name: "cumulative evaluation with validation error - zero frequency",
jsonInput: `{"kind":"cumulative","spec":{"schedule":{"type":"hourly","minute":30},"frequency":"0s","timezone":"UTC"}}`,
wantError: true,
},
{
name: "cumulative evaluation with validation error - invalid timezone",
jsonInput: `{"kind":"cumulative","spec":{"schedule":{"type":"daily","hour":9,"minute":30},"frequency":"1m","timezone":"Invalid/Timezone"}}`,
wantError: true,
},
{
name: "cumulative evaluation with validation error - missing minute for hourly",
jsonInput: `{"kind":"cumulative","spec":{"schedule":{"type":"hourly"},"frequency":"1m","timezone":"UTC"}}`,
wantError: true,
},
{
name: "unknown evaluation kind",
jsonInput: `{"kind":"unknown","spec":{"evalWindow":"5m","frequency":"1h"}}`,
wantError: true,
},
{
name: "invalid JSON",
jsonInput: `{"kind":"rolling","spec":invalid}`,
wantError: true,
},
{
name: "missing kind field",
jsonInput: `{"spec":{"evalWindow":"5m","frequency":"1m"}}`,
wantError: true,
},
{
name: "missing spec field",
jsonInput: `{"kind":"rolling"}`,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var envelope EvaluationEnvelope
err := json.Unmarshal([]byte(tt.jsonInput), &envelope)
if tt.wantError {
if err == nil {
t.Errorf("EvaluationEnvelope.UnmarshalJSON() expected error, got none")
}
return
}
if err != nil {
t.Fatalf("EvaluationEnvelope.UnmarshalJSON() unexpected error = %v", err)
}
if envelope.Kind != tt.wantKind {
t.Errorf("EvaluationEnvelope.Kind = %v, want %v", envelope.Kind, tt.wantKind)
}
// Check spec content based on type
switch tt.wantKind {
case RollingEvaluation:
gotSpec, ok := envelope.Spec.(RollingWindow)
if !ok {
t.Fatalf("Expected RollingWindow spec, got %T", envelope.Spec)
}
wantSpec := tt.wantSpec.(RollingWindow)
if gotSpec.EvalWindow != wantSpec.EvalWindow {
t.Errorf("RollingWindow.EvalWindow = %v, want %v", gotSpec.EvalWindow, wantSpec.EvalWindow)
}
if gotSpec.Frequency != wantSpec.Frequency {
t.Errorf("RollingWindow.Frequency = %v, want %v", gotSpec.Frequency, wantSpec.Frequency)
}
case CumulativeEvaluation:
gotSpec, ok := envelope.Spec.(CumulativeWindow)
if !ok {
t.Fatalf("Expected CumulativeWindow spec, got %T", envelope.Spec)
}
wantSpec := tt.wantSpec.(CumulativeWindow)
if gotSpec.Schedule.Type != wantSpec.Schedule.Type {
t.Errorf("CumulativeWindow.Schedule.Type = %v, want %v", gotSpec.Schedule.Type, wantSpec.Schedule.Type)
}
if (gotSpec.Schedule.Minute == nil) != (wantSpec.Schedule.Minute == nil) ||
(gotSpec.Schedule.Minute != nil && wantSpec.Schedule.Minute != nil && *gotSpec.Schedule.Minute != *wantSpec.Schedule.Minute) {
t.Errorf("CumulativeWindow.Schedule.Minute = %v, want %v", gotSpec.Schedule.Minute, wantSpec.Schedule.Minute)
}
if gotSpec.Frequency != wantSpec.Frequency {
t.Errorf("CumulativeWindow.Frequency = %v, want %v", gotSpec.Frequency, wantSpec.Frequency)
}
if gotSpec.Timezone != wantSpec.Timezone {
t.Errorf("CumulativeWindow.Timezone = %v, want %v", gotSpec.Timezone, wantSpec.Timezone)
}
}
})
}
}

View File

@ -0,0 +1,11 @@
#!/bin/bash
set -e
echo "Generating TypeScript parser..."
# Create output directory if it doesn't exist
mkdir -p frontend/src/parser
# Generate TypeScript parser
antlr4 -Dlanguage=TypeScript -o frontend/src/parser grammar/FilterQuery.g4 -visitor
echo "TypeScript parser generation complete"