mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 23:47:12 +00:00
Merge branch 'main' into enhancement/cmd-click-across-routes
This commit is contained in:
commit
6491e31242
@ -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)
|
||||
|
||||
213
docs/contributing/go/integration.md
Normal file
213
docs/contributing/go/integration.md
Normal 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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
});
|
||||
@ -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.';
|
||||
@ -0,0 +1,3 @@
|
||||
import AlertCondition from './AlertCondition';
|
||||
|
||||
export default AlertCondition;
|
||||
277
frontend/src/container/CreateAlertV2/AlertCondition/styles.scss
Normal file
277
frontend/src/container/CreateAlertV2/AlertCondition/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
frontend/src/container/CreateAlertV2/AlertCondition/types.ts
Normal file
23
frontend/src/container/CreateAlertV2/AlertCondition/types.ts
Normal 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[];
|
||||
}
|
||||
@ -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,
|
||||
}),
|
||||
) || []
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
28
frontend/src/container/CreateAlertV2/QuerySection/utils.tsx
Normal file
28
frontend/src/container/CreateAlertV2/QuerySection/utils.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -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' },
|
||||
];
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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' };
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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'll help you get the most out of SigNoz, whether you'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
@ -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 {
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -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)
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -129,6 +129,7 @@ func (f FilterOperator) IsNegativeOperator() bool {
|
||||
FilterOperatorILike,
|
||||
FilterOperatorBetween,
|
||||
FilterOperatorIn,
|
||||
FilterOperatorExists,
|
||||
FilterOperatorRegexp,
|
||||
FilterOperatorContains:
|
||||
return false
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
287
pkg/types/ruletypes/evaluation.go
Normal file
287
pkg/types/ruletypes/evaluation.go
Normal 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")
|
||||
}
|
||||
878
pkg/types/ruletypes/evaluation_test.go
Normal file
878
pkg/types/ruletypes/evaluation_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
11
scripts/grammar/generate-frontend-parser.sh
Executable file
11
scripts/grammar/generate-frontend-parser.sh
Executable 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"
|
||||
Loading…
x
Reference in New Issue
Block a user