Compare commits

...

5 Commits

Author SHA1 Message Date
Tushar Vats
fee8d86893 feat(querier): add the pattern request type for log patterns
A pattern request groups the logs of one logs builder query by pattern: the
message (body_v2.message) with variable parts such as IDs, IPs, timestamps
and numbers replaced by placeholders. It returns the top patterns per
service with an estimated count, samples, severities and a trend, counted
on a hash sample of at most about 100k logs.

The new pattern('<text>'[, ...]) filter function keeps the logs of the given
patterns. A placeholder in the filter matches the same placeholder or <*>
in a log's pattern, and the body is searched for the pattern's fixed words
first so the masks run only on the logs that can match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvYb7kcQPeRsTAoedAsb5W
2026-11-13 12:26:01 +05:30
Naman Verma
3acfb0c8c0 chore: remove source and version from rules api schema (#13541)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

We don't want external clients like Terraform or AI Agents to be sending
these fields anymore cuz they are useless. That's why we need to remove
these from the schema. But sadly, we probably have users who send these
in their scripts, and our rules APIs don't accept fields not in the
struct, so these fields stay in the struct.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/731
2026-11-12 08:29:38 +00:00
Nikhil Mantri
75994b0d6f fix(alert-channel-integrations): describe jira auth as a flat object in the openapi spec (#13540)
#### Description

- The Terraform provider is generated from our OpenAPI spec. #13424
described the Jira channel `auth` as a `oneOf` (cloud or self-hosted)
with no discriminator, which the generator cannot map, so the
notification channel resource stopped building.
- `deployment` sits next to `auth`, not inside it, so it cannot act as
the discriminator.
- This PR describes `auth` in the spec as one object with optional
`email`, `apiToken` and `pat`. Each description says which deployment
needs it.
- The Go type now matches the spec: one auth struct replaces the two
per-deployment structs, and the fields are validated against
`deployment`.
- The accepted payload, the responses and stored channels stay the same,
so no migration is needed.

#### Issues closed by this PR

Closes SigNoz/pulse-pod#728

#### Additional Information

- Problem and options doc:
https://app.notion.com/p/415fcc6bcd1980a0869fd5ed2e19ca3c
- The spec no longer marks the auth fields as required per deployment.
The server still rejects a wrong or missing combination with a 400.
- A field of the other deployment (for example `pat` on a cloud channel)
now gets an explicit error message instead of the unknown field one. It
is rejected only when it is not blank.
- The generated spec and API types are in a separate commit.
2026-11-12 08:13:20 +00:00
Gaurav Tewari
3bfb8708e9 test(ai-observability): explorer side panels and query builder in tab tests (#13523)
#### Description

- `QuickFiltersLayout` and `QuerySection` in the AI Explorer tests.
Neither is under test, and rendering them made the tab-switch test take
~1s; it now takes ~0.1s.
- Assert the trace view and its URL columns before switching tabs, so
the cleared-columns check can't pass when the columns were never set.
- After the switch, assert the list panel type is in the URL and the
trace view unmounts.

#### Issues closed by this PR

#### Screenshots / Screen Recordings

#### Additional Information

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-11-12 05:47:34 +00:00
Aditya Singh
876d84ac53 fix(sentry): expected 401, 403 and network failures page the frontend (#13510)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- API calls that nobody catches reach Sentry as unhandled rejections.
Most of these failures are expected: a 403 for a user without a viewer,
editor or admin role, a 401 for an expired session, or a network error
from a dropped connection.
- Since the axios 1.21.0 bump (#13339), these errors carry our call site
in the stack. Sentry opens a new issue for each call site, and each new
issue pages. SIGNOZ-UI-5N4 paged twice this way.
- `beforeSend` now drops axios errors with status 401 or 403, or with
code `ERR_NETWORK`. Sentry still gets 400, 404 and 5xx.

#### Issues closed by this PR

Closes https://github.com/SigNoz/events-pod/issues/261

#### Additional Information

- The filter matches raw axios errors only. A call that goes through
`ErrorResponseHandlerV2`, `ErrorResponseHandlerForGeneratedAPIs` or
`convertToApiError` rethrows an `APIError`. Sentry still reports that
error when nobody catches it.
- The filter drops all 403 and `ERR_NETWORK` errors. A missing
permission check in the UI, or a CORS or base-path regression, no longer
shows in Sentry. This is a deliberate choice.
- The filter also drops 401 errors from the token refresh retry path.
- A separate PR adds an error state for the empty key suggestions in the
query search bar.
2026-11-11 18:32:35 +00:00
107 changed files with 3419 additions and 1329 deletions

View File

@@ -553,23 +553,23 @@ components:
- apiKey
type: object
AlertmanagertypesChannelJiraAuth:
oneOf:
- $ref: '#/components/schemas/AlertmanagertypesChannelJiraCloudAuth'
- $ref: '#/components/schemas/AlertmanagertypesChannelJiraSelfHostedAuth'
type: object
AlertmanagertypesChannelJiraCloudAuth:
properties:
apiToken:
description: The Atlassian API token paired with email. A secret; returned
by create, update and fetch by ID, never by list.
description: The Atlassian API token paired with email. Required when deployment
is cloud. A secret; returned by create, update and fetch by ID, never
by list.
format: password
type: string
email:
description: The Atlassian account email used for basic auth.
description: The Atlassian account email used for basic auth. Required when
deployment is cloud.
type: string
pat:
description: The Jira personal access token, sent as a Bearer header. Required
when deployment is selfhosted. A secret; returned by create, update and
fetch by ID, never by list.
format: password
type: string
required:
- email
- apiToken
type: object
AlertmanagertypesChannelJiraConfig:
properties:
@@ -642,16 +642,6 @@ components:
- issueType
- auth
type: object
AlertmanagertypesChannelJiraSelfHostedAuth:
properties:
pat:
description: The Jira personal access token, sent as a Bearer header. A
secret; returned by create, update and fetch by ID, never by list.
format: password
type: string
required:
- pat
type: object
AlertmanagertypesChannelKind:
enum:
- slack
@@ -10151,6 +10141,113 @@ components:
- asc
- desc
type: string
Querybuildertypesv5Pattern:
properties:
count:
description: 'The estimated number of logs of the pattern in the time range:
the sampled count times sampleRate.'
minimum: 0
type: integer
firstSeen:
description: The Unix time in milliseconds of the earliest sampled log of
the pattern in the time range.
format: int64
type: integer
id:
description: The pattern's ID, the decimal cityHash64 of its text.
type: string
labels:
description: 'The group-by field values the pattern was counted for: always
service.name first, then the query''s own group by.'
items:
$ref: '#/components/schemas/Querybuildertypesv5Label'
type: array
lastSeen:
description: The Unix time in milliseconds of the latest sampled log of
the pattern in the time range.
format: int64
type: integer
pattern:
description: 'The pattern text: the log message with the values that change
replaced by placeholders such as <*>, <IP> or <UUID>. Filter on it with
pattern(''<pattern text>'').'
type: string
percent:
description: The pattern's count as a percentage of total.
format: double
type: number
samples:
description: Up to three sampled log messages of the pattern.
items:
type: string
nullable: true
type: array
severities:
description: The most common severity texts of the pattern's logs, most
common first.
items:
type: string
nullable: true
type: array
trend:
description: The estimated number of logs of the pattern in each step of
the time range, oldest first. Steps without a sampled log are left out.
items:
$ref: '#/components/schemas/Querybuildertypesv5PatternTrendPoint'
nullable: true
type: array
required:
- id
- pattern
- count
- percent
- firstSeen
- lastSeen
- samples
- severities
- trend
type: object
Querybuildertypesv5PatternTrendPoint:
properties:
count:
description: The estimated number of logs of the pattern in the step.
minimum: 0
type: integer
timestamp:
description: The Unix time in milliseconds the step starts at.
format: int64
type: integer
required:
- timestamp
- count
type: object
Querybuildertypesv5PatternsData:
properties:
patterns:
description: The patterns, most logs first.
items:
$ref: '#/components/schemas/Querybuildertypesv5Pattern'
nullable: true
type: array
queryName:
description: The name of the query that produced this result.
type: string
sampleRate:
description: One in sampleRate of the matching logs was read and every count
is scaled up by it. 1 means every log was read and the counts are exact.
minimum: 0
type: integer
total:
description: The number of logs the query matched in the time range, over
every pattern.
minimum: 0
type: integer
required:
- queryName
- sampleRate
- total
- patterns
type: object
Querybuildertypesv5PreviewStatement:
properties:
db.statement.args:
@@ -10712,11 +10809,12 @@ components:
- $ref: '#/components/schemas/Querybuildertypesv5ScalarData'
- $ref: '#/components/schemas/Querybuildertypesv5RawData'
- $ref: '#/components/schemas/Querybuildertypesv5DistributionData'
- $ref: '#/components/schemas/Querybuildertypesv5PatternsData'
properties:
results:
description: 'One result per enabled query, typed by the request type: TimeSeriesData
for time_series and heatmap, ScalarData for scalar, RawData for raw and
DistributionData for distribution requests.'
for time_series and heatmap, ScalarData for scalar, RawData for raw, DistributionData
for distribution and PatternsData for pattern requests.'
items: {}
nullable: true
type: array
@@ -10907,8 +11005,8 @@ components:
Querybuildertypesv5QueryRangeResponse:
description: 'Response from the v5 query range endpoint. The data.results array
contains typed results depending on the requestType: TimeSeriesData for time_series
and heatmap, ScalarData for scalar, RawData for raw, or DistributionData for
distribution requests.'
and heatmap, ScalarData for scalar, RawData for raw, DistributionData for
distribution, or PatternsData for pattern requests.'
properties:
data:
allOf:
@@ -11006,6 +11104,7 @@ components:
- trace
- heatmap
- distribution
- pattern
type: string
Querybuildertypesv5ScalarData:
properties:
@@ -11773,15 +11872,6 @@ components:
enum:
- v2alpha1
type: string
source:
deprecated: true
description: The URL of the page the rule was created from. Informational;
it is not used for evaluation.
type: string
version:
deprecated: true
description: The query payload version. Nothing reads it.
type: string
required:
- alert
- alertType
@@ -11900,11 +11990,6 @@ components:
enum:
- v2alpha1
type: string
source:
deprecated: true
description: The URL of the page the rule was created from. Informational;
it is not used for evaluation.
type: string
state:
allOf:
- $ref: '#/components/schemas/RuletypesAlertState'
@@ -11915,10 +12000,6 @@ components:
type: string
updatedBy:
type: string
version:
deprecated: true
description: The query payload version. Nothing reads it.
type: string
required:
- id
- state
@@ -25559,7 +25640,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
logs_threshold:
description: Counts matching log records (ERROR severity + body contains)
over a rolling window. Fires at least once per evaluation when the
@@ -25626,7 +25706,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
metric_anomaly:
description: Wraps a builder query in the `anomaly` function with
daily seasonality, so SigNoz compares each point against the forecast
@@ -25699,7 +25778,6 @@ paths:
interval: 4h
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment"). Useful
@@ -25755,7 +25833,6 @@ paths:
interval: 1h
ruleType: promql_rule
schemaVersion: v2alpha1
version: v5
metric_threshold_formula:
description: Computes disk utilization as (1 - available/capacity)
* 100 by combining two disabled base queries with a builder_formula.
@@ -25847,7 +25924,6 @@ paths:
interval: 2h
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
metric_threshold_single:
description: Fires when a pod consumes more than 80% of its requested
CPU for the whole evaluation window. Uses `k8s.pod.cpu_request_utilization`.
@@ -25916,7 +25992,6 @@ paths:
interval: 4h
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
notification_settings:
description: 'Demonstrates the full notificationSettings surface:
`groupBy` merges alerts across labels to cut noise, `newGroupEvalDelay`
@@ -26015,7 +26090,6 @@ paths:
usePolicy: false
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
tiered_thresholds:
description: Two tiers (warning and critical) in a single rule, each
with its own target, op, matchType, and channels so warnings and
@@ -26115,7 +26189,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
traces_error_rate_formula:
description: Two disabled trace count queries (A = error spans where
hasError=true, B = total spans) combined via a builder_formula into
@@ -26205,7 +26278,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
traces_threshold_latency:
description: Builder query against the traces signal with p99(duration_nano).
The series unit is ns (compositeQuery.unit), the target is in seconds
@@ -26274,7 +26346,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
schema:
$ref: '#/components/schemas/RuletypesPostableRule'
responses:
@@ -26525,7 +26596,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
logs_threshold:
description: Counts matching log records (ERROR severity + body contains)
over a rolling window. Fires at least once per evaluation when the
@@ -26592,7 +26662,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
metric_anomaly:
description: Wraps a builder query in the `anomaly` function with
daily seasonality, so SigNoz compares each point against the forecast
@@ -26665,7 +26734,6 @@ paths:
interval: 4h
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment"). Useful
@@ -26721,7 +26789,6 @@ paths:
interval: 1h
ruleType: promql_rule
schemaVersion: v2alpha1
version: v5
metric_threshold_formula:
description: Computes disk utilization as (1 - available/capacity)
* 100 by combining two disabled base queries with a builder_formula.
@@ -26813,7 +26880,6 @@ paths:
interval: 2h
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
metric_threshold_single:
description: Fires when a pod consumes more than 80% of its requested
CPU for the whole evaluation window. Uses `k8s.pod.cpu_request_utilization`.
@@ -26882,7 +26948,6 @@ paths:
interval: 4h
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
notification_settings:
description: 'Demonstrates the full notificationSettings surface:
`groupBy` merges alerts across labels to cut noise, `newGroupEvalDelay`
@@ -26981,7 +27046,6 @@ paths:
usePolicy: false
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
tiered_thresholds:
description: Two tiers (warning and critical) in a single rule, each
with its own target, op, matchType, and channels so warnings and
@@ -27081,7 +27145,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
traces_error_rate_formula:
description: Two disabled trace count queries (A = error spans where
hasError=true, B = total spans) combined via a builder_formula into
@@ -27171,7 +27234,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
traces_threshold_latency:
description: Builder query against the traces signal with p99(duration_nano).
The series unit is ns (compositeQuery.unit), the target is in seconds
@@ -27240,7 +27302,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
schema:
$ref: '#/components/schemas/RuletypesPostableRule'
responses:
@@ -27392,7 +27453,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
logs_threshold:
description: Counts matching log records (ERROR severity + body contains)
over a rolling window. Fires at least once per evaluation when the
@@ -27459,7 +27519,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
metric_anomaly:
description: Wraps a builder query in the `anomaly` function with
daily seasonality, so SigNoz compares each point against the forecast
@@ -27532,7 +27591,6 @@ paths:
interval: 4h
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment"). Useful
@@ -27588,7 +27646,6 @@ paths:
interval: 1h
ruleType: promql_rule
schemaVersion: v2alpha1
version: v5
metric_threshold_formula:
description: Computes disk utilization as (1 - available/capacity)
* 100 by combining two disabled base queries with a builder_formula.
@@ -27680,7 +27737,6 @@ paths:
interval: 2h
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
metric_threshold_single:
description: Fires when a pod consumes more than 80% of its requested
CPU for the whole evaluation window. Uses `k8s.pod.cpu_request_utilization`.
@@ -27749,7 +27805,6 @@ paths:
interval: 4h
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
notification_settings:
description: 'Demonstrates the full notificationSettings surface:
`groupBy` merges alerts across labels to cut noise, `newGroupEvalDelay`
@@ -27848,7 +27903,6 @@ paths:
usePolicy: false
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
tiered_thresholds:
description: Two tiers (warning and critical) in a single rule, each
with its own target, op, matchType, and channels so warnings and
@@ -27948,7 +28002,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
traces_error_rate_formula:
description: Two disabled trace count queries (A = error spans where
hasError=true, B = total spans) combined via a builder_formula into
@@ -28038,7 +28091,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
traces_threshold_latency:
description: Builder query against the traces signal with p99(duration_nano).
The series unit is ns (compositeQuery.unit), the target is in seconds
@@ -28107,7 +28159,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
schema:
$ref: '#/components/schemas/RuletypesPostableRule'
responses:
@@ -29021,7 +29072,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
logs_threshold:
description: Counts matching log records (ERROR severity + body contains)
over a rolling window. Fires at least once per evaluation when the
@@ -29088,7 +29138,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
metric_anomaly:
description: Wraps a builder query in the `anomaly` function with
daily seasonality, so SigNoz compares each point against the forecast
@@ -29161,7 +29210,6 @@ paths:
interval: 4h
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment"). Useful
@@ -29217,7 +29265,6 @@ paths:
interval: 1h
ruleType: promql_rule
schemaVersion: v2alpha1
version: v5
metric_threshold_formula:
description: Computes disk utilization as (1 - available/capacity)
* 100 by combining two disabled base queries with a builder_formula.
@@ -29309,7 +29356,6 @@ paths:
interval: 2h
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
metric_threshold_single:
description: Fires when a pod consumes more than 80% of its requested
CPU for the whole evaluation window. Uses `k8s.pod.cpu_request_utilization`.
@@ -29378,7 +29424,6 @@ paths:
interval: 4h
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
notification_settings:
description: 'Demonstrates the full notificationSettings surface:
`groupBy` merges alerts across labels to cut noise, `newGroupEvalDelay`
@@ -29477,7 +29522,6 @@ paths:
usePolicy: false
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
tiered_thresholds:
description: Two tiers (warning and critical) in a single rule, each
with its own target, op, matchType, and channels so warnings and
@@ -29577,7 +29621,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
traces_error_rate_formula:
description: Two disabled trace count queries (A = error spans where
hasError=true, B = total spans) combined via a builder_formula into
@@ -29667,7 +29710,6 @@ paths:
interval: 15m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
traces_threshold_latency:
description: Builder query against the traces signal with p99(duration_nano).
The series unit is ns (compositeQuery.unit), the target is in seconds
@@ -29736,7 +29778,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
schema:
$ref: '#/components/schemas/RuletypesPostableRule'
responses:

View File

@@ -1,7 +1,9 @@
import type { ErrorEvent, StackFrame } from '@sentry/react';
import { AxiosError, AxiosHeaders } from 'axios';
import {
hasNoAppFrames,
isExpectedRequestFailure,
isExternalIntegrationError,
SENTRY_IGNORE_ERRORS,
} from '../sentryFilters';
@@ -138,3 +140,36 @@ describe('SENTRY_IGNORE_ERRORS', () => {
).toBe(true);
});
});
describe('isExpectedRequestFailure', () => {
function axiosErrorWithStatus(status: number): AxiosError {
const config = { headers: new AxiosHeaders() };
return new AxiosError(
`Request failed with status code ${status}`,
AxiosError.ERR_BAD_REQUEST,
config,
undefined,
{ status, statusText: '', headers: {}, config, data: {} },
);
}
it.each([401, 403])('drops a %i response', (status) => {
expect(isExpectedRequestFailure(axiosErrorWithStatus(status))).toBe(true);
});
it('drops a network error', () => {
expect(
isExpectedRequestFailure(
new AxiosError('Network Error', AxiosError.ERR_NETWORK),
),
).toBe(true);
});
it.each([400, 404, 500, 503])('keeps a %i response', (status) => {
expect(isExpectedRequestFailure(axiosErrorWithStatus(status))).toBe(false);
});
it('keeps an error that is not from axios', () => {
expect(isExpectedRequestFailure(new Error('Network Error'))).toBe(false);
});
});

View File

@@ -40,6 +40,7 @@ import defaultRoutes, {
} from './routes';
import {
hasNoAppFrames,
isExpectedRequestFailure,
isExternalIntegrationError,
SENTRY_IGNORE_ERRORS,
} from './sentryFilters';
@@ -341,6 +342,9 @@ function App(): JSX.Element {
if (error?.name === 'AbortError') {
return null;
}
if (isExpectedRequestFailure(hint?.originalException)) {
return null;
}
// Ignore benign Monaco cancellation errors (name 'Canceled').
if (error?.name === 'Canceled') {
return null;

View File

@@ -1,4 +1,5 @@
import type { ErrorEvent } from '@sentry/react';
import { AxiosError, isAxiosError } from 'axios';
export const SENTRY_IGNORE_ERRORS = [
// A tab that outlived a deploy requests hashed assets the new build no longer
@@ -14,6 +15,18 @@ export const SENTRY_IGNORE_ERRORS = [
/Called on script loaded before session recording is available/,
];
// Not UI bugs: 401/403 come from the session or role, ERR_NETWORK from a
// dropped connection.
export function isExpectedRequestFailure(error: unknown): boolean {
if (!isAxiosError(error)) {
return false;
}
const status = error.response?.status;
return (
error.code === AxiosError.ERR_NETWORK || status === 401 || status === 403
);
}
const APP_ASSETS_PATH = '/assets/';
// Frames without a position are `[native code]`, which says nothing about the

View File

@@ -829,33 +829,26 @@ export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTy
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfigDTOKind {
jira = 'jira',
}
export interface AlertmanagertypesChannelJiraCloudAuthDTO {
export interface AlertmanagertypesChannelJiraAuthDTO {
/**
* @type string
* @format password
* @description The Atlassian API token paired with email. A secret; returned by create, update and fetch by ID, never by list.
* @description The Atlassian API token paired with email. Required when deployment is cloud. A secret; returned by create, update and fetch by ID, never by list.
*/
apiToken: string;
apiToken?: string;
/**
* @type string
* @description The Atlassian account email used for basic auth.
* @description The Atlassian account email used for basic auth. Required when deployment is cloud.
*/
email: string;
}
export interface AlertmanagertypesChannelJiraSelfHostedAuthDTO {
email?: string;
/**
* @type string
* @format password
* @description The Jira personal access token, sent as a Bearer header. A secret; returned by create, update and fetch by ID, never by list.
* @description The Jira personal access token, sent as a Bearer header. Required when deployment is selfhosted. A secret; returned by create, update and fetch by ID, never by list.
*/
pat: string;
pat?: string;
}
export type AlertmanagertypesChannelJiraAuthDTO =
| AlertmanagertypesChannelJiraCloudAuthDTO
| AlertmanagertypesChannelJiraSelfHostedAuthDTO;
export enum AlertmanagertypesJiraDeploymentDTO {
cloud = 'cloud',
selfhosted = 'selfhosted',
@@ -6971,6 +6964,7 @@ export enum Querybuildertypesv5RequestTypeDTO {
trace = 'trace',
heatmap = 'heatmap',
distribution = 'distribution',
pattern = 'pattern',
}
/**
* The query kind.
@@ -11888,6 +11882,103 @@ export interface Querybuildertypesv5FormatOptionsDTO {
formatTableResultForUI?: boolean;
}
export interface Querybuildertypesv5PatternTrendPointDTO {
/**
* @type integer
* @minimum 0
* @description The estimated number of logs of the pattern in the step.
*/
count: number;
/**
* @type integer
* @format int64
* @description The Unix time in milliseconds the step starts at.
*/
timestamp: number;
}
export interface Querybuildertypesv5PatternDTO {
/**
* @type integer
* @minimum 0
* @description The estimated number of logs of the pattern in the time range: the sampled count times sampleRate.
*/
count: number;
/**
* @type integer
* @format int64
* @description The Unix time in milliseconds of the earliest sampled log of the pattern in the time range.
*/
firstSeen: number;
/**
* @type string
* @description The pattern's ID, the decimal cityHash64 of its text.
*/
id: string;
/**
* @type array
* @description The group-by field values the pattern was counted for: always service.name first, then the query's own group by.
*/
labels?: Querybuildertypesv5LabelDTO[];
/**
* @type integer
* @format int64
* @description The Unix time in milliseconds of the latest sampled log of the pattern in the time range.
*/
lastSeen: number;
/**
* @type string
* @description The pattern text: the log message with the values that change replaced by placeholders such as <*>, <IP> or <UUID>. Filter on it with pattern('<pattern text>').
*/
pattern: string;
/**
* @type number
* @format double
* @description The pattern's count as a percentage of total.
*/
percent: number;
/**
* @type array,null
* @description Up to three sampled log messages of the pattern.
*/
samples: string[] | null;
/**
* @type array,null
* @description The most common severity texts of the pattern's logs, most common first.
*/
severities: string[] | null;
/**
* @type array,null
* @description The estimated number of logs of the pattern in each step of the time range, oldest first. Steps without a sampled log are left out.
*/
trend: Querybuildertypesv5PatternTrendPointDTO[] | null;
}
export interface Querybuildertypesv5PatternsDataDTO {
/**
* @type array,null
* @description The patterns, most logs first.
*/
patterns: Querybuildertypesv5PatternDTO[] | null;
/**
* @type string
* @description The name of the query that produced this result.
*/
queryName: string;
/**
* @type integer
* @minimum 0
* @description One in sampleRate of the matching logs was read and every count is scaled up by it. 1 means every log was read and the counts are exact.
*/
sampleRate: number;
/**
* @type integer
* @minimum 0
* @description The number of logs the query matched in the time range, over every pattern.
*/
total: number;
}
export interface TelemetrystoretypesIndexStepDTO {
/**
* @type string
@@ -12094,28 +12185,35 @@ export type Querybuildertypesv5QueryDataDTO =
| (Querybuildertypesv5TimeSeriesDataDTO & {
/**
* @type array,null
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, DistributionData for distribution and PatternsData for pattern requests.
*/
results?: unknown[] | null;
})
| (Querybuildertypesv5ScalarDataDTO & {
/**
* @type array,null
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, DistributionData for distribution and PatternsData for pattern requests.
*/
results?: unknown[] | null;
})
| (Querybuildertypesv5RawDataDTO & {
/**
* @type array,null
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, DistributionData for distribution and PatternsData for pattern requests.
*/
results?: unknown[] | null;
})
| (Querybuildertypesv5DistributionDataDTO & {
/**
* @type array,null
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, DistributionData for distribution and PatternsData for pattern requests.
*/
results?: unknown[] | null;
})
| (Querybuildertypesv5PatternsDataDTO & {
/**
* @type array,null
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, DistributionData for distribution and PatternsData for pattern requests.
*/
results?: unknown[] | null;
});
@@ -12249,7 +12347,7 @@ export interface Querybuildertypesv5QueryRangeRequestDTO {
}
/**
* Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, or DistributionData for distribution requests.
* Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, DistributionData for distribution, or PatternsData for pattern requests.
*/
export interface Querybuildertypesv5QueryRangeResponseDTO {
/**
@@ -13048,16 +13146,6 @@ export interface RuletypesPostableRuleDTO {
* @description The rule payload schema.
*/
schemaVersion: RuletypesPostableRuleDTOSchemaVersion;
/**
* @type string
* @description The URL of the page the rule was created from. Informational; it is not used for evaluation.
*/
source?: string;
/**
* @type string
* @description The query payload version. Nothing reads it.
*/
version?: string;
}
export interface RuletypesPostableRuleViewDTO {
@@ -13150,11 +13238,6 @@ export interface RuletypesRuleDTO {
* @description The rule payload schema.
*/
schemaVersion: RuletypesRuleDTOSchemaVersion;
/**
* @type string
* @description The URL of the page the rule was created from. Informational; it is not used for evaluation.
*/
source?: string;
/**
* @description The current evaluation state: inactive, pending, recovering, firing, nodata or disabled.
*/
@@ -13168,11 +13251,6 @@ export interface RuletypesRuleDTO {
* @type string
*/
updatedBy?: string;
/**
* @type string
* @description The query payload version. Nothing reads it.
*/
version?: string;
}
export enum RuletypesThresholdKindDTO {

View File

@@ -174,7 +174,6 @@ function QuerySection(): JSX.Element {
runQuery={runQueryHandler}
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
alertDef={alertDef}
panelType={PANEL_TYPES.TIME_SERIES}
isAnomalyDetection={isAnomalyDetection}
key={currentQuery.queryType}

View File

@@ -23,20 +23,11 @@ export type JiraDeployment = AlertmanagertypesJiraDeploymentDTO;
export type KeysOfUnion<T> = T extends unknown ? keyof T : never;
/** Every key of every member, optional, so a form can hold any member's fields. */
export type FlattenUnion<T> = {
[K in KeysOfUnion<T>]?: T extends unknown
? K extends keyof T
? T[K]
: never
: never;
};
export type JiraFormSpec = Omit<
AlertmanagertypesChannelJiraConfigDTO,
'auth'
> & {
auth?: FlattenUnion<AlertmanagertypesChannelJiraAuthDTO>;
auth?: AlertmanagertypesChannelJiraAuthDTO;
};
/** Merged so switching kind keeps the fields the kinds share. */

View File

@@ -1,5 +1,4 @@
import { RuletypesRuleTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { ENTITY_VERSION_V5 } from 'constants/app';
import {
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
@@ -28,7 +27,6 @@ const defaultAnnotations = {
export const alertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V5,
condition: {
compositeQuery: {
builderQueries: {
@@ -62,7 +60,6 @@ export const alertDefaults: AlertDef = {
export const anamolyAlertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V5,
ruleType: RuletypesRuleTypeDTO.anomaly_rule,
condition: {
compositeQuery: {
@@ -107,7 +104,6 @@ export const anamolyAlertDefaults: AlertDef = {
export const logAlertDefaults: AlertDef = {
alertType: AlertTypes.LOGS_BASED_ALERT,
version: ENTITY_VERSION_V5,
condition: {
compositeQuery: {
builderQueries: {
@@ -139,7 +135,6 @@ export const logAlertDefaults: AlertDef = {
export const traceAlertDefaults: AlertDef = {
alertType: AlertTypes.TRACES_BASED_ALERT,
version: ENTITY_VERSION_V5,
condition: {
compositeQuery: {
builderQueries: {
@@ -171,7 +166,6 @@ export const traceAlertDefaults: AlertDef = {
export const exceptionAlertDefaults: AlertDef = {
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
version: ENTITY_VERSION_V5,
condition: {
compositeQuery: {
builderQueries: {

View File

@@ -176,7 +176,6 @@ function ChartPreview({
},
originalGraphType: graphType,
},
// alertDef?.version || DEFAULT_ENTITY_VERSION,
ENTITY_VERSION_V5,
{
queryKey: [

View File

@@ -6,7 +6,7 @@ import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
@@ -15,7 +15,6 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { isEmpty } from 'lodash-es';
import { Atom, Terminal } from '@signozhq/icons';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
import ChQuerySection from './ChQuerySection';
@@ -37,7 +36,6 @@ function QuerySection({
runQuery,
isLoadingQueries,
handleCancelQuery,
alertDef,
panelType,
ruleId,
isAnomalyDetection,
@@ -71,12 +69,8 @@ function QuerySection({
signalSource: signalSource === 'meter' ? 'meter' : '',
}}
showTraceOperator={alertType === AlertTypes.TRACES_BASED_ALERT}
showFunctions={
(alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4) ||
alertType === AlertTypes.LOGS_BASED_ALERT
}
version={alertDef.version || 'v3'}
showFunctions={alertType === AlertTypes.LOGS_BASED_ALERT}
version={ENTITY_VERSION_V5}
onSignalSourceChange={handleSignalSourceChange}
signalSourceChangeEnabled
/>
@@ -301,7 +295,6 @@ interface QuerySectionProps {
runQuery: VoidFunction;
isLoadingQueries: boolean;
handleCancelQuery: () => void;
alertDef: AlertDef;
panelType: PANEL_TYPES;
ruleId: string;
isAnomalyDetection?: boolean;

View File

@@ -1,3 +1,4 @@
import type { QuickFiltersLayoutProps } from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -14,6 +15,19 @@ import { act, screen, userEvent } from 'tests/test-utils';
import Explorer from '../Explorer';
jest.mock(
'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout',
() => ({
__esModule: true,
default: ({ children }: QuickFiltersLayoutProps): JSX.Element => (
<div>{children}</div>
),
}),
);
jest.mock('../QuerySection/QuerySection', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('../ListView/ListView', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="ai-list-view" />,
@@ -74,9 +88,18 @@ describe('AI observability Explorer', () => {
)}`,
);
expect(screen.getByTestId('ai-trace-view')).toBeInTheDocument();
expect(
JSON.parse(urlParam(history, URL_OPTIONS) ?? '').selectColumns,
).toStrictEqual(traceColumns);
await user.click(screen.getByTestId('search-view'));
expect(screen.getByTestId('ai-list-view')).toBeInTheDocument();
expect(screen.queryByTestId('ai-trace-view')).not.toBeInTheDocument();
expect(JSON.parse(urlParam(history, QueryParams.panelTypes) ?? '')).toBe(
PANEL_TYPES.LIST,
);
expect(JSON.parse(urlParam(history, URL_OPTIONS) ?? '')).toStrictEqual({
...defaultOptionsQuery,
selectColumns: [],

View File

@@ -40,7 +40,6 @@ const make = (
state: RuletypesAlertStateDTO.inactive,
labels: { severity: 'info' },
annotations: {},
source: '',
ruleType: RuletypesRuleTypeDTO.threshold_rule,
schemaVersion: RuletypesRuleDTOSchemaVersion.v2alpha1,
evaluation: {

View File

@@ -379,8 +379,6 @@ const buildRule = (index: number, shape: RuleShape): RuletypesRuleDTO => {
createdBy: 'ada@signoz.io',
updatedBy: 'grace@signoz.io',
schemaVersion: RuletypesRuleDTOSchemaVersion.v2alpha1,
version: 'v5',
source: 'http://localhost/alerts',
notificationSettings: {
groupBy: ['alertname'],
usePolicy: false,

File diff suppressed because one or more lines are too long

View File

@@ -25,12 +25,13 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
PATTERN_CALL=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

@@ -25,12 +25,13 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
PATTERN_CALL=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
'('=1
')'=2
'['=3

View File

@@ -39,12 +39,13 @@ export default class FilterQueryLexer extends Lexer {
public static readonly HASANY = 25;
public static readonly HASALL = 26;
public static readonly SEARCH = 27;
public static readonly BOOL = 28;
public static readonly NUMBER = 29;
public static readonly QUOTED_TEXT = 30;
public static readonly KEY = 31;
public static readonly WS = 32;
public static readonly FREETEXT = 33;
public static readonly PATTERN_CALL = 28;
public static readonly BOOL = 29;
public static readonly NUMBER = 30;
public static readonly QUOTED_TEXT = 31;
public static readonly KEY = 32;
public static readonly WS = 33;
public static readonly FREETEXT = 34;
public static readonly EOF = Token.EOF;
public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];
@@ -70,6 +71,7 @@ export default class FilterQueryLexer extends Lexer {
"HASTOKEN",
"HAS", "HASANY",
"HASALL", "SEARCH",
"PATTERN_CALL",
"BOOL", "NUMBER",
"QUOTED_TEXT",
"KEY", "WS",
@@ -80,8 +82,8 @@ export default class FilterQueryLexer extends Lexer {
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS", "REGEXP",
"CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY", "HASALL",
"SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
"SEARCH", "PATTERN_CALL", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
];
@@ -102,122 +104,127 @@ export default class FilterQueryLexer extends Lexer {
public get modeNames(): string[] { return FilterQueryLexer.modeNames; }
public static readonly _serializedATN: number[] = [4,0,33,329,6,-1,2,0,
public static readonly _serializedATN: number[] = [4,0,34,346,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,
2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,1,0,
1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,91,8,5,1,6,1,6,1,6,
1,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,
1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,
14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,3,15,134,8,15,1,16,1,16,1,16,1,16,
1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,151,8,17,1,
18,1,18,1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,
1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,1,24,1,
24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,
1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,210,
8,27,1,28,1,28,1,29,3,29,215,8,29,1,29,4,29,218,8,29,11,29,12,29,219,1,
29,1,29,5,29,224,8,29,10,29,12,29,227,9,29,3,29,229,8,29,1,29,1,29,3,29,
233,8,29,1,29,4,29,236,8,29,11,29,12,29,237,3,29,240,8,29,1,29,3,29,243,
8,29,1,29,1,29,4,29,247,8,29,11,29,12,29,248,1,29,1,29,3,29,253,8,29,1,
29,4,29,256,8,29,11,29,12,29,257,3,29,260,8,29,3,29,262,8,29,1,30,1,30,
1,30,1,30,5,30,268,8,30,10,30,12,30,271,9,30,1,30,1,30,1,30,1,30,1,30,5,
30,278,8,30,10,30,12,30,281,9,30,1,30,3,30,284,8,30,1,31,1,31,5,31,288,
8,31,10,31,12,31,291,9,31,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,34,1,34,
1,34,1,34,1,34,1,34,1,34,4,34,307,8,34,11,34,12,34,308,5,34,311,8,34,10,
34,12,34,314,9,34,1,35,4,35,317,8,35,11,35,12,35,318,1,35,1,35,1,36,1,36,
1,37,4,37,326,8,37,11,37,12,37,327,0,0,38,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,28,57,0,59,29,61,30,63,
0,65,0,67,0,69,31,71,32,73,0,75,33,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,353,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,55,1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,
0,69,1,0,0,0,0,71,1,0,0,0,0,75,1,0,0,0,1,77,1,0,0,0,3,79,1,0,0,0,5,81,1,
0,0,0,7,83,1,0,0,0,9,85,1,0,0,0,11,90,1,0,0,0,13,92,1,0,0,0,15,95,1,0,0,
0,17,98,1,0,0,0,19,100,1,0,0,0,21,103,1,0,0,0,23,105,1,0,0,0,25,108,1,0,
0,0,27,113,1,0,0,0,29,119,1,0,0,0,31,127,1,0,0,0,33,135,1,0,0,0,35,142,
1,0,0,0,37,152,1,0,0,0,39,155,1,0,0,0,41,159,1,0,0,0,43,163,1,0,0,0,45,
166,1,0,0,0,47,175,1,0,0,0,49,179,1,0,0,0,51,186,1,0,0,0,53,193,1,0,0,0,
55,209,1,0,0,0,57,211,1,0,0,0,59,261,1,0,0,0,61,283,1,0,0,0,63,285,1,0,
0,0,65,292,1,0,0,0,67,295,1,0,0,0,69,299,1,0,0,0,71,316,1,0,0,0,73,322,
1,0,0,0,75,325,1,0,0,0,77,78,5,40,0,0,78,2,1,0,0,0,79,80,5,41,0,0,80,4,
1,0,0,0,81,82,5,91,0,0,82,6,1,0,0,0,83,84,5,93,0,0,84,8,1,0,0,0,85,86,5,
44,0,0,86,10,1,0,0,0,87,91,5,61,0,0,88,89,5,61,0,0,89,91,5,61,0,0,90,87,
1,0,0,0,90,88,1,0,0,0,91,12,1,0,0,0,92,93,5,33,0,0,93,94,5,61,0,0,94,14,
1,0,0,0,95,96,5,60,0,0,96,97,5,62,0,0,97,16,1,0,0,0,98,99,5,60,0,0,99,18,
1,0,0,0,100,101,5,60,0,0,101,102,5,61,0,0,102,20,1,0,0,0,103,104,5,62,0,
0,104,22,1,0,0,0,105,106,5,62,0,0,106,107,5,61,0,0,107,24,1,0,0,0,108,109,
7,0,0,0,109,110,7,1,0,0,110,111,7,2,0,0,111,112,7,3,0,0,112,26,1,0,0,0,
113,114,7,1,0,0,114,115,7,0,0,0,115,116,7,1,0,0,116,117,7,2,0,0,117,118,
7,3,0,0,118,28,1,0,0,0,119,120,7,4,0,0,120,121,7,3,0,0,121,122,7,5,0,0,
122,123,7,6,0,0,123,124,7,3,0,0,124,125,7,3,0,0,125,126,7,7,0,0,126,30,
1,0,0,0,127,128,7,3,0,0,128,129,7,8,0,0,129,130,7,1,0,0,130,131,7,9,0,0,
131,133,7,5,0,0,132,134,7,9,0,0,133,132,1,0,0,0,133,134,1,0,0,0,134,32,
1,0,0,0,135,136,7,10,0,0,136,137,7,3,0,0,137,138,7,11,0,0,138,139,7,3,0,
0,139,140,7,8,0,0,140,141,7,12,0,0,141,34,1,0,0,0,142,143,7,13,0,0,143,
144,7,14,0,0,144,145,7,7,0,0,145,146,7,5,0,0,146,147,7,15,0,0,147,148,7,
1,0,0,148,150,7,7,0,0,149,151,7,9,0,0,150,149,1,0,0,0,150,151,1,0,0,0,151,
36,1,0,0,0,152,153,7,1,0,0,153,154,7,7,0,0,154,38,1,0,0,0,155,156,7,7,0,
0,156,157,7,14,0,0,157,158,7,5,0,0,158,40,1,0,0,0,159,160,7,15,0,0,160,
161,7,7,0,0,161,162,7,16,0,0,162,42,1,0,0,0,163,164,7,14,0,0,164,165,7,
10,0,0,165,44,1,0,0,0,166,167,7,17,0,0,167,168,7,15,0,0,168,169,7,9,0,0,
169,170,7,5,0,0,170,171,7,14,0,0,171,172,7,2,0,0,172,173,7,3,0,0,173,174,
7,7,0,0,174,46,1,0,0,0,175,176,7,17,0,0,176,177,7,15,0,0,177,178,7,9,0,
0,178,48,1,0,0,0,179,180,7,17,0,0,180,181,7,15,0,0,181,182,7,9,0,0,182,
183,7,15,0,0,183,184,7,7,0,0,184,185,7,18,0,0,185,50,1,0,0,0,186,187,7,
17,0,0,187,188,7,15,0,0,188,189,7,9,0,0,189,190,7,15,0,0,190,191,7,0,0,
0,191,192,7,0,0,0,192,52,1,0,0,0,193,194,7,9,0,0,194,195,7,3,0,0,195,196,
7,15,0,0,196,197,7,10,0,0,197,198,7,13,0,0,198,199,7,17,0,0,199,54,1,0,
0,0,200,201,7,5,0,0,201,202,7,10,0,0,202,203,7,19,0,0,203,210,7,3,0,0,204,
205,7,20,0,0,205,206,7,15,0,0,206,207,7,0,0,0,207,208,7,9,0,0,208,210,7,
3,0,0,209,200,1,0,0,0,209,204,1,0,0,0,210,56,1,0,0,0,211,212,7,21,0,0,212,
58,1,0,0,0,213,215,3,57,28,0,214,213,1,0,0,0,214,215,1,0,0,0,215,217,1,
0,0,0,216,218,3,73,36,0,217,216,1,0,0,0,218,219,1,0,0,0,219,217,1,0,0,0,
219,220,1,0,0,0,220,228,1,0,0,0,221,225,5,46,0,0,222,224,3,73,36,0,223,
222,1,0,0,0,224,227,1,0,0,0,225,223,1,0,0,0,225,226,1,0,0,0,226,229,1,0,
0,0,227,225,1,0,0,0,228,221,1,0,0,0,228,229,1,0,0,0,229,239,1,0,0,0,230,
232,7,3,0,0,231,233,3,57,28,0,232,231,1,0,0,0,232,233,1,0,0,0,233,235,1,
0,0,0,234,236,3,73,36,0,235,234,1,0,0,0,236,237,1,0,0,0,237,235,1,0,0,0,
237,238,1,0,0,0,238,240,1,0,0,0,239,230,1,0,0,0,239,240,1,0,0,0,240,262,
1,0,0,0,241,243,3,57,28,0,242,241,1,0,0,0,242,243,1,0,0,0,243,244,1,0,0,
0,244,246,5,46,0,0,245,247,3,73,36,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,259,1,0,0,0,250,252,7,3,0,0,251,253,3,57,
28,0,252,251,1,0,0,0,252,253,1,0,0,0,253,255,1,0,0,0,254,256,3,73,36,0,
255,254,1,0,0,0,256,257,1,0,0,0,257,255,1,0,0,0,257,258,1,0,0,0,258,260,
1,0,0,0,259,250,1,0,0,0,259,260,1,0,0,0,260,262,1,0,0,0,261,214,1,0,0,0,
261,242,1,0,0,0,262,60,1,0,0,0,263,269,5,34,0,0,264,268,8,22,0,0,265,266,
5,92,0,0,266,268,9,0,0,0,267,264,1,0,0,0,267,265,1,0,0,0,268,271,1,0,0,
0,269,267,1,0,0,0,269,270,1,0,0,0,270,272,1,0,0,0,271,269,1,0,0,0,272,284,
5,34,0,0,273,279,5,39,0,0,274,278,8,23,0,0,275,276,5,92,0,0,276,278,9,0,
0,0,277,274,1,0,0,0,277,275,1,0,0,0,278,281,1,0,0,0,279,277,1,0,0,0,279,
280,1,0,0,0,280,282,1,0,0,0,281,279,1,0,0,0,282,284,5,39,0,0,283,263,1,
0,0,0,283,273,1,0,0,0,284,62,1,0,0,0,285,289,7,24,0,0,286,288,7,25,0,0,
287,286,1,0,0,0,288,291,1,0,0,0,289,287,1,0,0,0,289,290,1,0,0,0,290,64,
1,0,0,0,291,289,1,0,0,0,292,293,5,91,0,0,293,294,5,93,0,0,294,66,1,0,0,
0,295,296,5,91,0,0,296,297,5,42,0,0,297,298,5,93,0,0,298,68,1,0,0,0,299,
312,3,63,31,0,300,301,5,46,0,0,301,311,3,63,31,0,302,311,3,65,32,0,303,
311,3,67,33,0,304,306,5,46,0,0,305,307,3,73,36,0,306,305,1,0,0,0,307,308,
1,0,0,0,308,306,1,0,0,0,308,309,1,0,0,0,309,311,1,0,0,0,310,300,1,0,0,0,
310,302,1,0,0,0,310,303,1,0,0,0,310,304,1,0,0,0,311,314,1,0,0,0,312,310,
1,0,0,0,312,313,1,0,0,0,313,70,1,0,0,0,314,312,1,0,0,0,315,317,7,26,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,320,
1,0,0,0,320,321,6,35,0,0,321,72,1,0,0,0,322,323,7,27,0,0,323,74,1,0,0,0,
324,326,8,28,0,0,325,324,1,0,0,0,326,327,1,0,0,0,327,325,1,0,0,0,327,328,
1,0,0,0,328,76,1,0,0,0,29,0,90,133,150,209,214,219,225,228,232,237,239,
242,248,252,257,259,261,267,269,277,279,283,289,308,310,312,318,327,1,6,
0,0];
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,
7,38,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,93,8,5,1,6,
1,6,1,6,1,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,
12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,
1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,3,15,136,8,15,1,16,1,16,1,
16,1,16,1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,153,
8,17,1,18,1,18,1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,
21,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,
1,24,1,24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,
26,1,26,1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,5,27,
211,8,27,10,27,12,27,214,9,27,1,27,1,27,1,28,1,28,1,28,1,28,1,28,1,28,1,
28,1,28,1,28,3,28,227,8,28,1,29,1,29,1,30,3,30,232,8,30,1,30,4,30,235,8,
30,11,30,12,30,236,1,30,1,30,5,30,241,8,30,10,30,12,30,244,9,30,3,30,246,
8,30,1,30,1,30,3,30,250,8,30,1,30,4,30,253,8,30,11,30,12,30,254,3,30,257,
8,30,1,30,3,30,260,8,30,1,30,1,30,4,30,264,8,30,11,30,12,30,265,1,30,1,
30,3,30,270,8,30,1,30,4,30,273,8,30,11,30,12,30,274,3,30,277,8,30,3,30,
279,8,30,1,31,1,31,1,31,1,31,5,31,285,8,31,10,31,12,31,288,9,31,1,31,1,
31,1,31,1,31,1,31,5,31,295,8,31,10,31,12,31,298,9,31,1,31,3,31,301,8,31,
1,32,1,32,5,32,305,8,32,10,32,12,32,308,9,32,1,33,1,33,1,33,1,34,1,34,1,
34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1,35,4,35,324,8,35,11,35,12,35,325,
5,35,328,8,35,10,35,12,35,331,9,35,1,36,4,36,334,8,36,11,36,12,36,335,1,
36,1,36,1,37,1,37,1,38,4,38,343,8,38,11,38,12,38,344,0,0,39,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,28,57,
29,59,0,61,30,63,31,65,0,67,0,69,0,71,32,73,33,75,0,77,34,1,0,30,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,9,9,32,32,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,371,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,55,1,0,0,0,0,57,1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,71,1,0,0,0,0,73,
1,0,0,0,0,77,1,0,0,0,1,79,1,0,0,0,3,81,1,0,0,0,5,83,1,0,0,0,7,85,1,0,0,
0,9,87,1,0,0,0,11,92,1,0,0,0,13,94,1,0,0,0,15,97,1,0,0,0,17,100,1,0,0,0,
19,102,1,0,0,0,21,105,1,0,0,0,23,107,1,0,0,0,25,110,1,0,0,0,27,115,1,0,
0,0,29,121,1,0,0,0,31,129,1,0,0,0,33,137,1,0,0,0,35,144,1,0,0,0,37,154,
1,0,0,0,39,157,1,0,0,0,41,161,1,0,0,0,43,165,1,0,0,0,45,168,1,0,0,0,47,
177,1,0,0,0,49,181,1,0,0,0,51,188,1,0,0,0,53,195,1,0,0,0,55,202,1,0,0,0,
57,226,1,0,0,0,59,228,1,0,0,0,61,278,1,0,0,0,63,300,1,0,0,0,65,302,1,0,
0,0,67,309,1,0,0,0,69,312,1,0,0,0,71,316,1,0,0,0,73,333,1,0,0,0,75,339,
1,0,0,0,77,342,1,0,0,0,79,80,5,40,0,0,80,2,1,0,0,0,81,82,5,41,0,0,82,4,
1,0,0,0,83,84,5,91,0,0,84,6,1,0,0,0,85,86,5,93,0,0,86,8,1,0,0,0,87,88,5,
44,0,0,88,10,1,0,0,0,89,93,5,61,0,0,90,91,5,61,0,0,91,93,5,61,0,0,92,89,
1,0,0,0,92,90,1,0,0,0,93,12,1,0,0,0,94,95,5,33,0,0,95,96,5,61,0,0,96,14,
1,0,0,0,97,98,5,60,0,0,98,99,5,62,0,0,99,16,1,0,0,0,100,101,5,60,0,0,101,
18,1,0,0,0,102,103,5,60,0,0,103,104,5,61,0,0,104,20,1,0,0,0,105,106,5,62,
0,0,106,22,1,0,0,0,107,108,5,62,0,0,108,109,5,61,0,0,109,24,1,0,0,0,110,
111,7,0,0,0,111,112,7,1,0,0,112,113,7,2,0,0,113,114,7,3,0,0,114,26,1,0,
0,0,115,116,7,1,0,0,116,117,7,0,0,0,117,118,7,1,0,0,118,119,7,2,0,0,119,
120,7,3,0,0,120,28,1,0,0,0,121,122,7,4,0,0,122,123,7,3,0,0,123,124,7,5,
0,0,124,125,7,6,0,0,125,126,7,3,0,0,126,127,7,3,0,0,127,128,7,7,0,0,128,
30,1,0,0,0,129,130,7,3,0,0,130,131,7,8,0,0,131,132,7,1,0,0,132,133,7,9,
0,0,133,135,7,5,0,0,134,136,7,9,0,0,135,134,1,0,0,0,135,136,1,0,0,0,136,
32,1,0,0,0,137,138,7,10,0,0,138,139,7,3,0,0,139,140,7,11,0,0,140,141,7,
3,0,0,141,142,7,8,0,0,142,143,7,12,0,0,143,34,1,0,0,0,144,145,7,13,0,0,
145,146,7,14,0,0,146,147,7,7,0,0,147,148,7,5,0,0,148,149,7,15,0,0,149,150,
7,1,0,0,150,152,7,7,0,0,151,153,7,9,0,0,152,151,1,0,0,0,152,153,1,0,0,0,
153,36,1,0,0,0,154,155,7,1,0,0,155,156,7,7,0,0,156,38,1,0,0,0,157,158,7,
7,0,0,158,159,7,14,0,0,159,160,7,5,0,0,160,40,1,0,0,0,161,162,7,15,0,0,
162,163,7,7,0,0,163,164,7,16,0,0,164,42,1,0,0,0,165,166,7,14,0,0,166,167,
7,10,0,0,167,44,1,0,0,0,168,169,7,17,0,0,169,170,7,15,0,0,170,171,7,9,0,
0,171,172,7,5,0,0,172,173,7,14,0,0,173,174,7,2,0,0,174,175,7,3,0,0,175,
176,7,7,0,0,176,46,1,0,0,0,177,178,7,17,0,0,178,179,7,15,0,0,179,180,7,
9,0,0,180,48,1,0,0,0,181,182,7,17,0,0,182,183,7,15,0,0,183,184,7,9,0,0,
184,185,7,15,0,0,185,186,7,7,0,0,186,187,7,18,0,0,187,50,1,0,0,0,188,189,
7,17,0,0,189,190,7,15,0,0,190,191,7,9,0,0,191,192,7,15,0,0,192,193,7,0,
0,0,193,194,7,0,0,0,194,52,1,0,0,0,195,196,7,9,0,0,196,197,7,3,0,0,197,
198,7,15,0,0,198,199,7,10,0,0,199,200,7,13,0,0,200,201,7,17,0,0,201,54,
1,0,0,0,202,203,7,12,0,0,203,204,7,15,0,0,204,205,7,5,0,0,205,206,7,5,0,
0,206,207,7,3,0,0,207,208,7,10,0,0,208,212,7,7,0,0,209,211,7,19,0,0,210,
209,1,0,0,0,211,214,1,0,0,0,212,210,1,0,0,0,212,213,1,0,0,0,213,215,1,0,
0,0,214,212,1,0,0,0,215,216,5,40,0,0,216,56,1,0,0,0,217,218,7,5,0,0,218,
219,7,10,0,0,219,220,7,20,0,0,220,227,7,3,0,0,221,222,7,21,0,0,222,223,
7,15,0,0,223,224,7,0,0,0,224,225,7,9,0,0,225,227,7,3,0,0,226,217,1,0,0,
0,226,221,1,0,0,0,227,58,1,0,0,0,228,229,7,22,0,0,229,60,1,0,0,0,230,232,
3,59,29,0,231,230,1,0,0,0,231,232,1,0,0,0,232,234,1,0,0,0,233,235,3,75,
37,0,234,233,1,0,0,0,235,236,1,0,0,0,236,234,1,0,0,0,236,237,1,0,0,0,237,
245,1,0,0,0,238,242,5,46,0,0,239,241,3,75,37,0,240,239,1,0,0,0,241,244,
1,0,0,0,242,240,1,0,0,0,242,243,1,0,0,0,243,246,1,0,0,0,244,242,1,0,0,0,
245,238,1,0,0,0,245,246,1,0,0,0,246,256,1,0,0,0,247,249,7,3,0,0,248,250,
3,59,29,0,249,248,1,0,0,0,249,250,1,0,0,0,250,252,1,0,0,0,251,253,3,75,
37,0,252,251,1,0,0,0,253,254,1,0,0,0,254,252,1,0,0,0,254,255,1,0,0,0,255,
257,1,0,0,0,256,247,1,0,0,0,256,257,1,0,0,0,257,279,1,0,0,0,258,260,3,59,
29,0,259,258,1,0,0,0,259,260,1,0,0,0,260,261,1,0,0,0,261,263,5,46,0,0,262,
264,3,75,37,0,263,262,1,0,0,0,264,265,1,0,0,0,265,263,1,0,0,0,265,266,1,
0,0,0,266,276,1,0,0,0,267,269,7,3,0,0,268,270,3,59,29,0,269,268,1,0,0,0,
269,270,1,0,0,0,270,272,1,0,0,0,271,273,3,75,37,0,272,271,1,0,0,0,273,274,
1,0,0,0,274,272,1,0,0,0,274,275,1,0,0,0,275,277,1,0,0,0,276,267,1,0,0,0,
276,277,1,0,0,0,277,279,1,0,0,0,278,231,1,0,0,0,278,259,1,0,0,0,279,62,
1,0,0,0,280,286,5,34,0,0,281,285,8,23,0,0,282,283,5,92,0,0,283,285,9,0,
0,0,284,281,1,0,0,0,284,282,1,0,0,0,285,288,1,0,0,0,286,284,1,0,0,0,286,
287,1,0,0,0,287,289,1,0,0,0,288,286,1,0,0,0,289,301,5,34,0,0,290,296,5,
39,0,0,291,295,8,24,0,0,292,293,5,92,0,0,293,295,9,0,0,0,294,291,1,0,0,
0,294,292,1,0,0,0,295,298,1,0,0,0,296,294,1,0,0,0,296,297,1,0,0,0,297,299,
1,0,0,0,298,296,1,0,0,0,299,301,5,39,0,0,300,280,1,0,0,0,300,290,1,0,0,
0,301,64,1,0,0,0,302,306,7,25,0,0,303,305,7,26,0,0,304,303,1,0,0,0,305,
308,1,0,0,0,306,304,1,0,0,0,306,307,1,0,0,0,307,66,1,0,0,0,308,306,1,0,
0,0,309,310,5,91,0,0,310,311,5,93,0,0,311,68,1,0,0,0,312,313,5,91,0,0,313,
314,5,42,0,0,314,315,5,93,0,0,315,70,1,0,0,0,316,329,3,65,32,0,317,318,
5,46,0,0,318,328,3,65,32,0,319,328,3,67,33,0,320,328,3,69,34,0,321,323,
5,46,0,0,322,324,3,75,37,0,323,322,1,0,0,0,324,325,1,0,0,0,325,323,1,0,
0,0,325,326,1,0,0,0,326,328,1,0,0,0,327,317,1,0,0,0,327,319,1,0,0,0,327,
320,1,0,0,0,327,321,1,0,0,0,328,331,1,0,0,0,329,327,1,0,0,0,329,330,1,0,
0,0,330,72,1,0,0,0,331,329,1,0,0,0,332,334,7,27,0,0,333,332,1,0,0,0,334,
335,1,0,0,0,335,333,1,0,0,0,335,336,1,0,0,0,336,337,1,0,0,0,337,338,6,36,
0,0,338,74,1,0,0,0,339,340,7,28,0,0,340,76,1,0,0,0,341,343,8,29,0,0,342,
341,1,0,0,0,343,344,1,0,0,0,344,342,1,0,0,0,344,345,1,0,0,0,345,78,1,0,
0,0,30,0,92,135,152,212,226,231,236,242,245,249,254,256,259,265,269,274,
276,278,284,286,294,296,300,306,325,327,329,335,344,1,6,0,0];
private static __ATN: ATN;
public static get _ATN(): ATN {

View File

@@ -16,6 +16,7 @@ import { ValueListContext } from "./FilterQueryParser.js";
import { FullTextContext } from "./FilterQueryParser.js";
import { FunctionCallContext } from "./FilterQueryParser.js";
import { SearchCallContext } from "./FilterQueryParser.js";
import { PatternCallContext } from "./FilterQueryParser.js";
import { FunctionParamListContext } from "./FilterQueryParser.js";
import { FunctionParamContext } from "./FilterQueryParser.js";
import { ArrayContext } from "./FilterQueryParser.js";
@@ -158,6 +159,16 @@ export default class FilterQueryListener extends ParseTreeListener {
* @param ctx the parse tree
*/
exitSearchCall?: (ctx: SearchCallContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.patternCall`.
* @param ctx the parse tree
*/
enterPatternCall?: (ctx: PatternCallContext) => void;
/**
* Exit a parse tree produced by `FilterQueryParser.patternCall`.
* @param ctx the parse tree
*/
exitPatternCall?: (ctx: PatternCallContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.functionParamList`.
* @param ctx the parse tree

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ import { ValueListContext } from "./FilterQueryParser.js";
import { FullTextContext } from "./FilterQueryParser.js";
import { FunctionCallContext } from "./FilterQueryParser.js";
import { SearchCallContext } from "./FilterQueryParser.js";
import { PatternCallContext } from "./FilterQueryParser.js";
import { FunctionParamListContext } from "./FilterQueryParser.js";
import { FunctionParamContext } from "./FilterQueryParser.js";
import { ArrayContext } from "./FilterQueryParser.js";
@@ -109,6 +110,12 @@ export default class FilterQueryVisitor<Result> extends ParseTreeVisitor<Result>
* @return the visitor result
*/
visitSearchCall?: (ctx: SearchCallContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.patternCall`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPatternCall?: (ctx: PatternCallContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.functionParamList`.
* @param ctx the parse tree

View File

@@ -26,11 +26,9 @@ export interface AlertDef {
labels?: Labels;
annotations?: Labels;
evalWindow?: string;
source?: string;
disabled?: boolean;
preferredChannels?: string[];
broadcastToAll?: boolean;
version?: string;
}
export interface RuleCondition {

View File

@@ -38,6 +38,7 @@ primary
| comparison
| functionCall
| searchCall
| patternCall
| fullText
| key
| value
@@ -121,6 +122,16 @@ searchCall
;
// Function parameters can be keys, single scalar values, or arrays
/*
* Log pattern filter: pattern('User <*> logged in from <IP>'[, '...']) keeps the
* logs whose pattern is one of the given pattern texts. Handled in the visitor.
* PATTERN_CALL carries its opening parenthesis, so a bare `pattern` stays a key
* or a full-text word.
*/
patternCall
: PATTERN_CALL valueList RPAREN
;
functionParamList
: functionParam ( COMMA functionParam )*
;
@@ -195,6 +206,7 @@ HAS : [Hh][Aa][Ss] ;
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
SEARCH : [Ss][Ee][Aa][Rr][Cc][Hh] ;
PATTERN_CALL : [Pp][Aa][Tt][Tt][Ee][Rr][Nn] [ \t]* '(' ;
// Potential boolean constants
BOOL

View File

@@ -33,7 +33,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "METRIC_BASED_ALERT",
"description": "CPU usage for api-service pods exceeds 80% of the requested CPU",
"ruleType": "threshold_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{
@@ -93,7 +92,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "METRIC_BASED_ALERT",
"description": "Disk utilization for a persistent volume is above 80%",
"ruleType": "threshold_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{
@@ -176,7 +174,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "METRIC_BASED_ALERT",
"description": "Consumer group lag computed via PromQL",
"ruleType": "promql_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{
@@ -228,7 +225,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "METRIC_BASED_ALERT",
"description": "Detect an abrupt drop in span ingestion using a z-score anomaly function",
"ruleType": "anomaly_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{
@@ -293,7 +289,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "LOGS_BASED_ALERT",
"description": "Any panic log line emitted by the payments service",
"ruleType": "threshold_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{
@@ -352,7 +347,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "LOGS_BASED_ALERT",
"description": "Error log ratio as a percentage of total logs for payments-api",
"ruleType": "threshold_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{
@@ -429,7 +423,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "TRACES_BASED_ALERT",
"description": "p99 duration of the search endpoint exceeds 5s",
"ruleType": "threshold_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{
@@ -490,7 +483,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "TRACES_BASED_ALERT",
"description": "Request error rate for search-api, grouped by route",
"ruleType": "threshold_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{
@@ -573,7 +565,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "METRIC_BASED_ALERT",
"description": "Warn at lag ≥ 50 and page at ≥ 200, tiered via thresholds.spec.",
"ruleType": "threshold_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{
@@ -664,7 +655,6 @@ func postableRuleExamples() []handler.OpenAPIExample {
"alertType": "TRACES_BASED_ALERT",
"description": "Noise-controlled 5xx error rate alert with renotify on gaps",
"ruleType": "threshold_rule",
"version": "v5",
"schemaVersion": "v2alpha1",
"condition": map[string]any{
"compositeQuery": map[string]any{

View File

@@ -176,6 +176,8 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
ctx.FunctionCall().Accept(r)
} else if ctx.SearchCall() != nil {
ctx.SearchCall().Accept(r)
} else if ctx.PatternCall() != nil {
ctx.PatternCall().Accept(r)
} else if ctx.FullText() != nil {
ctx.FullText().Accept(r)
} else if ctx.Key() != nil {
@@ -374,6 +376,13 @@ func (r *WhereClauseRewriter) VisitSearchCall(ctx *parser.SearchCallContext) any
return nil
}
// VisitPatternCall visits pattern() calls. It has no keys to rewrite, so it
// is preserved verbatim.
func (r *WhereClauseRewriter) VisitPatternCall(ctx *parser.PatternCallContext) any {
r.rewritten.WriteString(ctx.GetText())
return nil
}
// VisitFunctionCall visits function calls.
func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) any {
// Write function name

File diff suppressed because one or more lines are too long

View File

@@ -25,12 +25,13 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
PATTERN_CALL=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

@@ -25,12 +25,13 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
PATTERN_CALL=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
'('=1
')'=2
'['=3

View File

@@ -99,6 +99,12 @@ func (s *BaseFilterQueryListener) EnterSearchCall(ctx *SearchCallContext) {}
// ExitSearchCall is called when production searchCall is exited.
func (s *BaseFilterQueryListener) ExitSearchCall(ctx *SearchCallContext) {}
// EnterPatternCall is called when production patternCall is entered.
func (s *BaseFilterQueryListener) EnterPatternCall(ctx *PatternCallContext) {}
// ExitPatternCall is called when production patternCall is exited.
func (s *BaseFilterQueryListener) ExitPatternCall(ctx *PatternCallContext) {}
// EnterFunctionParamList is called when production functionParamList is entered.
func (s *BaseFilterQueryListener) EnterFunctionParamList(ctx *FunctionParamListContext) {}

View File

@@ -60,6 +60,10 @@ func (v *BaseFilterQueryVisitor) VisitSearchCall(ctx *SearchCallContext) interfa
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitPatternCall(ctx *PatternCallContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitFunctionParamList(ctx *FunctionParamListContext) interface{} {
return v.VisitChildren(ctx)
}

View File

@@ -50,174 +50,184 @@ func filterquerylexerLexerInit() {
"", "LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
"HASALL", "SEARCH", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
"HASALL", "SEARCH", "PATTERN_CALL", "BOOL", "NUMBER", "QUOTED_TEXT",
"KEY", "WS", "FREETEXT",
}
staticData.RuleNames = []string{
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
"HASALL", "SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
"HASALL", "SEARCH", "PATTERN_CALL", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT",
"SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT",
"FREETEXT",
}
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
staticData.serializedATN = []int32{
4, 0, 33, 329, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 0, 34, 346, 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, 2, 24, 7, 24, 2, 25, 7, 25,
2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2,
31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36,
7, 36, 2, 37, 7, 37, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1,
4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 91, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7,
1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11,
1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1,
13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15,
1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 134, 8, 15, 1, 16, 1, 16, 1, 16, 1,
16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17,
1, 17, 3, 17, 151, 8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1,
19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22,
1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1,
24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25,
1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1,
27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 210,
8, 27, 1, 28, 1, 28, 1, 29, 3, 29, 215, 8, 29, 1, 29, 4, 29, 218, 8, 29,
11, 29, 12, 29, 219, 1, 29, 1, 29, 5, 29, 224, 8, 29, 10, 29, 12, 29, 227,
9, 29, 3, 29, 229, 8, 29, 1, 29, 1, 29, 3, 29, 233, 8, 29, 1, 29, 4, 29,
236, 8, 29, 11, 29, 12, 29, 237, 3, 29, 240, 8, 29, 1, 29, 3, 29, 243,
8, 29, 1, 29, 1, 29, 4, 29, 247, 8, 29, 11, 29, 12, 29, 248, 1, 29, 1,
29, 3, 29, 253, 8, 29, 1, 29, 4, 29, 256, 8, 29, 11, 29, 12, 29, 257, 3,
29, 260, 8, 29, 3, 29, 262, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 5, 30, 268,
8, 30, 10, 30, 12, 30, 271, 9, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 5,
30, 278, 8, 30, 10, 30, 12, 30, 281, 9, 30, 1, 30, 3, 30, 284, 8, 30, 1,
31, 1, 31, 5, 31, 288, 8, 31, 10, 31, 12, 31, 291, 9, 31, 1, 32, 1, 32,
1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1,
34, 1, 34, 4, 34, 307, 8, 34, 11, 34, 12, 34, 308, 5, 34, 311, 8, 34, 10,
34, 12, 34, 314, 9, 34, 1, 35, 4, 35, 317, 8, 35, 11, 35, 12, 35, 318,
1, 35, 1, 35, 1, 36, 1, 36, 1, 37, 4, 37, 326, 8, 37, 11, 37, 12, 37, 327,
0, 0, 38, 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,
28, 57, 0, 59, 29, 61, 30, 63, 0, 65, 0, 67, 0, 69, 31, 71, 32, 73, 0,
75, 33, 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, 353,
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, 55, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 69, 1, 0, 0,
0, 0, 71, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 1, 77, 1, 0, 0, 0, 3, 79, 1, 0,
0, 0, 5, 81, 1, 0, 0, 0, 7, 83, 1, 0, 0, 0, 9, 85, 1, 0, 0, 0, 11, 90,
1, 0, 0, 0, 13, 92, 1, 0, 0, 0, 15, 95, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0,
19, 100, 1, 0, 0, 0, 21, 103, 1, 0, 0, 0, 23, 105, 1, 0, 0, 0, 25, 108,
1, 0, 0, 0, 27, 113, 1, 0, 0, 0, 29, 119, 1, 0, 0, 0, 31, 127, 1, 0, 0,
0, 33, 135, 1, 0, 0, 0, 35, 142, 1, 0, 0, 0, 37, 152, 1, 0, 0, 0, 39, 155,
1, 0, 0, 0, 41, 159, 1, 0, 0, 0, 43, 163, 1, 0, 0, 0, 45, 166, 1, 0, 0,
0, 47, 175, 1, 0, 0, 0, 49, 179, 1, 0, 0, 0, 51, 186, 1, 0, 0, 0, 53, 193,
1, 0, 0, 0, 55, 209, 1, 0, 0, 0, 57, 211, 1, 0, 0, 0, 59, 261, 1, 0, 0,
0, 61, 283, 1, 0, 0, 0, 63, 285, 1, 0, 0, 0, 65, 292, 1, 0, 0, 0, 67, 295,
1, 0, 0, 0, 69, 299, 1, 0, 0, 0, 71, 316, 1, 0, 0, 0, 73, 322, 1, 0, 0,
0, 75, 325, 1, 0, 0, 0, 77, 78, 5, 40, 0, 0, 78, 2, 1, 0, 0, 0, 79, 80,
5, 41, 0, 0, 80, 4, 1, 0, 0, 0, 81, 82, 5, 91, 0, 0, 82, 6, 1, 0, 0, 0,
83, 84, 5, 93, 0, 0, 84, 8, 1, 0, 0, 0, 85, 86, 5, 44, 0, 0, 86, 10, 1,
0, 0, 0, 87, 91, 5, 61, 0, 0, 88, 89, 5, 61, 0, 0, 89, 91, 5, 61, 0, 0,
90, 87, 1, 0, 0, 0, 90, 88, 1, 0, 0, 0, 91, 12, 1, 0, 0, 0, 92, 93, 5,
33, 0, 0, 93, 94, 5, 61, 0, 0, 94, 14, 1, 0, 0, 0, 95, 96, 5, 60, 0, 0,
96, 97, 5, 62, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 18, 1,
0, 0, 0, 100, 101, 5, 60, 0, 0, 101, 102, 5, 61, 0, 0, 102, 20, 1, 0, 0,
0, 103, 104, 5, 62, 0, 0, 104, 22, 1, 0, 0, 0, 105, 106, 5, 62, 0, 0, 106,
107, 5, 61, 0, 0, 107, 24, 1, 0, 0, 0, 108, 109, 7, 0, 0, 0, 109, 110,
7, 1, 0, 0, 110, 111, 7, 2, 0, 0, 111, 112, 7, 3, 0, 0, 112, 26, 1, 0,
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 0, 0, 0, 115, 116, 7, 1, 0, 0,
116, 117, 7, 2, 0, 0, 117, 118, 7, 3, 0, 0, 118, 28, 1, 0, 0, 0, 119, 120,
7, 4, 0, 0, 120, 121, 7, 3, 0, 0, 121, 122, 7, 5, 0, 0, 122, 123, 7, 6,
0, 0, 123, 124, 7, 3, 0, 0, 124, 125, 7, 3, 0, 0, 125, 126, 7, 7, 0, 0,
126, 30, 1, 0, 0, 0, 127, 128, 7, 3, 0, 0, 128, 129, 7, 8, 0, 0, 129, 130,
7, 1, 0, 0, 130, 131, 7, 9, 0, 0, 131, 133, 7, 5, 0, 0, 132, 134, 7, 9,
0, 0, 133, 132, 1, 0, 0, 0, 133, 134, 1, 0, 0, 0, 134, 32, 1, 0, 0, 0,
135, 136, 7, 10, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 11, 0, 0, 138,
139, 7, 3, 0, 0, 139, 140, 7, 8, 0, 0, 140, 141, 7, 12, 0, 0, 141, 34,
1, 0, 0, 0, 142, 143, 7, 13, 0, 0, 143, 144, 7, 14, 0, 0, 144, 145, 7,
7, 0, 0, 145, 146, 7, 5, 0, 0, 146, 147, 7, 15, 0, 0, 147, 148, 7, 1, 0,
0, 148, 150, 7, 7, 0, 0, 149, 151, 7, 9, 0, 0, 150, 149, 1, 0, 0, 0, 150,
151, 1, 0, 0, 0, 151, 36, 1, 0, 0, 0, 152, 153, 7, 1, 0, 0, 153, 154, 7,
7, 0, 0, 154, 38, 1, 0, 0, 0, 155, 156, 7, 7, 0, 0, 156, 157, 7, 14, 0,
0, 157, 158, 7, 5, 0, 0, 158, 40, 1, 0, 0, 0, 159, 160, 7, 15, 0, 0, 160,
161, 7, 7, 0, 0, 161, 162, 7, 16, 0, 0, 162, 42, 1, 0, 0, 0, 163, 164,
7, 14, 0, 0, 164, 165, 7, 10, 0, 0, 165, 44, 1, 0, 0, 0, 166, 167, 7, 17,
0, 0, 167, 168, 7, 15, 0, 0, 168, 169, 7, 9, 0, 0, 169, 170, 7, 5, 0, 0,
170, 171, 7, 14, 0, 0, 171, 172, 7, 2, 0, 0, 172, 173, 7, 3, 0, 0, 173,
174, 7, 7, 0, 0, 174, 46, 1, 0, 0, 0, 175, 176, 7, 17, 0, 0, 176, 177,
7, 15, 0, 0, 177, 178, 7, 9, 0, 0, 178, 48, 1, 0, 0, 0, 179, 180, 7, 17,
0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 9, 0, 0, 182, 183, 7, 15, 0,
0, 183, 184, 7, 7, 0, 0, 184, 185, 7, 18, 0, 0, 185, 50, 1, 0, 0, 0, 186,
187, 7, 17, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 9, 0, 0, 189, 190,
7, 15, 0, 0, 190, 191, 7, 0, 0, 0, 191, 192, 7, 0, 0, 0, 192, 52, 1, 0,
0, 0, 193, 194, 7, 9, 0, 0, 194, 195, 7, 3, 0, 0, 195, 196, 7, 15, 0, 0,
196, 197, 7, 10, 0, 0, 197, 198, 7, 13, 0, 0, 198, 199, 7, 17, 0, 0, 199,
54, 1, 0, 0, 0, 200, 201, 7, 5, 0, 0, 201, 202, 7, 10, 0, 0, 202, 203,
7, 19, 0, 0, 203, 210, 7, 3, 0, 0, 204, 205, 7, 20, 0, 0, 205, 206, 7,
15, 0, 0, 206, 207, 7, 0, 0, 0, 207, 208, 7, 9, 0, 0, 208, 210, 7, 3, 0,
0, 209, 200, 1, 0, 0, 0, 209, 204, 1, 0, 0, 0, 210, 56, 1, 0, 0, 0, 211,
212, 7, 21, 0, 0, 212, 58, 1, 0, 0, 0, 213, 215, 3, 57, 28, 0, 214, 213,
1, 0, 0, 0, 214, 215, 1, 0, 0, 0, 215, 217, 1, 0, 0, 0, 216, 218, 3, 73,
36, 0, 217, 216, 1, 0, 0, 0, 218, 219, 1, 0, 0, 0, 219, 217, 1, 0, 0, 0,
219, 220, 1, 0, 0, 0, 220, 228, 1, 0, 0, 0, 221, 225, 5, 46, 0, 0, 222,
224, 3, 73, 36, 0, 223, 222, 1, 0, 0, 0, 224, 227, 1, 0, 0, 0, 225, 223,
1, 0, 0, 0, 225, 226, 1, 0, 0, 0, 226, 229, 1, 0, 0, 0, 227, 225, 1, 0,
0, 0, 228, 221, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 239, 1, 0, 0, 0,
230, 232, 7, 3, 0, 0, 231, 233, 3, 57, 28, 0, 232, 231, 1, 0, 0, 0, 232,
233, 1, 0, 0, 0, 233, 235, 1, 0, 0, 0, 234, 236, 3, 73, 36, 0, 235, 234,
1, 0, 0, 0, 236, 237, 1, 0, 0, 0, 237, 235, 1, 0, 0, 0, 237, 238, 1, 0,
0, 0, 238, 240, 1, 0, 0, 0, 239, 230, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0,
240, 262, 1, 0, 0, 0, 241, 243, 3, 57, 28, 0, 242, 241, 1, 0, 0, 0, 242,
243, 1, 0, 0, 0, 243, 244, 1, 0, 0, 0, 244, 246, 5, 46, 0, 0, 245, 247,
3, 73, 36, 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, 259, 1, 0, 0, 0, 250, 252, 7, 3, 0,
0, 251, 253, 3, 57, 28, 0, 252, 251, 1, 0, 0, 0, 252, 253, 1, 0, 0, 0,
253, 255, 1, 0, 0, 0, 254, 256, 3, 73, 36, 0, 255, 254, 1, 0, 0, 0, 256,
257, 1, 0, 0, 0, 257, 255, 1, 0, 0, 0, 257, 258, 1, 0, 0, 0, 258, 260,
1, 0, 0, 0, 259, 250, 1, 0, 0, 0, 259, 260, 1, 0, 0, 0, 260, 262, 1, 0,
0, 0, 261, 214, 1, 0, 0, 0, 261, 242, 1, 0, 0, 0, 262, 60, 1, 0, 0, 0,
263, 269, 5, 34, 0, 0, 264, 268, 8, 22, 0, 0, 265, 266, 5, 92, 0, 0, 266,
268, 9, 0, 0, 0, 267, 264, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 271,
1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 272, 1, 0,
0, 0, 271, 269, 1, 0, 0, 0, 272, 284, 5, 34, 0, 0, 273, 279, 5, 39, 0,
0, 274, 278, 8, 23, 0, 0, 275, 276, 5, 92, 0, 0, 276, 278, 9, 0, 0, 0,
277, 274, 1, 0, 0, 0, 277, 275, 1, 0, 0, 0, 278, 281, 1, 0, 0, 0, 279,
277, 1, 0, 0, 0, 279, 280, 1, 0, 0, 0, 280, 282, 1, 0, 0, 0, 281, 279,
1, 0, 0, 0, 282, 284, 5, 39, 0, 0, 283, 263, 1, 0, 0, 0, 283, 273, 1, 0,
0, 0, 284, 62, 1, 0, 0, 0, 285, 289, 7, 24, 0, 0, 286, 288, 7, 25, 0, 0,
287, 286, 1, 0, 0, 0, 288, 291, 1, 0, 0, 0, 289, 287, 1, 0, 0, 0, 289,
290, 1, 0, 0, 0, 290, 64, 1, 0, 0, 0, 291, 289, 1, 0, 0, 0, 292, 293, 5,
91, 0, 0, 293, 294, 5, 93, 0, 0, 294, 66, 1, 0, 0, 0, 295, 296, 5, 91,
0, 0, 296, 297, 5, 42, 0, 0, 297, 298, 5, 93, 0, 0, 298, 68, 1, 0, 0, 0,
299, 312, 3, 63, 31, 0, 300, 301, 5, 46, 0, 0, 301, 311, 3, 63, 31, 0,
302, 311, 3, 65, 32, 0, 303, 311, 3, 67, 33, 0, 304, 306, 5, 46, 0, 0,
305, 307, 3, 73, 36, 0, 306, 305, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308,
306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 311, 1, 0, 0, 0, 310, 300,
1, 0, 0, 0, 310, 302, 1, 0, 0, 0, 310, 303, 1, 0, 0, 0, 310, 304, 1, 0,
0, 0, 311, 314, 1, 0, 0, 0, 312, 310, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0,
313, 70, 1, 0, 0, 0, 314, 312, 1, 0, 0, 0, 315, 317, 7, 26, 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, 320, 1, 0, 0, 0, 320, 321, 6, 35, 0, 0, 321, 72, 1, 0,
0, 0, 322, 323, 7, 27, 0, 0, 323, 74, 1, 0, 0, 0, 324, 326, 8, 28, 0, 0,
325, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 325, 1, 0, 0, 0, 327,
328, 1, 0, 0, 0, 328, 76, 1, 0, 0, 0, 29, 0, 90, 133, 150, 209, 214, 219,
225, 228, 232, 237, 239, 242, 248, 252, 257, 259, 261, 267, 269, 277, 279,
283, 289, 308, 310, 312, 318, 327, 1, 6, 0, 0,
7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2,
1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 93, 8, 5, 1, 6, 1, 6, 1,
6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11,
1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1,
13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14,
1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 136, 8, 15, 1, 16, 1,
16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17,
1, 17, 1, 17, 1, 17, 3, 17, 153, 8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1,
19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22,
1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1,
23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25,
1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1,
26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 5, 27,
211, 8, 27, 10, 27, 12, 27, 214, 9, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1,
28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 3, 28, 227, 8, 28, 1, 29,
1, 29, 1, 30, 3, 30, 232, 8, 30, 1, 30, 4, 30, 235, 8, 30, 11, 30, 12,
30, 236, 1, 30, 1, 30, 5, 30, 241, 8, 30, 10, 30, 12, 30, 244, 9, 30, 3,
30, 246, 8, 30, 1, 30, 1, 30, 3, 30, 250, 8, 30, 1, 30, 4, 30, 253, 8,
30, 11, 30, 12, 30, 254, 3, 30, 257, 8, 30, 1, 30, 3, 30, 260, 8, 30, 1,
30, 1, 30, 4, 30, 264, 8, 30, 11, 30, 12, 30, 265, 1, 30, 1, 30, 3, 30,
270, 8, 30, 1, 30, 4, 30, 273, 8, 30, 11, 30, 12, 30, 274, 3, 30, 277,
8, 30, 3, 30, 279, 8, 30, 1, 31, 1, 31, 1, 31, 1, 31, 5, 31, 285, 8, 31,
10, 31, 12, 31, 288, 9, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 5, 31, 295,
8, 31, 10, 31, 12, 31, 298, 9, 31, 1, 31, 3, 31, 301, 8, 31, 1, 32, 1,
32, 5, 32, 305, 8, 32, 10, 32, 12, 32, 308, 9, 32, 1, 33, 1, 33, 1, 33,
1, 34, 1, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1,
35, 4, 35, 324, 8, 35, 11, 35, 12, 35, 325, 5, 35, 328, 8, 35, 10, 35,
12, 35, 331, 9, 35, 1, 36, 4, 36, 334, 8, 36, 11, 36, 12, 36, 335, 1, 36,
1, 36, 1, 37, 1, 37, 1, 38, 4, 38, 343, 8, 38, 11, 38, 12, 38, 344, 0,
0, 39, 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, 28,
57, 29, 59, 0, 61, 30, 63, 31, 65, 0, 67, 0, 69, 0, 71, 32, 73, 33, 75,
0, 77, 34, 1, 0, 30, 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, 9, 9, 32, 32, 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, 371, 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, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 61,
1, 0, 0, 0, 0, 63, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0,
77, 1, 0, 0, 0, 1, 79, 1, 0, 0, 0, 3, 81, 1, 0, 0, 0, 5, 83, 1, 0, 0, 0,
7, 85, 1, 0, 0, 0, 9, 87, 1, 0, 0, 0, 11, 92, 1, 0, 0, 0, 13, 94, 1, 0,
0, 0, 15, 97, 1, 0, 0, 0, 17, 100, 1, 0, 0, 0, 19, 102, 1, 0, 0, 0, 21,
105, 1, 0, 0, 0, 23, 107, 1, 0, 0, 0, 25, 110, 1, 0, 0, 0, 27, 115, 1,
0, 0, 0, 29, 121, 1, 0, 0, 0, 31, 129, 1, 0, 0, 0, 33, 137, 1, 0, 0, 0,
35, 144, 1, 0, 0, 0, 37, 154, 1, 0, 0, 0, 39, 157, 1, 0, 0, 0, 41, 161,
1, 0, 0, 0, 43, 165, 1, 0, 0, 0, 45, 168, 1, 0, 0, 0, 47, 177, 1, 0, 0,
0, 49, 181, 1, 0, 0, 0, 51, 188, 1, 0, 0, 0, 53, 195, 1, 0, 0, 0, 55, 202,
1, 0, 0, 0, 57, 226, 1, 0, 0, 0, 59, 228, 1, 0, 0, 0, 61, 278, 1, 0, 0,
0, 63, 300, 1, 0, 0, 0, 65, 302, 1, 0, 0, 0, 67, 309, 1, 0, 0, 0, 69, 312,
1, 0, 0, 0, 71, 316, 1, 0, 0, 0, 73, 333, 1, 0, 0, 0, 75, 339, 1, 0, 0,
0, 77, 342, 1, 0, 0, 0, 79, 80, 5, 40, 0, 0, 80, 2, 1, 0, 0, 0, 81, 82,
5, 41, 0, 0, 82, 4, 1, 0, 0, 0, 83, 84, 5, 91, 0, 0, 84, 6, 1, 0, 0, 0,
85, 86, 5, 93, 0, 0, 86, 8, 1, 0, 0, 0, 87, 88, 5, 44, 0, 0, 88, 10, 1,
0, 0, 0, 89, 93, 5, 61, 0, 0, 90, 91, 5, 61, 0, 0, 91, 93, 5, 61, 0, 0,
92, 89, 1, 0, 0, 0, 92, 90, 1, 0, 0, 0, 93, 12, 1, 0, 0, 0, 94, 95, 5,
33, 0, 0, 95, 96, 5, 61, 0, 0, 96, 14, 1, 0, 0, 0, 97, 98, 5, 60, 0, 0,
98, 99, 5, 62, 0, 0, 99, 16, 1, 0, 0, 0, 100, 101, 5, 60, 0, 0, 101, 18,
1, 0, 0, 0, 102, 103, 5, 60, 0, 0, 103, 104, 5, 61, 0, 0, 104, 20, 1, 0,
0, 0, 105, 106, 5, 62, 0, 0, 106, 22, 1, 0, 0, 0, 107, 108, 5, 62, 0, 0,
108, 109, 5, 61, 0, 0, 109, 24, 1, 0, 0, 0, 110, 111, 7, 0, 0, 0, 111,
112, 7, 1, 0, 0, 112, 113, 7, 2, 0, 0, 113, 114, 7, 3, 0, 0, 114, 26, 1,
0, 0, 0, 115, 116, 7, 1, 0, 0, 116, 117, 7, 0, 0, 0, 117, 118, 7, 1, 0,
0, 118, 119, 7, 2, 0, 0, 119, 120, 7, 3, 0, 0, 120, 28, 1, 0, 0, 0, 121,
122, 7, 4, 0, 0, 122, 123, 7, 3, 0, 0, 123, 124, 7, 5, 0, 0, 124, 125,
7, 6, 0, 0, 125, 126, 7, 3, 0, 0, 126, 127, 7, 3, 0, 0, 127, 128, 7, 7,
0, 0, 128, 30, 1, 0, 0, 0, 129, 130, 7, 3, 0, 0, 130, 131, 7, 8, 0, 0,
131, 132, 7, 1, 0, 0, 132, 133, 7, 9, 0, 0, 133, 135, 7, 5, 0, 0, 134,
136, 7, 9, 0, 0, 135, 134, 1, 0, 0, 0, 135, 136, 1, 0, 0, 0, 136, 32, 1,
0, 0, 0, 137, 138, 7, 10, 0, 0, 138, 139, 7, 3, 0, 0, 139, 140, 7, 11,
0, 0, 140, 141, 7, 3, 0, 0, 141, 142, 7, 8, 0, 0, 142, 143, 7, 12, 0, 0,
143, 34, 1, 0, 0, 0, 144, 145, 7, 13, 0, 0, 145, 146, 7, 14, 0, 0, 146,
147, 7, 7, 0, 0, 147, 148, 7, 5, 0, 0, 148, 149, 7, 15, 0, 0, 149, 150,
7, 1, 0, 0, 150, 152, 7, 7, 0, 0, 151, 153, 7, 9, 0, 0, 152, 151, 1, 0,
0, 0, 152, 153, 1, 0, 0, 0, 153, 36, 1, 0, 0, 0, 154, 155, 7, 1, 0, 0,
155, 156, 7, 7, 0, 0, 156, 38, 1, 0, 0, 0, 157, 158, 7, 7, 0, 0, 158, 159,
7, 14, 0, 0, 159, 160, 7, 5, 0, 0, 160, 40, 1, 0, 0, 0, 161, 162, 7, 15,
0, 0, 162, 163, 7, 7, 0, 0, 163, 164, 7, 16, 0, 0, 164, 42, 1, 0, 0, 0,
165, 166, 7, 14, 0, 0, 166, 167, 7, 10, 0, 0, 167, 44, 1, 0, 0, 0, 168,
169, 7, 17, 0, 0, 169, 170, 7, 15, 0, 0, 170, 171, 7, 9, 0, 0, 171, 172,
7, 5, 0, 0, 172, 173, 7, 14, 0, 0, 173, 174, 7, 2, 0, 0, 174, 175, 7, 3,
0, 0, 175, 176, 7, 7, 0, 0, 176, 46, 1, 0, 0, 0, 177, 178, 7, 17, 0, 0,
178, 179, 7, 15, 0, 0, 179, 180, 7, 9, 0, 0, 180, 48, 1, 0, 0, 0, 181,
182, 7, 17, 0, 0, 182, 183, 7, 15, 0, 0, 183, 184, 7, 9, 0, 0, 184, 185,
7, 15, 0, 0, 185, 186, 7, 7, 0, 0, 186, 187, 7, 18, 0, 0, 187, 50, 1, 0,
0, 0, 188, 189, 7, 17, 0, 0, 189, 190, 7, 15, 0, 0, 190, 191, 7, 9, 0,
0, 191, 192, 7, 15, 0, 0, 192, 193, 7, 0, 0, 0, 193, 194, 7, 0, 0, 0, 194,
52, 1, 0, 0, 0, 195, 196, 7, 9, 0, 0, 196, 197, 7, 3, 0, 0, 197, 198, 7,
15, 0, 0, 198, 199, 7, 10, 0, 0, 199, 200, 7, 13, 0, 0, 200, 201, 7, 17,
0, 0, 201, 54, 1, 0, 0, 0, 202, 203, 7, 12, 0, 0, 203, 204, 7, 15, 0, 0,
204, 205, 7, 5, 0, 0, 205, 206, 7, 5, 0, 0, 206, 207, 7, 3, 0, 0, 207,
208, 7, 10, 0, 0, 208, 212, 7, 7, 0, 0, 209, 211, 7, 19, 0, 0, 210, 209,
1, 0, 0, 0, 211, 214, 1, 0, 0, 0, 212, 210, 1, 0, 0, 0, 212, 213, 1, 0,
0, 0, 213, 215, 1, 0, 0, 0, 214, 212, 1, 0, 0, 0, 215, 216, 5, 40, 0, 0,
216, 56, 1, 0, 0, 0, 217, 218, 7, 5, 0, 0, 218, 219, 7, 10, 0, 0, 219,
220, 7, 20, 0, 0, 220, 227, 7, 3, 0, 0, 221, 222, 7, 21, 0, 0, 222, 223,
7, 15, 0, 0, 223, 224, 7, 0, 0, 0, 224, 225, 7, 9, 0, 0, 225, 227, 7, 3,
0, 0, 226, 217, 1, 0, 0, 0, 226, 221, 1, 0, 0, 0, 227, 58, 1, 0, 0, 0,
228, 229, 7, 22, 0, 0, 229, 60, 1, 0, 0, 0, 230, 232, 3, 59, 29, 0, 231,
230, 1, 0, 0, 0, 231, 232, 1, 0, 0, 0, 232, 234, 1, 0, 0, 0, 233, 235,
3, 75, 37, 0, 234, 233, 1, 0, 0, 0, 235, 236, 1, 0, 0, 0, 236, 234, 1,
0, 0, 0, 236, 237, 1, 0, 0, 0, 237, 245, 1, 0, 0, 0, 238, 242, 5, 46, 0,
0, 239, 241, 3, 75, 37, 0, 240, 239, 1, 0, 0, 0, 241, 244, 1, 0, 0, 0,
242, 240, 1, 0, 0, 0, 242, 243, 1, 0, 0, 0, 243, 246, 1, 0, 0, 0, 244,
242, 1, 0, 0, 0, 245, 238, 1, 0, 0, 0, 245, 246, 1, 0, 0, 0, 246, 256,
1, 0, 0, 0, 247, 249, 7, 3, 0, 0, 248, 250, 3, 59, 29, 0, 249, 248, 1,
0, 0, 0, 249, 250, 1, 0, 0, 0, 250, 252, 1, 0, 0, 0, 251, 253, 3, 75, 37,
0, 252, 251, 1, 0, 0, 0, 253, 254, 1, 0, 0, 0, 254, 252, 1, 0, 0, 0, 254,
255, 1, 0, 0, 0, 255, 257, 1, 0, 0, 0, 256, 247, 1, 0, 0, 0, 256, 257,
1, 0, 0, 0, 257, 279, 1, 0, 0, 0, 258, 260, 3, 59, 29, 0, 259, 258, 1,
0, 0, 0, 259, 260, 1, 0, 0, 0, 260, 261, 1, 0, 0, 0, 261, 263, 5, 46, 0,
0, 262, 264, 3, 75, 37, 0, 263, 262, 1, 0, 0, 0, 264, 265, 1, 0, 0, 0,
265, 263, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 276, 1, 0, 0, 0, 267,
269, 7, 3, 0, 0, 268, 270, 3, 59, 29, 0, 269, 268, 1, 0, 0, 0, 269, 270,
1, 0, 0, 0, 270, 272, 1, 0, 0, 0, 271, 273, 3, 75, 37, 0, 272, 271, 1,
0, 0, 0, 273, 274, 1, 0, 0, 0, 274, 272, 1, 0, 0, 0, 274, 275, 1, 0, 0,
0, 275, 277, 1, 0, 0, 0, 276, 267, 1, 0, 0, 0, 276, 277, 1, 0, 0, 0, 277,
279, 1, 0, 0, 0, 278, 231, 1, 0, 0, 0, 278, 259, 1, 0, 0, 0, 279, 62, 1,
0, 0, 0, 280, 286, 5, 34, 0, 0, 281, 285, 8, 23, 0, 0, 282, 283, 5, 92,
0, 0, 283, 285, 9, 0, 0, 0, 284, 281, 1, 0, 0, 0, 284, 282, 1, 0, 0, 0,
285, 288, 1, 0, 0, 0, 286, 284, 1, 0, 0, 0, 286, 287, 1, 0, 0, 0, 287,
289, 1, 0, 0, 0, 288, 286, 1, 0, 0, 0, 289, 301, 5, 34, 0, 0, 290, 296,
5, 39, 0, 0, 291, 295, 8, 24, 0, 0, 292, 293, 5, 92, 0, 0, 293, 295, 9,
0, 0, 0, 294, 291, 1, 0, 0, 0, 294, 292, 1, 0, 0, 0, 295, 298, 1, 0, 0,
0, 296, 294, 1, 0, 0, 0, 296, 297, 1, 0, 0, 0, 297, 299, 1, 0, 0, 0, 298,
296, 1, 0, 0, 0, 299, 301, 5, 39, 0, 0, 300, 280, 1, 0, 0, 0, 300, 290,
1, 0, 0, 0, 301, 64, 1, 0, 0, 0, 302, 306, 7, 25, 0, 0, 303, 305, 7, 26,
0, 0, 304, 303, 1, 0, 0, 0, 305, 308, 1, 0, 0, 0, 306, 304, 1, 0, 0, 0,
306, 307, 1, 0, 0, 0, 307, 66, 1, 0, 0, 0, 308, 306, 1, 0, 0, 0, 309, 310,
5, 91, 0, 0, 310, 311, 5, 93, 0, 0, 311, 68, 1, 0, 0, 0, 312, 313, 5, 91,
0, 0, 313, 314, 5, 42, 0, 0, 314, 315, 5, 93, 0, 0, 315, 70, 1, 0, 0, 0,
316, 329, 3, 65, 32, 0, 317, 318, 5, 46, 0, 0, 318, 328, 3, 65, 32, 0,
319, 328, 3, 67, 33, 0, 320, 328, 3, 69, 34, 0, 321, 323, 5, 46, 0, 0,
322, 324, 3, 75, 37, 0, 323, 322, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325,
323, 1, 0, 0, 0, 325, 326, 1, 0, 0, 0, 326, 328, 1, 0, 0, 0, 327, 317,
1, 0, 0, 0, 327, 319, 1, 0, 0, 0, 327, 320, 1, 0, 0, 0, 327, 321, 1, 0,
0, 0, 328, 331, 1, 0, 0, 0, 329, 327, 1, 0, 0, 0, 329, 330, 1, 0, 0, 0,
330, 72, 1, 0, 0, 0, 331, 329, 1, 0, 0, 0, 332, 334, 7, 27, 0, 0, 333,
332, 1, 0, 0, 0, 334, 335, 1, 0, 0, 0, 335, 333, 1, 0, 0, 0, 335, 336,
1, 0, 0, 0, 336, 337, 1, 0, 0, 0, 337, 338, 6, 36, 0, 0, 338, 74, 1, 0,
0, 0, 339, 340, 7, 28, 0, 0, 340, 76, 1, 0, 0, 0, 341, 343, 8, 29, 0, 0,
342, 341, 1, 0, 0, 0, 343, 344, 1, 0, 0, 0, 344, 342, 1, 0, 0, 0, 344,
345, 1, 0, 0, 0, 345, 78, 1, 0, 0, 0, 30, 0, 92, 135, 152, 212, 226, 231,
236, 242, 245, 249, 254, 256, 259, 265, 269, 274, 276, 278, 284, 286, 294,
296, 300, 306, 325, 327, 329, 335, 344, 1, 6, 0, 0,
}
deserializer := antlr.NewATNDeserializer(nil)
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
@@ -258,37 +268,38 @@ func NewFilterQueryLexer(input antlr.CharStream) *FilterQueryLexer {
// FilterQueryLexer tokens.
const (
FilterQueryLexerLPAREN = 1
FilterQueryLexerRPAREN = 2
FilterQueryLexerLBRACK = 3
FilterQueryLexerRBRACK = 4
FilterQueryLexerCOMMA = 5
FilterQueryLexerEQUALS = 6
FilterQueryLexerNOT_EQUALS = 7
FilterQueryLexerNEQ = 8
FilterQueryLexerLT = 9
FilterQueryLexerLE = 10
FilterQueryLexerGT = 11
FilterQueryLexerGE = 12
FilterQueryLexerLIKE = 13
FilterQueryLexerILIKE = 14
FilterQueryLexerBETWEEN = 15
FilterQueryLexerEXISTS = 16
FilterQueryLexerREGEXP = 17
FilterQueryLexerCONTAINS = 18
FilterQueryLexerIN = 19
FilterQueryLexerNOT = 20
FilterQueryLexerAND = 21
FilterQueryLexerOR = 22
FilterQueryLexerHASTOKEN = 23
FilterQueryLexerHAS = 24
FilterQueryLexerHASANY = 25
FilterQueryLexerHASALL = 26
FilterQueryLexerSEARCH = 27
FilterQueryLexerBOOL = 28
FilterQueryLexerNUMBER = 29
FilterQueryLexerQUOTED_TEXT = 30
FilterQueryLexerKEY = 31
FilterQueryLexerWS = 32
FilterQueryLexerFREETEXT = 33
FilterQueryLexerLPAREN = 1
FilterQueryLexerRPAREN = 2
FilterQueryLexerLBRACK = 3
FilterQueryLexerRBRACK = 4
FilterQueryLexerCOMMA = 5
FilterQueryLexerEQUALS = 6
FilterQueryLexerNOT_EQUALS = 7
FilterQueryLexerNEQ = 8
FilterQueryLexerLT = 9
FilterQueryLexerLE = 10
FilterQueryLexerGT = 11
FilterQueryLexerGE = 12
FilterQueryLexerLIKE = 13
FilterQueryLexerILIKE = 14
FilterQueryLexerBETWEEN = 15
FilterQueryLexerEXISTS = 16
FilterQueryLexerREGEXP = 17
FilterQueryLexerCONTAINS = 18
FilterQueryLexerIN = 19
FilterQueryLexerNOT = 20
FilterQueryLexerAND = 21
FilterQueryLexerOR = 22
FilterQueryLexerHASTOKEN = 23
FilterQueryLexerHAS = 24
FilterQueryLexerHASANY = 25
FilterQueryLexerHASALL = 26
FilterQueryLexerSEARCH = 27
FilterQueryLexerPATTERN_CALL = 28
FilterQueryLexerBOOL = 29
FilterQueryLexerNUMBER = 30
FilterQueryLexerQUOTED_TEXT = 31
FilterQueryLexerKEY = 32
FilterQueryLexerWS = 33
FilterQueryLexerFREETEXT = 34
)

View File

@@ -47,6 +47,9 @@ type FilterQueryListener interface {
// EnterSearchCall is called when entering the searchCall production.
EnterSearchCall(c *SearchCallContext)
// EnterPatternCall is called when entering the patternCall production.
EnterPatternCall(c *PatternCallContext)
// EnterFunctionParamList is called when entering the functionParamList production.
EnterFunctionParamList(c *FunctionParamListContext)
@@ -101,6 +104,9 @@ type FilterQueryListener interface {
// ExitSearchCall is called when exiting the searchCall production.
ExitSearchCall(c *SearchCallContext)
// ExitPatternCall is called when exiting the patternCall production.
ExitPatternCall(c *PatternCallContext)
// ExitFunctionParamList is called when exiting the functionParamList production.
ExitFunctionParamList(c *FunctionParamListContext)

File diff suppressed because it is too large Load Diff

View File

@@ -47,6 +47,9 @@ type FilterQueryVisitor interface {
// Visit a parse tree produced by FilterQueryParser#searchCall.
VisitSearchCall(ctx *SearchCallContext) interface{}
// Visit a parse tree produced by FilterQueryParser#patternCall.
VisitPatternCall(ctx *PatternCallContext) interface{}
// Visit a parse tree produced by FilterQueryParser#functionParamList.
VisitFunctionParamList(ctx *FunctionParamListContext) interface{}

View File

@@ -446,6 +446,8 @@ func emptyResultFor(kind qbtypes.RequestType, queryName string) *qbtypes.Result
value = &qbtypes.TimeSeriesData{QueryName: queryName}
case kind == qbtypes.RequestTypeScalar:
value = &qbtypes.ScalarData{QueryName: queryName}
case kind == qbtypes.RequestTypePattern:
value = &qbtypes.PatternsData{QueryName: queryName, SampleRate: 1, Patterns: []*qbtypes.Pattern{}}
default:
value = &qbtypes.RawData{QueryName: queryName}
}

View File

@@ -98,6 +98,8 @@ func consume(rows driver.Rows, kind qbtypes.RequestType, queryWindow *qbtypes.Ti
payload, err = readAsHeatmap(rows, queryWindow, step, queryName)
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeTrace, qbtypes.RequestTypeRawStream:
payload, err = readAsRaw(rows, queryName)
case qbtypes.RequestTypePattern:
payload, err = readAsPatterns(rows, queryName)
// TODO: add support for other request types
}
@@ -503,6 +505,90 @@ func readAsScalar(rows driver.Rows, queryName string) (*qbtypes.ScalarData, erro
}, nil
}
// readAsPatterns reads a patterns statement: one row per pattern, its columns
// named by the qbtypes.PatternsColumn constants, and any other column a
// group-by key. The sampled counts scale up by the sample rate.
func readAsPatterns(rows driver.Rows, queryName string) (*qbtypes.PatternsData, error) {
colNames := rows.Columns()
colTypes := rows.ColumnTypes()
scan := make([]any, len(colTypes))
for i := range scan {
scan[i] = reflect.New(colTypes[i].ScanType()).Interface()
}
data := &qbtypes.PatternsData{QueryName: queryName, SampleRate: 1, Patterns: []*qbtypes.Pattern{}}
for rows.Next() {
if err := rows.Scan(scan...); err != nil {
return nil, err
}
pattern := &qbtypes.Pattern{Samples: []string{}, Severities: []string{}, Trend: []*qbtypes.PatternTrendPoint{}}
var sampledCount uint64
var trendTs, trendCounts []int64
for i, name := range colNames {
value := derefValue(scan[i])
switch name {
case qbtypes.PatternsColumnPattern:
pattern.Pattern, _ = value.(string)
case qbtypes.PatternsColumnID:
pattern.ID, _ = value.(string)
case qbtypes.PatternsColumnSampledCount:
sampledCount, _ = value.(uint64)
case qbtypes.PatternsColumnSampleRate:
if rate, ok := value.(uint64); ok && rate > 0 {
data.SampleRate = rate
}
case qbtypes.PatternsColumnTotal:
data.Total, _ = value.(uint64)
case qbtypes.PatternsColumnFirstSeen:
if ns, ok := value.(uint64); ok {
pattern.FirstSeen = int64(ns / 1_000_000)
}
case qbtypes.PatternsColumnLastSeen:
if ns, ok := value.(uint64); ok {
pattern.LastSeen = int64(ns / 1_000_000)
}
case qbtypes.PatternsColumnSamples:
if samples, ok := value.([]string); ok {
pattern.Samples = samples
}
case qbtypes.PatternsColumnSeverities:
if severities, ok := value.([]string); ok {
pattern.Severities = severities
}
case qbtypes.PatternsColumnTrendTs:
trendTs, _ = value.([]int64)
case qbtypes.PatternsColumnTrendCounts:
trendCounts, _ = value.([]int64)
default:
pattern.Labels = append(pattern.Labels, &qbtypes.Label{
Key: telemetrytypes.TelemetryFieldKey{Name: stripKeyAlias(name)},
Value: value,
})
}
}
pattern.Count = sampledCount * data.SampleRate
for i := 0; i < len(trendTs) && i < len(trendCounts); i++ {
pattern.Trend = append(pattern.Trend, &qbtypes.PatternTrendPoint{
Timestamp: trendTs[i] / 1_000_000,
Count: uint64(trendCounts[i]) * data.SampleRate,
})
}
data.Patterns = append(data.Patterns, pattern)
}
if err := rows.Err(); err != nil {
return nil, err
}
for _, pattern := range data.Patterns {
if data.Total > 0 {
pattern.Percent = 100 * float64(pattern.Count) / float64(data.Total)
}
}
return data, nil
}
func derefValue(v any) any {
if v == nil {
return nil

View File

@@ -0,0 +1,84 @@
package querier
import (
"testing"
cmock "github.com/SigNoz/clickhouse-go-mock"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReadAsPatterns(t *testing.T) {
cols := []cmock.ColumnType{
{Name: qbtypes.PatternsColumnPattern, Type: "String"},
{Name: qbtypes.PatternsColumnID, Type: "String"},
{Name: "__GROUP_BY_KEY_0_service.name", Type: "String"},
{Name: qbtypes.PatternsColumnSampledCount, Type: "UInt64"},
{Name: qbtypes.PatternsColumnSampleRate, Type: "UInt64"},
{Name: qbtypes.PatternsColumnTotal, Type: "UInt64"},
{Name: qbtypes.PatternsColumnFirstSeen, Type: "UInt64"},
{Name: qbtypes.PatternsColumnLastSeen, Type: "UInt64"},
{Name: qbtypes.PatternsColumnSamples, Type: "Array(String)"},
{Name: qbtypes.PatternsColumnSeverities, Type: "Array(String)"},
{Name: qbtypes.PatternsColumnTrendTs, Type: "Array(Int64)"},
{Name: qbtypes.PatternsColumnTrendCounts, Type: "Array(Int64)"},
}
rows := cmock.NewRows(cols, [][]any{
{
"User <*> logged in from <IP>", "13956782398643507091", "api",
uint64(30), uint64(4), uint64(200),
uint64(1767225600000000007), uint64(1767231599000000995),
[]string{"User alice logged in from 10.0.0.1"}, []string{"INFO"},
[]int64{1767225600000000000, 1767225660000000000}, []int64{10, 20},
},
{
"<IP>", "1", "web",
uint64(20), uint64(4), uint64(200),
uint64(1767225600000000000), uint64(1767225600000000000),
[]string{}, []string{},
[]int64{}, []int64{},
},
})
data, err := readAsPatterns(rows, "A")
require.NoError(t, err)
assert.Equal(t, "A", data.QueryName)
assert.Equal(t, uint64(4), data.SampleRate)
assert.Equal(t, uint64(200), data.Total)
require.Len(t, data.Patterns, 2)
first := data.Patterns[0]
assert.Equal(t, "User <*> logged in from <IP>", first.Pattern)
assert.Equal(t, "13956782398643507091", first.ID)
assert.Equal(t, uint64(120), first.Count)
assert.InDelta(t, 60, first.Percent, 1e-9)
assert.Equal(t, int64(1767225600000), first.FirstSeen)
assert.Equal(t, int64(1767231599000), first.LastSeen)
assert.Equal(t, []string{"User alice logged in from 10.0.0.1"}, first.Samples)
assert.Equal(t, []string{"INFO"}, first.Severities)
require.Len(t, first.Labels, 1)
assert.Equal(t, "service.name", first.Labels[0].Key.Name)
assert.Equal(t, "api", first.Labels[0].Value)
assert.Equal(t, []*qbtypes.PatternTrendPoint{
{Timestamp: 1767225600000, Count: 40},
{Timestamp: 1767225660000, Count: 80},
}, first.Trend)
second := data.Patterns[1]
assert.Equal(t, uint64(80), second.Count)
assert.InDelta(t, 40, second.Percent, 1e-9)
assert.Empty(t, second.Trend)
assert.NotNil(t, second.Samples)
}
func TestReadAsPatterns_NoRows(t *testing.T) {
rows := cmock.NewRows([]cmock.ColumnType{{Name: qbtypes.PatternsColumnPattern, Type: "String"}}, nil)
data, err := readAsPatterns(rows, "A")
require.NoError(t, err)
assert.Equal(t, uint64(1), data.SampleRate)
assert.NotNil(t, data.Patterns)
assert.Empty(t, data.Patterns)
}

View File

@@ -681,6 +681,8 @@ func (q *querier) run(
return slices.ContainsFunc(val.Aggregations, func(aggregation *qbtypes.DistributionAggregation) bool {
return len(aggregation.Distributions) != 0
})
case *qbtypes.PatternsData:
return len(val.Patterns) != 0
}
return false
}
@@ -729,6 +731,8 @@ func (q *querier) run(
v.QueryName = name
case *qbtypes.DistributionData:
v.QueryName = name
case *qbtypes.PatternsData:
v.QueryName = name
}
queryResults[i] = result
return nil

View File

@@ -49,7 +49,6 @@ func ThresholdRuleAtLeastOnceValueAbove(target float64, recovery *float64) rulet
Annotations: map[string]string{
"value": "{{$value}}",
},
Version: "v5",
SchemaVersion: ruletypes.SchemaVersionV2Alpha1,
RuleCondition: &ruletypes.RuleCondition{
SelectedQuery: "A",
@@ -108,7 +107,6 @@ func BuildPromAtLeastOnceValueAbove(target float64, recovery *float64) ruletypes
Annotations: map[string]string{
"value": "{{$value}}",
},
Version: "v5",
SchemaVersion: ruletypes.SchemaVersionV2Alpha1,
RuleCondition: &ruletypes.RuleCondition{
SelectedQuery: "A",

View File

@@ -240,7 +240,6 @@ func TestPrepareParamsForLogsFilterExpression(t *testing.T) {
},
SelectedQuery: "A",
},
Version: "v5",
}
logger := instrumentationtest.New().Logger()
@@ -303,7 +302,6 @@ func TestPrepareParamsForTracesFilterExpression(t *testing.T) {
},
SelectedQuery: "A",
},
Version: "v5",
}
logger := instrumentationtest.New().Logger()

View File

@@ -32,7 +32,7 @@ var friendly = map[string]string{
"BETWEEN": "BETWEEN", "IN": "IN", "EXISTS": "EXISTS",
"REGEXP": "REGEXP", "CONTAINS": "CONTAINS",
"HAS": "has()", "HASANY": "hasAny()", "HASALL": "hasAll()",
"HASTOKEN": "hasToken()", "SEARCH": "search()",
"HASTOKEN": "hasToken()", "SEARCH": "search()", "PATTERN_CALL": "pattern()",
// literals / identifiers
"NUMBER": "number",

View File

@@ -194,6 +194,9 @@ func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext)
} else if ctx.SearchCall() != nil {
// search() spans all fields; it can never be a logical contradiction
return nil
} else if ctx.PatternCall() != nil {
// pattern() reads no key, so it can't contradict a comparison
return nil
} else if ctx.FullText() != nil {
// Handle full text search if needed
return nil

View File

@@ -204,6 +204,8 @@ func (v *filterExpressionVisitor) Visit(tree antlr.ParseTree) any {
return v.VisitFullText(t)
case *grammar.SearchCallContext:
return v.VisitSearchCall(t)
case *grammar.PatternCallContext:
return v.VisitPatternCall(t)
case *grammar.FunctionCallContext:
return v.VisitFunctionCall(t)
case *grammar.FunctionParamListContext:
@@ -320,6 +322,8 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
return v.Visit(ctx.FunctionCall())
} else if ctx.SearchCall() != nil {
return v.Visit(ctx.SearchCall())
} else if ctx.PatternCall() != nil {
return v.Visit(ctx.PatternCall())
} else if ctx.FullText() != nil {
return v.Visit(ctx.FullText())
}
@@ -841,6 +845,46 @@ func (v *filterExpressionVisitor) VisitSearchCall(ctx *grammar.SearchCallContext
return v.builder.Or(conds...)
}
// PatternFilterer is a storage that can keep the logs of given log patterns.
type PatternFilterer interface {
PatternCondition(ctx context.Context, q qbtypes.QueryInfo, patterns []string, sb *sqlbuilder.SelectBuilder) (string, error)
}
// VisitPatternCall handles pattern('<pattern text>'[, ...]): the logs whose
// pattern is one of the given pattern texts.
func (v *filterExpressionVisitor) VisitPatternCall(ctx *grammar.PatternCallContext) any {
if v.storage.Traits().Split == qbtypes.FingerprintOfSplit {
// the main query evaluates it
return SkipConditionLiteral
}
filterer, ok := v.storage.(PatternFilterer)
if !ok {
v.errors = append(v.errors, "function `pattern` is supported only for logs")
return ErrorConditionLiteral
}
// the pattern of every log in the time range is computed
v.requiresCostGuard = true
var patterns []string
if valueList := ctx.ValueList(); valueList != nil {
for _, param := range valueList.AllValue() {
pattern, ok := searchParamText(param)
if !ok {
v.errors = append(v.errors, "function `pattern` expects pattern texts, e.g. pattern('User <*> logged in')")
return ErrorConditionLiteral
}
patterns = append(patterns, pattern)
}
}
condition, err := filterer.PatternCondition(v.context, v.query, patterns, v.builder)
if err != nil {
v.recordError(err)
return ErrorConditionLiteral
}
return condition
}
// searchParamText returns an argument's raw token text (quoted or bare) rather than its
// visited value, so a bare word stays literal and search(1000000) isn't "1e+06".
func searchParamText(val grammar.IValueContext) (string, bool) {

View File

@@ -0,0 +1,184 @@
package logsstatementbuilder
import (
"context"
"fmt"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetryschema/logstelemetryschema"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/huandu/go-sqlbuilder"
)
const (
// patternsSampleTarget is about how many logs a patterns query masks: it
// reads one in ceil(matching logs / patternsSampleTarget) of them.
patternsSampleTarget = 100000
// patternsDefaultLimit is how many patterns a query without a limit returns.
patternsDefaultLimit = 50
patternsSampleCTE = "__pattern_sample"
)
// patternsServiceKey is the key every patterns statement groups by first:
// patterns are always counted per service.
var patternsServiceKey = telemetrytypes.TelemetryFieldKey{
Name: "service.name",
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
// isServiceNameKey reports whether a group-by key names the service, which a
// patterns statement already groups by.
func isServiceNameKey(key telemetrytypes.TelemetryFieldKey) bool {
return key.Name == patternsServiceKey.Name &&
(key.FieldContext == telemetrytypes.FieldContextUnspecified || key.FieldContext == telemetrytypes.FieldContextResource)
}
// buildPatternsQuery counts the logs of the query by pattern, on a sample.
// A CTE counts the matching logs and picks the sample rate, so that about
// patternsSampleTarget logs are masked. The main query keeps one in that many
// logs by a hash of their ID, the same logs on every run, and groups them by
// pattern, by service and by the query's group by.
func (b *logQueryStatementBuilder) buildPatternsQuery(
ctx context.Context,
orgID valuer.UUID,
sb *sqlbuilder.SelectBuilder,
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
start, end uint64,
keys map[string][]*telemetrytypes.TelemetryFieldKey,
variables map[string]qbtypes.VariableItem,
) (*qbtypes.Statement, error) {
var (
cteFragments []string
cteArgs [][]any
warnings []string
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, orgID, sb, query, start, end, variables)
if err != nil {
return nil, err
}
if frag != "" {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
// the sample rate, from the logs the filter keeps
countSB := sqlbuilder.NewSelectBuilder()
countSB.Select(
fmt.Sprintf("greatest(toUInt64(1), toUInt64(ceil(count() / %d))) AS rate", patternsSampleTarget),
"count() AS total",
)
countSB.From(fmt.Sprintf("%s.%s", logstelemetryschema.DBName, logstelemetryschema.LogsV2TableName))
if frag != "" {
countSB.Where("resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
}
if _, err := b.addFilterCondition(ctx, orgID, countSB, start, end, query, keys, variables, skipResourceFilter); err != nil {
return nil, err
}
countSQL, countArgs := countSB.BuildWithFlavor(sqlbuilder.ClickHouse)
cteFragments = append(cteFragments, fmt.Sprintf("%s AS (%s)", patternsSampleCTE, countSQL))
cteArgs = append(cteArgs, countArgs)
info := querybuilder.NewQueryInfo(ctx, orgID, b.fl, telemetrytypes.SignalLogs, nil, start, end)
patternExpr := logstelemetryschema.PatternExpr()
stepNs := uint64(query.StepInterval.Seconds()) * querybuilder.NsToSeconds
if stepNs == 0 {
stepNs = querybuilder.RecommendedStepInterval(start, end) * querybuilder.NsToSeconds
}
sb.Select(
fmt.Sprintf("%s AS %s", patternExpr, qbtypes.PatternsColumnPattern),
fmt.Sprintf("%s AS %s", logstelemetryschema.PatternIDExpr(qbtypes.PatternsColumnPattern), qbtypes.PatternsColumnID),
)
// patterns are always counted per service, then per the query's own group by
groupBy := []telemetrytypes.TelemetryFieldKey{patternsServiceKey}
for _, gb := range query.GroupBy {
if !isServiceNameKey(gb.TelemetryFieldKey) {
groupBy = append(groupBy, gb.TelemetryFieldKey)
}
}
groupByAliases := make([]string, 0, len(groupBy))
for i := range groupBy {
expr, keyWarnings, err := querybuilder.ResolveColumn(ctx, info, b.storage, columnKey(&groupBy[i]), telemetrytypes.FieldDataTypeString, keys)
if err != nil {
return nil, err
}
warnings = append(warnings, keyWarnings...)
alias := groupByColumnAlias(i, groupBy[i].Name)
sb.SelectMore(sqlbuilder.Escape(fmt.Sprintf("toString(%s) AS %s", expr, alias)))
groupByAliases = append(groupByAliases, sqlbuilder.Escape(alias))
}
topStrings := func(expr string) string {
return fmt.Sprintf("arrayFilter(x -> x != '', arrayMap(x -> ifNull(toString(x), ''), topK(3)(%s)))", expr)
}
sb.SelectMore(
fmt.Sprintf("count() AS %s", qbtypes.PatternsColumnSampledCount),
fmt.Sprintf("(SELECT rate FROM %s) AS %s", patternsSampleCTE, qbtypes.PatternsColumnSampleRate),
fmt.Sprintf("(SELECT total FROM %s) AS %s", patternsSampleCTE, qbtypes.PatternsColumnTotal),
fmt.Sprintf("min(timestamp) AS %s", qbtypes.PatternsColumnFirstSeen),
fmt.Sprintf("max(timestamp) AS %s", qbtypes.PatternsColumnLastSeen),
fmt.Sprintf("groupArraySample(3, 1)(leftUTF8(%s, 2048)) AS %s", logstelemetryschema.PatternMessageExpr, qbtypes.PatternsColumnSamples),
fmt.Sprintf("%s AS %s", topStrings(logstelemetryschema.LogsV2SeverityTextColumn), qbtypes.PatternsColumnSeverities),
fmt.Sprintf("tupleElement(sumMap([toInt64(intDiv(timestamp, %d) * %d)], [toInt64(1)]) AS __trend, 1) AS %s", stepNs, stepNs, qbtypes.PatternsColumnTrendTs),
fmt.Sprintf("tupleElement(__trend, 2) AS %s", qbtypes.PatternsColumnTrendCounts),
)
sb.From(fmt.Sprintf("%s.%s", logstelemetryschema.DBName, logstelemetryschema.LogsV2TableName))
preparedWhereClause, err := b.addFilterCondition(ctx, orgID, sb, start, end, query, keys, variables, skipResourceFilter)
if err != nil {
return nil, err
}
sb.Where(fmt.Sprintf("cityHash64(%s) %% (SELECT rate FROM %s) = 0", logstelemetryschema.LogsV2IDColumn, patternsSampleCTE))
sb.GroupBy(append([]string{qbtypes.PatternsColumnPattern}, groupByAliases...)...)
sb.OrderBy(fmt.Sprintf("%s DESC", qbtypes.PatternsColumnSampledCount), fmt.Sprintf("%s ASC", qbtypes.PatternsColumnPattern))
limit := query.Limit
if limit <= 0 {
limit = patternsDefaultLimit
}
if limit > qbtypes.PatternsMaxLimit {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "pattern requests return at most %d patterns, got limit %d", qbtypes.PatternsMaxLimit, limit)
}
sb.Limit(limit)
if query.Offset > 0 {
sb.Offset(query.Offset)
}
mainSQL, mainArgs := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return &qbtypes.Statement{
Query: querybuilder.CombineCTEs(cteFragments) + mainSQL,
Args: querybuilder.PrependArgs(cteArgs, mainArgs),
Warnings: append(preparedWhereClause.Warnings, warnings...),
WarningsDocURL: preparedWhereClause.WarningsDocURL,
CostGuard: b.costGuardFor(ctx, orgID, preparedWhereClause.RequiresCostGuard),
}, nil
}
// patternsKeySelectors adds the keys a patterns statement reads besides the
// query's own.
func patternsKeySelectors(selectors []*telemetrytypes.FieldKeySelector) []*telemetrytypes.FieldKeySelector {
for _, selector := range selectors {
if strings.EqualFold(selector.Name, patternsServiceKey.Name) {
return selectors
}
}
return append(selectors, &telemetrytypes.FieldKeySelector{
Name: patternsServiceKey.Name,
Signal: telemetrytypes.SignalLogs,
FieldContext: patternsServiceKey.FieldContext,
FieldDataType: patternsServiceKey.FieldDataType,
})
}

View File

@@ -0,0 +1,84 @@
package logsstatementbuilder
import (
"context"
"strings"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/statementbuilder"
"github.com/SigNoz/signoz/pkg/telemetryschema/logstelemetryschema"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStatementBuilderPatterns(t *testing.T) {
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
fl := flaggertest.New(t)
metadataStore := telemetrytypestest.NewMockMetadataStore()
metadataStore.KeysMap = logstelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
storage := logstelemetryschema.NewStorage()
statementBuilder := NewLogQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
metadataStore,
storage,
querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalLogs),
logstelemetryschema.DefaultFullTextColumn,
fl,
nil,
statementbuilder.Config{SkipResourceFingerprint: statementbuilder.SkipResourceFingerprint{Enabled: false, Threshold: 100000}},
)
start, end := uint64(1767225600000), uint64(1767232800000)
patternExpr := logstelemetryschema.PatternExpr()
serviceColumn := "toString(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, NULL)) AS `__GROUP_BY_KEY_0_service.name`"
t.Run("CountsOnASampleSizedByTheFilteredLogs", func(t *testing.T) {
stmt, err := statementBuilder.Build(context.Background(), valuer.UUID{}, start, end, qbtypes.RequestTypePattern, qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "severity_text = 'ERROR'"},
}, nil)
require.NoError(t, err)
filter := "WHERE severity_text = ? AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ?"
assert.True(t, strings.HasPrefix(stmt.Query, "WITH __pattern_sample AS (SELECT greatest(toUInt64(1), toUInt64(ceil(count() / 100000))) AS rate, count() AS total FROM signoz_logs.distributed_logs_v2 "+filter+") SELECT "+patternExpr+" AS __pattern, toString(cityHash64(__pattern)) AS __pattern_id, "+serviceColumn+", count() AS __sampled_count"), stmt.Query)
assert.True(t, strings.HasSuffix(stmt.Query, " FROM signoz_logs.distributed_logs_v2 "+filter+" AND cityHash64(id) % (SELECT rate FROM __pattern_sample) = 0 GROUP BY __pattern, `__GROUP_BY_KEY_0_service.name` ORDER BY __sampled_count DESC, __pattern ASC LIMIT ?"), stmt.Query)
assert.Contains(t, stmt.Query, "sumMap([toInt64(intDiv(timestamp, 20000000000) * 20000000000)], [toInt64(1)]) AS __trend")
// the filter's arguments, once for the sample CTE and once for the main query
filterArgs := []any{"ERROR", "1767225600000000000", uint64(1767223800), "1767232800000000000", uint64(1767232800)}
assert.Equal(t, append(append(append([]any{}, filterArgs...), filterArgs...), 50), stmt.Args)
assert.Nil(t, stmt.CostGuard)
})
t.Run("GroupBy_LimitAndPatternFilter", func(t *testing.T) {
stmt, err := statementBuilder.Build(context.Background(), valuer.UUID{}, start, end, qbtypes.RequestTypePattern, qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "NOT pattern('<IP>')"},
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "severity_text"}}},
Limit: 10,
}, nil)
require.NoError(t, err)
// patterns are always counted per service first, then per the query's group by
assert.Contains(t, stmt.Query, serviceColumn+", toString(severity_text) AS `__GROUP_BY_KEY_1_severity_text`")
assert.True(t, strings.HasSuffix(stmt.Query, " GROUP BY __pattern, `__GROUP_BY_KEY_0_service.name`, `__GROUP_BY_KEY_1_severity_text` ORDER BY __sampled_count DESC, __pattern ASC LIMIT ?"), stmt.Query)
assert.Contains(t, stmt.Query, "WHERE NOT (match("+patternExpr+", ?))")
assert.Equal(t, 10, stmt.Args[len(stmt.Args)-1])
require.NotNil(t, stmt.CostGuard)
})
t.Run("LimitOverMax", func(t *testing.T) {
_, err := statementBuilder.Build(context.Background(), valuer.UUID{}, start, end, qbtypes.RequestTypePattern, qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Limit: qbtypes.PatternsMaxLimit + 1,
}, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "at most 1000 patterns")
})
}

View File

@@ -125,6 +125,9 @@ func (b *logQueryStatementBuilder) Build(
bodyJSONEnabled := b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID))
keySelectors, warnings := getKeySelectors(query, bodyJSONEnabled)
if requestType == qbtypes.RequestTypePattern {
keySelectors = patternsKeySelectors(keySelectors)
}
keySelectors = querybuilder.ExpandKeySelectorsForFamilies(ctx, orgID, b.fl, keySelectors)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, keySelectors)
if err != nil {
@@ -146,6 +149,8 @@ func (b *logQueryStatementBuilder) Build(
stmt, err = b.buildHeatmapTimeSeriesQuery(ctx, orgID, q, query, start, end, keys, variables)
case qbtypes.RequestTypeScalar:
stmt, err = b.buildScalarQuery(ctx, orgID, q, query, start, end, keys, false, variables)
case qbtypes.RequestTypePattern:
stmt, err = b.buildPatternsQuery(ctx, orgID, q, query, start, end, keys, variables)
default:
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported request type: %s", requestType)
}

View File

@@ -112,7 +112,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Single word",
query: "<script>alert('xss')</script>",
shouldPass: false,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '<'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got '<'",
},
// Single word searches with spaces
@@ -178,7 +178,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "[tracing]",
shouldPass: false,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '['",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got '['",
},
{
category: "Special characters",
@@ -208,7 +208,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "ERROR: cannot execute update() in a read-only context",
shouldPass: false,
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got ')'",
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got ')'",
},
{
category: "Special characters",
@@ -630,7 +630,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'and'",
},
{
category: "Keyword conflict",
@@ -638,7 +638,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'or'",
},
{
category: "Keyword conflict",
@@ -646,7 +646,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF",
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got EOF",
},
{
category: "Keyword conflict",
@@ -654,7 +654,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'like'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'like'",
},
{
category: "Keyword conflict",
@@ -662,7 +662,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'between'",
},
{
category: "Keyword conflict",
@@ -670,7 +670,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'in'",
},
{
category: "Keyword conflict",
@@ -678,7 +678,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'exists'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'exists'",
},
{
category: "Keyword conflict",
@@ -686,7 +686,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'regexp'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'regexp'",
},
{
category: "Keyword conflict",
@@ -694,7 +694,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'contains'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'contains'",
},
{
category: "Keyword conflict",
@@ -2062,9 +2062,9 @@ func TestFilterExprLogs(t *testing.T) {
expectedErrorContains: "",
},
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'AND'"},
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'OR'"},
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF"},
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'AND'"},
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'OR'"},
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got EOF"},
{category: "Only functions", query: "has", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
{category: "Only functions", query: "hasAny", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
@@ -2206,7 +2206,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'and'",
},
{
category: "Operator keywords as keys",
@@ -2214,7 +2214,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'or'",
},
{
category: "Operator keywords as keys",
@@ -2222,7 +2222,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '='",
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got '='",
},
{
category: "Operator keywords as keys",
@@ -2230,7 +2230,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'between'",
},
{
category: "Operator keywords as keys",
@@ -2238,7 +2238,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, pattern(), quoted text, search()} but got 'in'",
},
// Using function keywords as keys

View File

@@ -0,0 +1,288 @@
package logstelemetryschema
import (
"context"
"fmt"
"regexp"
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/huandu/go-sqlbuilder"
)
// A log's pattern is its message with the values that change from log to
// log replaced by fixed placeholders. Logs whose pattern text is the same
// belong to one pattern. The filter function pattern('...') keeps the logs
// of the given patterns.
// PatternMaskVersion versions patternMasks. Changing a rule changes the
// pattern text, and so the pattern, a log maps to, so a saved filter on a
// pattern stops matching.
const PatternMaskVersion = 2
// patternMaxMessageChars caps the characters of a message the masks read, so
// one huge log cannot dominate the cost of a query. It counts characters, not
// bytes, so the cut never splits a multi-byte character into invalid UTF-8.
const patternMaxMessageChars = 2048
// PatternPlaceholders are the only placeholders patternMasks write. Any other
// text in angle brackets, like <nil>, is text from the log.
var PatternPlaceholders = []string{"<UUID>", "<MAC>", "<TS>", "<IP>", "<EMAIL>", "<URL>", "<HEX>", "<FRAME>", "<STR>", "<EMPTY>", "<*>"}
// patternEmpty is the pattern of a log whose message is empty once masked.
const patternEmpty = "<EMPTY>"
type patternMask struct {
name string
regexp string
replacement string
}
// The building blocks of patternMasks.
const (
hexDigit = `[0-9a-fA-F]`
macSeparatedPair = `[:-][0-9a-fA-F][0-9a-fA-F]`
base64Char = `[A-Za-z0-9+/]`
// k8sNameChar is the alphabet Kubernetes draws the random parts of pod
// names from: no vowels, so they never spell a word.
k8sNameChar = `[bcdfghjklmnpqrstvwxz2456789]`
// keyQuoteMarker stands in for the quotes of a key while values are
// masked; blobStartMarker and blobEndMarker wrap a candidate base64 run.
// A log message practically never holds these control characters.
keyQuoteMarker = "\x01"
blobStartMarker = "\x02"
blobEndMarker = "\x03"
)
// patternMasks run in order, each on the output of the one before. Each
// replaces only a value recognisable on its own, from a known format, and
// only the matched text, so the same rules can run unchanged in the collector
// later. Words made only of letters are never guessed at.
//
// The regular expressions are plain RE2 with ASCII classes, the dialect
// ClickHouse and Go share. They go into the query as string literals, and
// clickhouse-go scans a query without regard to literals: it binds every '?'
// to an argument and reads any {...:...} or $N as a parameter. So they hold
// no '?', '{', '}' or '$': a repeat is written out, an optional part is (x|),
// and { } $ are written \x7b \x7d \x24. A replacement refers to a group as
// \1, or to the whole match as \0, the way ClickHouse does.
var patternMasks = []patternMask{
// clean up: terminal colour codes, leading space, and a stack trace after
// its first line (Java, JavaScript, Python, Go)
{name: "ansi", regexp: `\x1b\[[0-9;]*[A-Za-z]`, replacement: ""},
{name: "lead_ws", regexp: `^\s+`, replacement: ""},
{name: "stack_cut", regexp: `\n([ \t]+at \S|Caused by: |[ \t]*\.\.\. \d+ (more|common frames omitted)|Traceback \(|[ \t]+File "|goroutine \d+ \[)[\s\S]*`, replacement: ""},
// values with a known format; the URL goes first so the rules after it
// never take part of one
{name: "url", regexp: `\b[a-zA-Z][a-zA-Z0-9+.-]*://[^\s"'<>,\x7d\])]+`, replacement: "<URL>"},
{name: "uuid", regexp: repeat(hexDigit, 8) + `-` + repeat(hexDigit, 4) + `-` + repeat(hexDigit, 4) + `-` + repeat(hexDigit, 4) + `-` + repeat(hexDigit, 12), replacement: "<UUID>"},
{name: "mac", regexp: `(^|[^0-9A-Za-z:-])` + repeat(hexDigit, 2) + repeat(macSeparatedPair, 5) + `([^0-9A-Za-z:-]|\z)`, replacement: `\1<MAC>\2`},
// timestamps: ISO 8601, weekday (RFC 2822, Java Date), syslog, US style
{name: "ts_iso", regexp: `\d\d\d\d[-/]\d\d[-/]\d\d[T ]\d\d:\d\d:\d\d([.,]\d+|)(Z|[+-]\d\d:\d\d|[+-]\d\d\d\d|)`, replacement: "<TS>"},
{name: "ts_weekday", regexp: `\b(Mon|Tue|Wed|Thu|Fri|Sat|Sun)(,|) (\d\d |\d |)(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d\d |\d | \d |)(\d\d\d\d |)\d\d:\d\d:\d\d( [A-Z][A-Z][A-Z]|)( \d\d\d\d|)\b`, replacement: "<TS>"},
{name: "ts_syslog", regexp: `\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d|\d) \d\d:\d\d:\d\d\b`, replacement: "<TS>"},
{name: "ts_slash", regexp: `\b\d(\d|)/\d(\d|)/\d\d\d\d(,|) \d(\d|):\d\d:\d\d( [AP]M|)`, replacement: "<TS>"},
{name: "ip", regexp: `\b\d(\d|)(\d|)\.\d(\d|)(\d|)\.\d(\d|)(\d|)\.\d(\d|)(\d|)(:\d+|)\b`, replacement: "<IP>"},
// an email address ends in a domain of letters, so module@1.2.3 is not one
{name: "email", regexp: `[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z][A-Za-z]+\b`, replacement: "<EMAIL>"},
{name: "hex", regexp: `\b0[xX][0-9a-fA-F]+\b`, replacement: "<HEX>"},
// a stack frame logged on its own line
{name: "frame", regexp: `^at ((async |)[^\s(]*[./][^\s(]*( |)\([^)]*\)|/\S+:\d+:\d+)`, replacement: "at <FRAME>"},
// a base64 blob: a run of 20 base64 characters and more that holds both a
// digit and a '+'. RE2 has no lookahead, so runs are marked first, the
// marked runs that qualify are masked, and the other marks are removed.
{name: "b64_mark", regexp: repeat(base64Char, 20) + base64Char + `*=*`, replacement: blobStartMarker + `\0` + blobEndMarker},
{name: "b64_mask", regexp: blobStartMarker + `([^` + blobEndMarker + `]*[0-9][^` + blobEndMarker + `]*\+|[^` + blobEndMarker + `]*\+[^` + blobEndMarker + `]*[0-9])[^` + blobEndMarker + `]*` + blobEndMarker, replacement: "<*>"},
{name: "b64_unmark", regexp: `[` + blobStartMarker + blobEndMarker + `]`, replacement: ""},
// structured text: the text of an XML element, and a JSON number, true,
// false or null after a key
{name: "xml_text", regexp: `(<[A-Za-z][\w:.-]*(\s[^<>]*|)>)([^<>]*[^<>\s][^<>]*)(</[A-Za-z][\w:.-]*>)`, replacement: `\1<*>\4`},
{name: "json_scalar", regexp: `("[^"]*"\s*:\s*)(-\d|\d|true\b|false\b|null\b)[0-9.eE+-]*`, replacement: `\1<*>`},
// a quoted string followed by : or = is a key, so it stays: its quotes are
// swapped for a marker while every other quoted string, a value, becomes
// <STR>, and then put back
{name: "key_protect", regexp: `"([^"]*)"(\s*[:=])`, replacement: keyQuoteMarker + `\1` + keyQuoteMarker + `\2`},
{name: "str", regexp: `"[^"]*"`, replacement: "<STR>"},
{name: "key_unmark", regexp: keyQuoteMarker, replacement: `"`},
// the key of a key=value pair stays. The value may hold placeholders, but
// a value that opens an object or array is left to the rules above.
{name: "kv", regexp: `([\w.-]+)=(<[A-Z*]+>|[^\s,;&\x7b\[(<>])(<[A-Z*]+>|[^\s,;&()\]\x7d<>])*`, replacement: `\1=<*>`},
// any run of letters and digits that holds a digit; the letters before it,
// like vpc- or URF_, stay
{name: "digit", regexp: `(^|[^A-Za-z0-9])[A-Za-z]*[0-9][A-Za-z0-9]*`, replacement: `\1<*>`},
// the random suffix of a Kubernetes pod name, like -xrsrt
{name: "pod_suffix", regexp: `-` + repeat(k8sNameChar, 5) + `([\s"'_.,)\]\x7d]|\z)`, replacement: `-<*>\1`},
// placeholders the rules above left side by side are one value
{name: "collapse", regexp: `(<\*>)(([._:/@#-]|)<\*>)+`, replacement: "<*>"},
{name: "unquote", regexp: `"<\*>"|'<\*>'`, replacement: "<*>"},
{name: "ws", regexp: `\s+`, replacement: " "},
}
// repeat writes out n copies of a regular expression atom.
func repeat(atom string, n int) string {
return strings.Repeat(atom, n)
}
// PatternMessageExpr is the text a log's pattern is computed from: the
// message sub-column of the JSON body, which holds the whole body of a log
// whose body is plain text.
const PatternMessageExpr = messageSubColumn
// PatternExpr is the ClickHouse expression for a log's pattern text.
func PatternExpr() string {
expr := fmt.Sprintf("leftUTF8(%s, %d)", PatternMessageExpr, patternMaxMessageChars)
for _, mask := range patternMasks {
expr = fmt.Sprintf("replaceRegexpAll(%s, %s, %s)", expr, quoteClickHouseString(mask.regexp), quoteClickHouseString(mask.replacement))
}
return fmt.Sprintf("ifNull(nullIf(trimBoth(%s), ''), %s)", expr, quoteClickHouseString(patternEmpty))
}
// PatternIDExpr is the ClickHouse expression for a pattern's ID, the hash of
// its text, as a decimal string so that a JSON number cannot round it.
func PatternIDExpr(patternExpr string) string {
return fmt.Sprintf("toString(cityHash64(%s))", patternExpr)
}
// PatternLiteralWords returns up to limit of the longest words a pattern
// keeps as they appear in the log, lowercase and unique. Every log of the
// pattern holds each of them in its body, so a body search on them narrows
// the logs before the masks run, and can use the body index. A word is
// ASCII letters and digits only and at least four characters long, so a
// JSON escape in the stored body cannot hide it and the ngram index can use
// it.
func PatternLiteralWords(pattern string, limit int) []string {
for _, placeholder := range PatternPlaceholders {
pattern = strings.ReplaceAll(pattern, placeholder, " ")
}
isWordByte := func(c byte) bool {
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c >= 0x80
}
var words []string
for start := 0; start < len(pattern); {
if !isWordByte(pattern[start]) {
start++
continue
}
end := start
ascii := true
for end < len(pattern) && isWordByte(pattern[end]) {
if pattern[end] >= 0x80 {
ascii = false
}
end++
}
word := strings.ToLower(pattern[start:end])
if ascii && len(word) >= 4 && !slices.Contains(words, word) {
words = append(words, word)
}
start = end
}
slices.SortStableFunc(words, func(a, b string) int { return len(b) - len(a) })
if len(words) > limit {
words = words[:limit]
}
return words
}
// patternPrecheckWords is how many words of a pattern the filter also
// searches the body for.
const patternPrecheckWords = 3
// PatternCondition keeps the logs whose pattern is one of patterns. A
// placeholder given to the filter and the one in a log's pattern match when
// they are the same or either is <*>: pattern('User <*> logged in') keeps the
// logs of 'User <UUID> logged in', and pattern('User <UUID> logged in') keeps
// the logs of 'User <*> logged in'. Two different named placeholders never
// match, and <EMPTY> matches only itself. Each pattern also searches
// the message for its fixed words first, a cheap substring search, so the
// masks run only on the logs it keeps.
func (s *storage) PatternCondition(_ context.Context, _ qbtypes.QueryInfo, patterns []string, sb *sqlbuilder.SelectBuilder) (string, error) {
if len(patterns) == 0 {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "function `pattern` expects at least one pattern, e.g. pattern('User <*> logged in')")
}
patternExpr := PatternExpr()
alternatives := make([]string, 0, len(patterns))
for _, pattern := range patterns {
conditions := []string{}
for _, word := range PatternLiteralWords(pattern, patternPrecheckWords) {
// a word is letters and digits, so it holds no LIKE wildcard
conditions = append(conditions, fmt.Sprintf("LOWER(%s) LIKE %s", PatternMessageExpr, sb.Var("%"+word+"%")))
}
if patternFilterPlaceholder.MatchString(pattern) {
// the regular expression is an argument, so clickhouse-go never scans it
conditions = append(conditions, fmt.Sprintf("match(%s, %s)", patternExpr, sb.Var(PatternFilterRegexp(pattern))))
} else {
conditions = append(conditions, sb.E(sqlbuilder.Escape(patternExpr), pattern))
}
if len(conditions) == 1 {
alternatives = append(alternatives, conditions[0])
continue
}
alternatives = append(alternatives, sb.And(conditions...))
}
if len(alternatives) == 1 {
return alternatives[0], nil
}
return sb.Or(alternatives...), nil
}
// patternWildcard is the placeholder that, in the filter, matches any
// placeholder that stands for a value, and that any of them matches.
const patternWildcard = "<*>"
// patternValuePlaceholders are the placeholders that stand for a value, the
// ones patternWildcard matches. <EMPTY> stands for the whole message.
var patternValuePlaceholders = slices.DeleteFunc(slices.Clone(PatternPlaceholders), func(placeholder string) bool {
return placeholder == patternEmpty || placeholder == patternWildcard
})
// patternFilterPlaceholder finds the placeholders of a pattern given to the
// filter that match more than their own text.
var patternFilterPlaceholder = regexp.MustCompile(patternPlaceholderAlternation(append(slices.Clone(patternValuePlaceholders), patternWildcard)))
// patternPlaceholderAlternation is the regular expression that matches any of
// placeholders.
func patternPlaceholderAlternation(placeholders []string) string {
quoted := make([]string, len(placeholders))
for i, placeholder := range placeholders {
quoted[i] = regexp.QuoteMeta(placeholder)
}
return "(" + strings.Join(quoted, "|") + ")"
}
// PatternFilterRegexp is the anchored regular expression a pattern given to
// the filter matches pattern texts with: its text as it is, each <*> any
// placeholder that stands for a value, and each other such placeholder
// itself or <*>.
func PatternFilterRegexp(pattern string) string {
anyValue := patternPlaceholderAlternation(append(slices.Clone(patternValuePlaceholders), patternWildcard))
var builder strings.Builder
builder.WriteString("^")
last := 0
for _, loc := range patternFilterPlaceholder.FindAllStringIndex(pattern, -1) {
builder.WriteString(regexp.QuoteMeta(pattern[last:loc[0]]))
placeholder := pattern[loc[0]:loc[1]]
if placeholder == patternWildcard {
builder.WriteString(anyValue)
} else {
builder.WriteString(patternPlaceholderAlternation([]string{placeholder, patternWildcard}))
}
last = loc[1]
}
builder.WriteString(regexp.QuoteMeta(pattern[last:]))
builder.WriteString("$")
return builder.String()
}
// quoteClickHouseString quotes s as a ClickHouse string literal, where the
// backslash escapes.
func quoteClickHouseString(s string) string {
return "'" + strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(s) + "'"
}

View File

@@ -0,0 +1,137 @@
package logstelemetryschema
import (
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// applyPatternMasks computes a pattern the way PatternExpr does in ClickHouse,
// with Go's regexp, the same RE2 dialect. It is how the collector will compute
// patterns, so the two must agree on every message.
func applyPatternMasks(message string) string {
if runes := []rune(message); len(runes) > patternMaxMessageChars {
message = string(runes[:patternMaxMessageChars])
}
group := regexp.MustCompile(`\\(\d)`)
for _, mask := range patternMasks {
// ClickHouse writes a group as \1, Go as ${1}
message = regexp.MustCompile(mask.regexp).ReplaceAllString(message, group.ReplaceAllString(mask.replacement, "$${$1}"))
}
if message = strings.Trim(message, " "); message == "" {
return patternEmpty
}
return message
}
// TestPatternMasks pins what each rule does, and what it must leave alone.
func TestPatternMasks(t *testing.T) {
testCases := []struct {
rule string
message string
expected string
}{
{rule: "ansi", message: "\u001b[32mINFO\u001b[0m server started", expected: "INFO server started"},
{rule: "lead_ws", message: "\n leading newline then text", expected: "leading newline then text"},
{rule: "stack_cut", message: "java.lang.IllegalStateException: boom\n\tat com.foo.Bar.baz(Bar.java:42)\n\tat com.foo.Main.main(Main.java:7)", expected: "java.lang.IllegalStateException: boom"},
{rule: "stack_cut", message: "boom\nCaused by: java.io.IOException: x", expected: "boom"},
{rule: "stack_cut", message: "TypeError: x is undefined\n at Consumer.handle (/app/node_modules/c/index.js:12:5)", expected: "TypeError: x is undefined"},
{rule: "stack_cut", message: "Traceback (most recent call last):\n File \"a.py\", line 3", expected: "Traceback (most recent call last):"},
{rule: "stack_cut", message: "panic: boom\n\ngoroutine 1 [running]:\nmain.main()", expected: "panic: boom"},
{rule: "stack_cut", message: "Executed DbCommand (5ms)\nSELECT id FROM t", expected: "Executed DbCommand (<*>) SELECT id FROM t"},
{rule: "stack_cut", message: "meeting notes\nat noon we met", expected: "meeting notes at noon we met"},
{rule: "empty", message: "", expected: "<EMPTY>"},
{rule: "empty", message: " \n ", expected: "<EMPTY>"},
{rule: "uuid", message: "user 123e4567-e89b-12d3-a456-426614174000 logged in", expected: "user <UUID> logged in"},
{rule: "mac", message: "device 00:1A:2B:3C:4D:5E connected", expected: "device <MAC> connected"},
{rule: "mac", message: "aa:bb:cc:dd:ee:ff", expected: "<MAC>"},
{rule: "mac", message: "mac=00-1a-2b-3c-4d-5e", expected: "mac=<*>"},
{rule: "ts_iso", message: "started 2026-11-12 10:22:01,123 ok", expected: "started <TS> ok"},
{rule: "ts_iso", message: "at 2026-11-12T10:22:01.123Z done", expected: "at <TS> done"},
{rule: "ts_iso", message: "2026/11/12 10:59:56 [notice] 7#7: signal 3 received", expected: "<TS> [notice] <*>: signal <*> received"},
{rule: "ts_weekday", message: "Thu Nov 12 10:22:01 UTC 2026 Unable to add frame", expected: "<TS> Unable to add frame"},
{rule: "ts_weekday", message: "Date: Thu, 12 Nov 2026 10:22:01 GMT", expected: "Date: <TS>"},
{rule: "ts_weekday", message: "Thu Nov 5 10:22:01 2026 x", expected: "<TS> x"},
{rule: "ts_weekday", message: "Thursday standup moved", expected: "Thursday standup moved"},
{rule: "ts_syslog", message: "Nov 12 10:22:01 host sshd[42]: accepted", expected: "<TS> host sshd[<*>]: accepted"},
{rule: "ts_syslog", message: "May 3 users signed up", expected: "May <*> users signed up"},
{rule: "ts_slash", message: "[Nest] 1 - 11/12/2026, 3:04:05 PM LOG ready", expected: "[Nest] <*> - <TS> LOG ready"},
{rule: "ts_slash", message: "on 12/11/2026 15:04:05 done", expected: "on <TS> done"},
{rule: "ip", message: "from 10.0.0.12:8080 to 192.168.1.1", expected: "from <IP> to <IP>"},
{rule: "email", message: "sent to jane.doe+x@example.co.uk", expected: "sent to <EMAIL>"},
{rule: "email", message: "loading module spring-core@5.3.1", expected: "loading module spring-core@<*>"},
{rule: "email", message: "user root@localhost denied", expected: "user root@localhost denied"},
{rule: "url", message: "GET https://api.example.com/v1/users?id=5 200", expected: "GET <URL> <*>"},
{rule: "url", message: "{\"u\":\"https://a.com/x\",\"k\":\"v\"}", expected: "{\"u\":<STR>,\"k\":<STR>}"},
{rule: "url", message: "(OkHttp https://a.com/x) failed", expected: "(OkHttp <URL>) failed"},
{rule: "url", message: "see http://a.com/x, then retry", expected: "see <URL>, then retry"},
{rule: "hex", message: "ptr 0x7ffde4a1 freed", expected: "ptr <HEX> freed"},
{rule: "hex", message: "amount 12345678901234567890123 ok", expected: "amount <*> ok"},
{rule: "hex", message: "trace 4bf92f3577b34da6a3ce929d0e0e4736", expected: "trace <*>"},
{rule: "frame", message: "\tat com.foo.Bar.baz(Bar.java:42)", expected: "at <FRAME>"},
{rule: "frame", message: "at com.foo.Bar$1.run(Bar.java:10)", expected: "at <FRAME>"},
{rule: "frame", message: "at org.x.Y.z(Y.java:1) ~[spring-core-5.3.1.jar:5.3.1]", expected: "at <FRAME> ~[spring-core-<*>.jar:<*>]"},
{rule: "frame", message: "at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)", expected: "at <FRAME>"},
{rule: "frame", message: "at async Consumer.handle (/app/index.js:12:5)", expected: "at <FRAME>"},
{rule: "frame", message: "at /home/app/services/log.js:42:7", expected: "at <FRAME>"},
{rule: "frame", message: "at least(3) items", expected: "at least(<*>) items"},
{rule: "frame", message: "look at this(thing)", expected: "look at this(thing)"},
{rule: "json_scalar", message: "{\"ok\":true,\"count\":42,\"err\":null,\"name\":\"bob\",\"neg\": -1.5e3}", expected: "{\"ok\":<*>,\"count\":<*>,\"err\":<*>,\"name\":<STR>,\"neg\": <*>}"},
{rule: "json_scalar", message: "feature enabled true", expected: "feature enabled true"},
{rule: "json_scalar", message: "{\"trueValue\":\"x\"}", expected: "{\"trueValue\":<STR>}"},
{rule: "str", message: "said \"hello world\" and \"key\": 5", expected: "said <STR> and \"key\": <*>"},
{rule: "kv", message: "user_id=abc status=ok", expected: "user_id=<*> status=<*>"},
{rule: "kv", message: "stats (total=10, idle=2)", expected: "stats (total=<*>, idle=<*>)"},
{rule: "kv", message: "request=CallBackRequest(type=status, amount=5)", expected: "request=<*>(type=<*>, amount=<*>)"},
{rule: "kv", message: "list=[a, b] map={x}", expected: "list=[a, b] map={x}"},
{rule: "kv", message: "RequestId=123e4567-e89b-12d3-a456-426614174000 done", expected: "RequestId=<*> done"},
{rule: "kv", message: "Response: path=/api/v1/123e4567-e89b-12d3-a456-426614174000/tests/start, code=200", expected: "Response: path=<*>, code=<*>"},
{rule: "kv", message: "<req user=bob id=7>x</req>", expected: "<req user=<*> id=<*>><*></req>"},
{rule: "digit", message: "order TTMW12AB34 created", expected: "order <*> created"},
{rule: "digit", message: "vpc-d0011db5 ACCEPT", expected: "vpc-<*> ACCEPT"},
{rule: "digit", message: "ref URF_1794490605914_3542935", expected: "ref URF_<*>"},
{rule: "digit", message: "host ingest.us2.signoz.cloud", expected: "host ingest.<*>.signoz.cloud"},
{rule: "digit", message: "took 150ms", expected: "took <*>"},
{rule: "digit", message: "retry 3 of 5", expected: "retry <*> of <*>"},
{rule: "digit", message: "v1 api over http2", expected: "<*> api over <*>"},
{rule: "digit", message: "Started AgencyBankingArcaIntegrationApplication in 5.2 seconds", expected: "Started AgencyBankingArcaIntegrationApplication in <*> seconds"},
{rule: "collapse", message: "id 12:34/56@78#9 end", expected: "id <*> end"},
{rule: "collapse", message: "path /api/v1/users/12345/orders", expected: "path /api/<*>/users/<*>/orders"},
{rule: "collapse", message: "ids 1, 2, 3 done", expected: "ids <*>, <*>, <*> done"},
{rule: "unquote", message: "name '42' set", expected: "name <*> set"},
{rule: "ws", message: "a \t b", expected: "a b"},
{rule: "url_order", message: "POST https://api.x.com/v1/ref/123e4567-e89b-12d3-a456-426614174000/reference failed", expected: "POST <URL> failed"},
{rule: "url_order", message: "GET http://10.0.0.1:8080/health ok", expected: "GET <URL> ok"},
{rule: "xml_text", message: "<response><code>200</code><message>success</message></response>", expected: "<response><code><*></code><message><*></message></response>"},
{rule: "xml_text", message: "<oxip version=\"7.1\"><message>garbled message</message></oxip>", expected: "<oxip version=<*>><message><*></message></oxip>"},
{rule: "xml_text", message: "<a>\n </a>", expected: "<a> </a>"},
{rule: "xml_text", message: "<TS> ERROR <*> failed for user", expected: "<TS> ERROR <*> failed for user"},
{rule: "xml_text", message: "value <b>bold</b> text", expected: "value <b><*></b> text"},
{rule: "xml_text", message: "list<String> and Map<K,V> types", expected: "list<String> and Map<K,V> types"},
{rule: "pod_suffix", message: "PUSH for node:cos-business-logic-7bccd7b796-xrsrt.complianceos-uat ok", expected: "PUSH for node:cos-business-logic-<*>.complianceos-uat ok"},
{rule: "pod_suffix", message: "var.log.containers.cilium-sfhtn_kube-system", expected: "var.log.containers.cilium-<*>_kube-system"},
{rule: "pod_suffix", message: "restarting pod cilium-58spv, reason OOM", expected: "restarting pod cilium-<*>, reason OOM"},
{rule: "pod_suffix", message: "kafka-consumer-b957fc547-lgzh5 started", expected: "kafka-consumer-<*> started"},
{rule: "pod_suffix", message: "agent python-httpx/0.27.0", expected: "agent python-httpx/<*>"},
{rule: "pod_suffix", message: "call url-https://x.com/a", expected: "call <URL>"},
{rule: "pod_suffix", message: "level-crypt", expected: "level-crypt"},
{rule: "b64", message: "ReceiptHandle': 'AQEBJ7UqUro46SFpG8hmPf4mWw4jSZk1qYCFGm/IAAlPT03l+NhgC3hCsuqliqvxIq5xqON=' done", expected: "ReceiptHandle': <*> done"},
{rule: "b64", message: "payload eJydkltPAjEQhf/abc+def12345/XYZ+qwerty== ok", expected: "payload <*> ok"},
{rule: "b64", message: "content_type application/cloudevents+json", expected: "content_type application/cloudevents+json"},
{rule: "b64", message: "GET /api/v1/customers/12345/accounts/67890/items", expected: "GET /api/<*>/customers/<*>/accounts/<*>/items"},
{rule: "b64", message: "Started AbstractAnnotationConfigDispatcherServletInitializer now", expected: "Started AbstractAnnotationConfigDispatcherServletInitializer now"},
{rule: "b64", message: "sum 1+2+3 is six", expected: "sum <*>+<*>+<*> is six"},
}
for _, testCase := range testCases {
t.Run(testCase.rule+"/"+testCase.message, func(t *testing.T) {
assert.Equal(t, testCase.expected, applyPatternMasks(testCase.message))
})
}
}
func TestPatternMasksCutOnACharacterBoundary(t *testing.T) {
message := strings.Repeat("a", patternMaxMessageChars-1) + "é and the rest"
assert.Equal(t, strings.Repeat("a", patternMaxMessageChars-1)+"é", applyPatternMasks(message))
}

View File

@@ -0,0 +1,226 @@
package logstelemetryschema
import (
"context"
"regexp"
"strings"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/huandu/go-sqlbuilder"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPatternExpr(t *testing.T) {
expr := PatternExpr()
assert.True(t, strings.HasPrefix(expr, "ifNull(nullIf(trimBoth(replaceRegexpAll("), expr)
assert.True(t, strings.HasSuffix(expr, "), ''), '<EMPTY>')"), expr)
assert.Equal(t, len(patternMasks), strings.Count(expr, "replaceRegexpAll("))
// the cut counts characters, so it never splits one
assert.Contains(t, expr, "leftUTF8(body_v2.message, 2048)")
// ClickHouse string literals escape the backslash
assert.Contains(t, expr, `'(^|[^A-Za-z0-9])[A-Za-z]*[0-9][A-Za-z0-9]*', '\\1<*>'`)
// keys keep their quotes: protected by a marker while values become <STR>
assert.Contains(t, expr, "'\"([^\"]*)\"(\\\\s*[:=])', '\x01\\\\1\x01\\\\2'")
// clickhouse-go scans a query without regard to string literals: it binds a
// '?' to an argument, reads {...:...} as a query parameter and $N as a
// numbered argument
for _, forbidden := range []string{"?", "{", "}", "$"} {
assert.NotContains(t, expr, forbidden)
}
}
func TestPatternLiteralWords(t *testing.T) {
testCases := []struct {
name string
pattern string
limit int
expected []string
}{
{
name: "LongestFirst_TiesInOrder",
pattern: "No active replica has part <*>. Will fetch merged part instead.",
limit: 3,
expected: []string{"replica", "instead", "active"},
},
{
name: "PlaceholderNamesAreNotWords",
pattern: "<TS> ERROR order <*> failed after <*> <UUID> <EMAIL>",
limit: 10,
expected: []string{"failed", "error", "order", "after"},
},
{
name: "ShortAndNonASCIIWordsSkipped",
pattern: "GET /v1/api café naïveté ok",
limit: 10,
expected: nil,
},
{
name: "KeysKept_SplitOnUnderscore_Lowercase_Unique",
pattern: "Cache miss key=<*> for request_id=<*> cache",
limit: 10,
expected: []string{"request", "cache", "miss"},
},
{
name: "OnlyPlaceholders",
pattern: "<IP>",
limit: 3,
expected: nil,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.expected, PatternLiteralWords(testCase.pattern, testCase.limit))
})
}
}
func TestPatternFilterRegexp(t *testing.T) {
testCases := []struct {
pattern string
matches []string
misses []string
}{
{
pattern: "User <*> logged in from <IP>",
matches: []string{"User <*> logged in from <IP>", "User <UUID> logged in from <IP>", "User <EMAIL> logged in from <IP>", "User <UUID> logged in from <*>"},
misses: []string{"User alice logged in from <IP>", "User <UUID> logged in from <HEX>", "User <UUID> logged in from <IP> again", "User <UUID><IP> logged in from <IP>", "User <EMPTY> logged in from <IP>", "User <NIL> logged in from <IP>"},
},
{
pattern: "User <UUID> logged in",
matches: []string{"User <UUID> logged in", "User <*> logged in"},
misses: []string{"User <IP> logged in", "User <UUIDS> logged in"},
},
{
pattern: "GET /a.b?(c) [x] <*>",
matches: []string{"GET /a.b?(c) [x] <HEX>"},
misses: []string{"GET /aXb?(c) [x] <HEX>"},
},
{
pattern: "got <nil> for <*>",
matches: []string{"got <nil> for <IP>"},
misses: []string{"got <*> for <IP>", "got <IP> for <IP>"},
},
{
pattern: "<EMPTY>",
matches: []string{"<EMPTY>"},
misses: []string{"<*>"},
},
}
for _, testCase := range testCases {
t.Run(testCase.pattern, func(t *testing.T) {
re := regexp.MustCompile(PatternFilterRegexp(testCase.pattern))
for _, text := range testCase.matches {
assert.True(t, re.MatchString(text), text)
}
for _, text := range testCase.misses {
assert.False(t, re.MatchString(text), text)
}
})
}
}
func TestFilterExprPattern(t *testing.T) {
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
opts := querybuilder.FilterExprVisitorOpts{
Context: context.Background(),
Logger: instrumentationtest.New().Logger(),
Storage: NewStorage(),
Query: querybuilder.NewQueryInfo(context.Background(), valuer.UUID{}, flaggertest.New(t), telemetrytypes.SignalLogs, nil, uint64(releaseTime.Add(-5*time.Minute).UnixNano()), uint64(releaseTime.Add(5*time.Minute).UnixNano())),
FieldKeys: BuildCompleteFieldKeyMap(releaseTime),
FullTextColumn: DefaultFullTextColumn,
}
pattern := PatternExpr()
anyValue := `(<UUID>|<MAC>|<TS>|<IP>|<EMAIL>|<URL>|<HEX>|<FRAME>|<STR>|<\*>)`
testCases := []struct {
name string
query string
expectedQuery string
expectedArgs []any
expectedError string
}{
{
name: "Wildcard_SearchesBodyForFixedWordsFirst_ThenMatchesAnyPlaceholder",
query: "pattern('No active replica has part <*>. Will fetch merged part instead.')",
expectedQuery: "WHERE (LOWER(body_v2.message) LIKE ? AND LOWER(body_v2.message) LIKE ? AND LOWER(body_v2.message) LIKE ? AND match(" + pattern + ", ?))",
expectedArgs: []any{"%replica%", "%instead%", "%active%", `^No active replica has part ` + anyValue + `\. Will fetch merged part instead\.$`},
},
{
name: "NamedPlaceholder_MatchesItselfOrWildcard",
query: "pattern('User <UUID> logged in from <IP>')",
expectedQuery: "WHERE (LOWER(body_v2.message) LIKE ? AND LOWER(body_v2.message) LIKE ? AND LOWER(body_v2.message) LIKE ? AND match(" + pattern + ", ?))",
expectedArgs: []any{"%logged%", "%user%", "%from%", `^User (<UUID>|<\*>) logged in from (<IP>|<\*>)$`},
},
{
name: "NoPlaceholders_ComparedExactly",
query: "pattern('Server started')",
expectedQuery: "WHERE (LOWER(body_v2.message) LIKE ? AND LOWER(body_v2.message) LIKE ? AND " + pattern + " = ?)",
expectedArgs: []any{"%started%", "%server%", "Server started"},
},
{
name: "Empty_ComparedExactly",
query: "pattern('<EMPTY>')",
expectedQuery: "WHERE " + pattern + " = ?",
expectedArgs: []any{"<EMPTY>"},
},
{
name: "NoFixedWords_PatternOnly",
query: "pattern('<IP>')",
expectedQuery: "WHERE match(" + pattern + ", ?)",
expectedArgs: []any{`^(<IP>|<\*>)$`},
},
{
name: "Several_AnyOfThem",
query: `pattern('<IP>', "can't connect to <IP>")`,
expectedQuery: "WHERE (match(" + pattern + ", ?) OR (LOWER(body_v2.message) LIKE ? AND match(" + pattern + ", ?)))",
expectedArgs: []any{`^(<IP>|<\*>)$`, "%connect%", `^can't connect to (<IP>|<\*>)$`},
},
{
name: "EscapedQuote",
query: `pattern('can\'t <*>')`,
expectedQuery: "WHERE match(" + pattern + ", ?)",
expectedArgs: []any{`^can't ` + anyValue + `$`},
},
{
name: "Not_Excludes",
query: "service.name = 'api' AND NOT pattern('<IP>')",
expectedQuery: "WHERE (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND NOT (match(" + pattern + ", ?)))",
expectedArgs: []any{"api", `^(<IP>|<\*>)$`},
},
{
name: "PatternKeyStaysAKey",
query: "pattern = 'x'",
expectedQuery: "WHERE ((attributes_string['pattern'] = ? AND mapContains(attributes_string, 'pattern')) OR (JSON_VALUE(body, '$.\"pattern\"') = ? AND JSON_EXISTS(body, '$.\"pattern\"')))",
expectedArgs: []any{"x", "x"},
},
{
name: "NoPattern_Rejected",
query: "pattern()",
expectedError: "syntax error",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
clause, err := querybuilder.PrepareWhereClause(testCase.query, opts)
if testCase.expectedError != "" {
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), testCase.expectedError) || detailContains(err, testCase.expectedError), err.Error())
return
}
require.NoError(t, err)
sql, args := clause.WhereClause.BuildWithFlavor(sqlbuilder.ClickHouse)
assert.Equal(t, testCase.expectedQuery, sql)
assert.Equal(t, testCase.expectedArgs, args)
assert.Equal(t, strings.Contains(testCase.query, "pattern("), clause.RequiresCostGuard)
})
}
}

View File

@@ -1,7 +1,6 @@
package alertmanagertypes
import (
"encoding/json"
"fmt"
"slices"
"strings"
@@ -12,7 +11,6 @@ import (
"github.com/prometheus/alertmanager/config"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/swaggest/jsonschema-go"
)
const defaultJiraReopenDuration = model.Duration(3 * 24 * time.Hour)
@@ -82,101 +80,60 @@ type ChannelJiraConfig struct {
WontFixResolution string `json:"wontFixResolution" description:"The resolution name that stops an issue from being reopened."`
CustomFields map[string]any `json:"customFields,omitzero" description:"Extra issue fields keyed by Jira field ID. Stored, but not currently included in the issue Jira receives."`
Auth ChannelJiraAuth `json:"auth" required:"true" description:"The credentials: email and apiToken when deployment is cloud, pat when deployment is selfhosted."`
Auth *ChannelJiraAuth `json:"auth" required:"true" description:"The credentials: email and apiToken when deployment is cloud, pat when deployment is selfhosted."`
}
type JiraAuth interface {
deployment() JiraDeployment
validate() error
fillHTTPConfig(httpConfig *commoncfg.HTTPClientConfig)
}
// ChannelJiraAuth wraps the JiraAuth value because the OpenAPI reflector
// renders an interface-typed field as an empty component.
type ChannelJiraAuth struct {
JiraAuth
Email string `json:"email,omitempty" description:"The Atlassian account email used for basic auth. Required when deployment is cloud."`
APIToken string `json:"apiToken,omitempty" format:"password" description:"The Atlassian API token paired with email. Required when deployment is cloud. A secret; returned by create, update and fetch by ID, never by list."`
PAT string `json:"pat,omitempty" format:"password" description:"The Jira personal access token, sent as a Bearer header. Required when deployment is selfhosted. A secret; returned by create, update and fetch by ID, never by list."`
}
var (
_ jsonschema.OneOfExposer = ChannelJiraAuth{}
_ jsonschema.Preparer = ChannelJiraAuth{}
_ json.Marshaler = ChannelJiraAuth{}
)
func (ChannelJiraAuth) JSONSchemaOneOf() []any {
return []any{ChannelJiraCloudAuth{}, ChannelJiraSelfHostedAuth{}}
}
func (ChannelJiraAuth) PrepareJSONSchema(schema *jsonschema.Schema) error {
schema.Properties = nil
schema.Required = nil
return nil
}
func (a ChannelJiraAuth) MarshalJSON() ([]byte, error) {
if a.JiraAuth == nil {
return []byte("null"), nil
func (a ChannelJiraAuth) validate(deployment JiraDeployment) error {
if deployment == JiraDeploymentSelfHosted {
if a.PAT == "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth.pat is required for a self-hosted jira channel")
}
if a.Email != "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth.email is not supported for a self-hosted jira channel")
}
if a.APIToken != "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth.apiToken is not supported for a self-hosted jira channel")
}
return nil
}
return json.Marshal(a.JiraAuth)
}
type ChannelJiraCloudAuth struct {
Email string `json:"email" required:"true" description:"The Atlassian account email used for basic auth."`
APIToken string `json:"apiToken" required:"true" format:"password" description:"The Atlassian API token paired with email. A secret; returned by create, update and fetch by ID, never by list."`
}
func (ChannelJiraCloudAuth) deployment() JiraDeployment {
return JiraDeploymentCloud
}
func (a ChannelJiraCloudAuth) validate() error {
if a.Email == "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth.email is required for a jira cloud channel")
}
if a.APIToken == "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth.apiToken is required for a jira cloud channel")
}
if a.PAT != "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth.pat is not supported for a jira cloud channel")
}
return nil
}
func (a ChannelJiraCloudAuth) fillHTTPConfig(httpConfig *commoncfg.HTTPClientConfig) {
func (a ChannelJiraAuth) fillHTTPConfig(deployment JiraDeployment, httpConfig *commoncfg.HTTPClientConfig) {
if deployment == JiraDeploymentSelfHosted {
httpConfig.Authorization = &commoncfg.Authorization{
Type: jiraBearerAuthorizationType,
Credentials: commoncfg.Secret(a.PAT),
}
return
}
httpConfig.BasicAuth = &commoncfg.BasicAuth{
Username: a.Email,
Password: commoncfg.Secret(a.APIToken),
}
}
type ChannelJiraSelfHostedAuth struct {
PAT string `json:"pat" required:"true" format:"password" description:"The Jira personal access token, sent as a Bearer header. A secret; returned by create, update and fetch by ID, never by list."`
}
func (ChannelJiraSelfHostedAuth) deployment() JiraDeployment {
return JiraDeploymentSelfHosted
}
func (a ChannelJiraSelfHostedAuth) validate() error {
if a.PAT == "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth.pat is required for a self-hosted jira channel")
}
return nil
}
func (a ChannelJiraSelfHostedAuth) fillHTTPConfig(httpConfig *commoncfg.HTTPClientConfig) {
httpConfig.Authorization = &commoncfg.Authorization{
Type: jiraBearerAuthorizationType,
Credentials: commoncfg.Secret(a.PAT),
}
}
// UnmarshalJSON seeds send_resolved off, as JiraReceiverConfig does. The auth
// bytes are held back until deployment is known, which picks their type.
// UnmarshalJSON seeds send_resolved off, as JiraReceiverConfig does.
func (c *ChannelJiraConfig) UnmarshalJSON(data []byte) error {
type alias ChannelJiraConfig
envelope := struct {
*alias
Auth json.RawMessage `json:"auth"`
}{alias: (*alias)(c)}
if err := decodeStrict(data, &envelope); err != nil {
if err := decodeStrict(data, (*alias)(c)); err != nil {
return err
}
@@ -185,15 +142,6 @@ func (c *ChannelJiraConfig) UnmarshalJSON(data []byte) error {
c.Description.SetIfUnset(DefaultJiraDescriptionTemplate)
c.ReopenDuration.SetIfUnset(defaultJiraReopenDuration.String())
if err := c.validateDeployment(); err != nil {
return err
}
auth, err := decodeJiraAuth(c.Deployment, envelope.Auth)
if err != nil {
return err
}
c.Auth = ChannelJiraAuth{JiraAuth: auth}
return c.Validate()
}
@@ -215,13 +163,10 @@ func (c ChannelJiraConfig) Validate() error {
}
}
if c.Auth.JiraAuth == nil {
if c.Auth == nil {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth is required for a jira channel")
}
if c.Auth.deployment() != c.Deployment {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth does not match deployment %q", c.Deployment.StringValue())
}
if err := c.Auth.validate(); err != nil {
if err := c.Auth.validate(c.Deployment); err != nil {
return err
}
@@ -257,10 +202,10 @@ func (c ChannelJiraConfig) toUndefaultedReceiver(displayName string) (*Receiver,
// and EnableHTTP2 marshal unconditionally, so a zero value would persist them
// as false and read back as a config ChannelJiraConfig cannot represent.
httpConfig := commoncfg.DefaultHTTPClientConfig
if c.Auth.JiraAuth == nil {
if c.Auth == nil {
return nil, errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth is required for a jira channel")
}
c.Auth.fillHTTPConfig(&httpConfig)
c.Auth.fillHTTPConfig(c.Deployment, &httpConfig)
jira := &JiraReceiverConfig{
// JiraReceiverConfig seeds no send_resolved of its own, so unset means off.
@@ -336,7 +281,7 @@ func newChannelJiraConfigFromReceiver(name string, receiver *Receiver) (ChannelS
if !strings.EqualFold(authorization.Type, jiraBearerAuthorizationType) || authorization.CredentialsFile != "" || authorization.CredentialsRef != "" {
return nil, errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "channel %q must set http_config.authorization with type Bearer and inline credentials", name)
}
spec.Auth = ChannelJiraAuth{JiraAuth: ChannelJiraSelfHostedAuth{PAT: string(authorization.Credentials)}}
spec.Auth = &ChannelJiraAuth{PAT: string(authorization.Credentials)}
}
return spec, nil
}
@@ -345,7 +290,7 @@ func newChannelJiraConfigFromReceiver(name string, receiver *Receiver) (ChannelS
return nil, errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "channel %q sets http_config.authorization, which is not supported", name)
}
if jira.HTTPConfig.BasicAuth != nil {
spec.Auth = ChannelJiraAuth{JiraAuth: ChannelJiraCloudAuth{Email: jira.HTTPConfig.BasicAuth.Username, APIToken: string(jira.HTTPConfig.BasicAuth.Password)}}
spec.Auth = &ChannelJiraAuth{Email: jira.HTTPConfig.BasicAuth.Username, APIToken: string(jira.HTTPConfig.BasicAuth.Password)}
}
return spec, nil
@@ -463,26 +408,3 @@ func (c *JiraReceiverConfig) SearchPath() string {
}
return "/search/jql"
}
func decodeJiraAuth(deployment JiraDeployment, data json.RawMessage) (JiraAuth, error) {
if len(data) == 0 || string(data) == "null" {
return nil, errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth is required for a jira channel")
}
var auth JiraAuth
var err error
switch deployment {
case JiraDeploymentSelfHosted:
var selfHosted ChannelJiraSelfHostedAuth
err = decodeStrict(data, &selfHosted)
auth = selfHosted
default:
var cloud ChannelJiraCloudAuth
err = decodeStrict(data, &cloud)
auth = cloud
}
if err != nil {
return nil, errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.auth for a %s jira channel: %s", deployment.StringValue(), err.Error())
}
return auth, nil
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/swaggest/jsonschema-go"
)
func jiraReceiverJSON(project, issueType string, withAuth bool) string {
@@ -177,9 +178,13 @@ func TestChannelJiraConfigUnmarshal(t *testing.T) {
{"RootCredentials_Rejected", `{"deployment":"cloud","site":"https://acme.atlassian.net","project":"OPS","issueType":"Task","email":"e","apiToken":"t"}`, `unknown field "email"`, JiraDeploymentCloud},
{"Cloud_MissingEmail", `{"deployment":"cloud","site":"https://acme.atlassian.net","project":"OPS","issueType":"Task","auth":{"apiToken":"t"}}`, "config.spec.auth.email is required for a jira cloud channel", JiraDeploymentCloud},
{"Cloud_MissingAPIToken", `{"deployment":"cloud","site":"https://acme.atlassian.net","project":"OPS","issueType":"Task","auth":{"email":"e"}}`, "config.spec.auth.apiToken is required for a jira cloud channel", JiraDeploymentCloud},
{"Cloud_WithPAT_Rejected", `{"deployment":"cloud","site":"https://acme.atlassian.net","project":"OPS","issueType":"Task","auth":{"email":"e","apiToken":"t","pat":"p"}}`, `unknown field "pat"`, JiraDeploymentCloud},
{"Cloud_WithPAT_Rejected", `{"deployment":"cloud","site":"https://acme.atlassian.net","project":"OPS","issueType":"Task","auth":{"email":"e","apiToken":"t","pat":"p"}}`, "config.spec.auth.pat is not supported for a jira cloud channel", JiraDeploymentCloud},
{"Cloud_WithBlankPAT", `{"deployment":"cloud","site":"https://acme.atlassian.net","project":"OPS","issueType":"Task","auth":{"email":"e","apiToken":"t","pat":""}}`, "", JiraDeploymentCloud},
{"SelfHosted_MissingPAT", `{"deployment":"selfhosted","site":"https://jira.acme.com","project":"OPS","issueType":"Task","auth":{}}`, "config.spec.auth.pat is required for a self-hosted jira channel", JiraDeploymentSelfHosted},
{"SelfHosted_WithEmail_Rejected", `{"deployment":"selfhosted","site":"https://jira.acme.com","project":"OPS","issueType":"Task","auth":{"pat":"p","email":"e"}}`, `unknown field "email"`, JiraDeploymentSelfHosted},
{"SelfHosted_WithEmail_Rejected", `{"deployment":"selfhosted","site":"https://jira.acme.com","project":"OPS","issueType":"Task","auth":{"pat":"p","email":"e"}}`, "config.spec.auth.email is not supported for a self-hosted jira channel", JiraDeploymentSelfHosted},
{"SelfHosted_WithAPIToken_Rejected", `{"deployment":"selfhosted","site":"https://jira.acme.com","project":"OPS","issueType":"Task","auth":{"pat":"p","apiToken":"t"}}`, "config.spec.auth.apiToken is not supported for a self-hosted jira channel", JiraDeploymentSelfHosted},
{"SelfHosted_WithBlankEmail", `{"deployment":"selfhosted","site":"https://jira.acme.com","project":"OPS","issueType":"Task","auth":{"pat":"p","email":""}}`, "", JiraDeploymentSelfHosted},
{"NullAuth_Rejected", `{"deployment":"cloud","site":"https://acme.atlassian.net","project":"OPS","issueType":"Task","auth":null}`, "config.spec.auth is required for a jira channel", JiraDeploymentCloud},
{"UnknownDeployment", `{"deployment":"onprem","site":"https://jira.acme.com","project":"OPS","issueType":"Task","auth":{"pat":"p"}}`, `config.spec.deployment "onprem" is not supported`, JiraDeployment{}},
{"EmptyDeployment_Rejected", `{"deployment":"","site":"https://jira.acme.com","project":"OPS","issueType":"Task","auth":{"pat":"p"}}`, "string must not be empty", JiraDeployment{}},
}
@@ -208,13 +213,13 @@ func TestChannelJiraConfigReceiverAuth(t *testing.T) {
}{
{
name: "Cloud_BasicAuth",
spec: ChannelJiraConfig{Deployment: JiraDeploymentCloud, Site: "https://acme.atlassian.net", Project: "OPS", IssueType: "Task", Auth: ChannelJiraAuth{JiraAuth: ChannelJiraCloudAuth{Email: "e@acme.com", APIToken: "t"}}},
spec: ChannelJiraConfig{Deployment: JiraDeploymentCloud, Site: "https://acme.atlassian.net", Project: "OPS", IssueType: "Task", Auth: &ChannelJiraAuth{Email: "e@acme.com", APIToken: "t"}},
expectBasic: &commoncfg.BasicAuth{Username: "e@acme.com", Password: "t"},
expectJSONKey: `"basic_auth"`,
},
{
name: "SelfHosted_BearerAuthorization",
spec: ChannelJiraConfig{Deployment: JiraDeploymentSelfHosted, Site: "https://jira.acme.com", Project: "OPS", IssueType: "Task", Auth: ChannelJiraAuth{JiraAuth: ChannelJiraSelfHostedAuth{PAT: "p"}}},
spec: ChannelJiraConfig{Deployment: JiraDeploymentSelfHosted, Site: "https://jira.acme.com", Project: "OPS", IssueType: "Task", Auth: &ChannelJiraAuth{PAT: "p"}},
expectBearer: "p",
expectJSONKey: `"deployment":"selfhosted"`,
},
@@ -243,3 +248,45 @@ func TestChannelJiraConfigReceiverAuth(t *testing.T) {
})
}
}
func TestChannelJiraAuthJSONSchema(t *testing.T) {
reflector := jsonschema.Reflector{}
schema, err := reflector.Reflect(ChannelJiraAuth{})
require.NoError(t, err)
assert.Empty(t, schema.OneOf)
assert.Empty(t, schema.Required)
require.Len(t, schema.Properties, 3)
testCases := []struct {
name string
property string
format string
}{
{name: "Email_Optional_PlainString", property: "email"},
{name: "APIToken_Optional_Password", property: "apiToken", format: "password"},
{name: "PAT_Optional_Password", property: "pat", format: "password"},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
property, ok := schema.Properties[testCase.property]
require.True(t, ok)
require.NotNil(t, property.TypeObject)
raw, err := json.Marshal(property.TypeObject)
require.NoError(t, err)
var got struct {
Type string `json:"type"`
Format string `json:"format"`
Description string `json:"description"`
}
require.NoError(t, json.Unmarshal(raw, &got))
assert.Equal(t, "string", got.Type)
assert.Equal(t, testCase.format, got.Format)
assert.NotEmpty(t, got.Description)
})
}
}

View File

@@ -75,7 +75,7 @@ func TestPostableChannelValidate(t *testing.T) {
DisplayName: "jira",
Config: ChannelConfig{Kind: ChannelKindJira, Spec: &ChannelJiraConfig{
Deployment: JiraDeploymentCloud, Site: "https://acme.atlassian.net", Project: "OPS", IssueType: "Bug",
Auth: ChannelJiraAuth{JiraAuth: ChannelJiraCloudAuth{Email: "oncall@acme.com", APIToken: "api-token"}},
Auth: &ChannelJiraAuth{Email: "oncall@acme.com", APIToken: "api-token"},
Summary: valuer.MustNewUnsetOrNonEmptyString("jira summary"), Description: valuer.MustNewUnsetOrNonEmptyString("jira description"), ReopenDuration: valuer.MustNewUnsetOrNonEmptyString("three days"),
}},
},

View File

@@ -198,7 +198,7 @@ func TestDeriveChannelConfigRoundTripsEveryFieldOfEveryKind(t *testing.T) {
ReopenDuration: valuer.MustNewUnsetOrNonEmptyString("3d"),
WontFixResolution: "Won't Do",
CustomFields: map[string]any{"customfield_10010": "Ops"},
Auth: ChannelJiraAuth{JiraAuth: ChannelJiraCloudAuth{Email: "oncall@acme.com", APIToken: "api-token"}},
Auth: &ChannelJiraAuth{Email: "oncall@acme.com", APIToken: "api-token"},
},
expectedRoundTrip: &ChannelJiraConfig{
SendResolved: &sendResolved,
@@ -215,7 +215,7 @@ func TestDeriveChannelConfigRoundTripsEveryFieldOfEveryKind(t *testing.T) {
ReopenDuration: valuer.MustNewUnsetOrNonEmptyString("3d"),
WontFixResolution: "Won't Do",
CustomFields: map[string]any{"customfield_10010": "Ops"},
Auth: ChannelJiraAuth{JiraAuth: ChannelJiraCloudAuth{Email: "oncall@acme.com", APIToken: "api-token"}},
Auth: &ChannelJiraAuth{Email: "oncall@acme.com", APIToken: "api-token"},
},
},
{
@@ -230,7 +230,7 @@ func TestDeriveChannelConfigRoundTripsEveryFieldOfEveryKind(t *testing.T) {
Summary: valuer.MustNewUnsetOrNonEmptyString("jira summary"),
Description: valuer.MustNewUnsetOrNonEmptyString("jira description"),
ReopenDuration: valuer.MustNewUnsetOrNonEmptyString("3d"),
Auth: ChannelJiraAuth{JiraAuth: ChannelJiraSelfHostedAuth{PAT: "personal-access-token"}},
Auth: &ChannelJiraAuth{PAT: "personal-access-token"},
},
expectedRoundTrip: &ChannelJiraConfig{
SendResolved: &sendResolved,
@@ -241,7 +241,7 @@ func TestDeriveChannelConfigRoundTripsEveryFieldOfEveryKind(t *testing.T) {
Summary: valuer.MustNewUnsetOrNonEmptyString("jira summary"),
Description: valuer.MustNewUnsetOrNonEmptyString("jira description"),
ReopenDuration: valuer.MustNewUnsetOrNonEmptyString("3d"),
Auth: ChannelJiraAuth{JiraAuth: ChannelJiraSelfHostedAuth{PAT: "personal-access-token"}},
Auth: &ChannelJiraAuth{PAT: "personal-access-token"},
},
},
{

View File

@@ -0,0 +1,89 @@
package querybuildertypesv5
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestQueryRangeRequest_ValidatePatterns(t *testing.T) {
logsQuery := func(edit func(*QueryBuilderQuery[LogAggregation])) QueryEnvelope {
spec := QueryBuilderQuery[LogAggregation]{
Name: "A",
Signal: telemetrytypes.SignalLogs,
Filter: &Filter{Expression: "service.name = 'api'"},
Limit: 50,
}
if edit != nil {
edit(&spec)
}
return QueryEnvelope{Type: QueryTypeBuilder, Spec: spec}
}
request := func(queries ...QueryEnvelope) QueryRangeRequest {
return QueryRangeRequest{
Start: 1640995200000,
End: 1640998800000,
RequestType: RequestTypePattern,
CompositeQuery: CompositeQuery{Queries: queries},
}
}
testCases := []struct {
name string
request QueryRangeRequest
errMsg string
}{
{
name: "LogsQuery_WithGroupBy",
request: request(logsQuery(func(q *QueryBuilderQuery[LogAggregation]) {
q.GroupBy = []GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "service.name"}}}
})),
},
{
name: "TracesQuery",
request: request(QueryEnvelope{Type: QueryTypeBuilder, Spec: QueryBuilderQuery[TraceAggregation]{
Name: "A", Signal: telemetrytypes.SignalTraces,
}}),
errMsg: "pattern requests support only a logs builder query",
},
{
name: "TwoQueries",
request: request(logsQuery(nil), logsQuery(func(q *QueryBuilderQuery[LogAggregation]) { q.Name = "B" })),
errMsg: "exactly one logs builder query, got 2 queries",
},
{
name: "Aggregations",
request: request(logsQuery(func(q *QueryBuilderQuery[LogAggregation]) { q.Aggregations = []LogAggregation{{Expression: "count()"}} })),
errMsg: "takes no aggregations",
},
{
name: "Disabled",
request: request(logsQuery(func(q *QueryBuilderQuery[LogAggregation]) { q.Disabled = true })),
errMsg: "can't be disabled",
},
{
name: "Having",
request: request(logsQuery(func(q *QueryBuilderQuery[LogAggregation]) { q.Having = &Having{Expression: "count() > 1"} })),
errMsg: "don't support having",
},
{
name: "LimitOverMax",
request: request(logsQuery(func(q *QueryBuilderQuery[LogAggregation]) { q.Limit = PatternsMaxLimit + 1 })),
errMsg: "at most 1000 patterns",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
err := testCase.request.Validate()
if testCase.errMsg == "" {
require.NoError(t, err)
return
}
require.Error(t, err)
assert.Contains(t, err.Error(), testCase.errMsg)
})
}
}

View File

@@ -19,7 +19,7 @@ func (r *RequestType) UnmarshalJSON(data []byte) error {
}
v := RequestType{valuer.NewString(s)}
switch v {
case RequestTypeScalar, RequestTypeTimeSeries, RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace, RequestTypeDistribution, RequestTypeHeatmap:
case RequestTypeScalar, RequestTypeTimeSeries, RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace, RequestTypeDistribution, RequestTypeHeatmap, RequestTypePattern:
*r = v
return nil
default:
@@ -46,6 +46,9 @@ var (
// TimeSeriesData carrying one count per histogram bucket at each timestamp,
// with the shared bucket upper bounds on the aggregation's meta.
RequestTypeHeatmap = RequestType{valuer.NewString("heatmap")}
// PatternsData: the top log patterns of one logs query, counted on a
// sample, example: the patterns view of the logs explorer.
RequestTypePattern = RequestType{valuer.NewString("pattern")}
)
// IsAggregation returns true for request types that produce aggregated results
@@ -75,5 +78,6 @@ func (RequestType) Enum() []any {
RequestTypeTrace,
RequestTypeHeatmap,
RequestTypeDistribution,
RequestTypePattern,
}
}

View File

@@ -42,7 +42,7 @@ type QueryWarnDataAdditional struct {
}
type QueryData struct {
Results []any `json:"results" description:"One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests."`
Results []any `json:"results" description:"One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, DistributionData for distribution and PatternsData for pattern requests."`
}
var _ jsonschema.OneOfExposer = QueryData{}
@@ -54,6 +54,7 @@ func (QueryData) JSONSchemaOneOf() []any {
ScalarData{},
RawData{},
DistributionData{},
PatternsData{},
}
}
@@ -129,7 +130,7 @@ var _ jsonschema.Preparer = &QueryRangeResponse{}
// PrepareJSONSchema adds description to the QueryRangeResponse schema.
func (q *QueryRangeResponse) PrepareJSONSchema(schema *jsonschema.Schema) error {
schema.WithDescription("Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, or DistributionData for distribution requests.")
schema.WithDescription("Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, DistributionData for distribution, or PatternsData for pattern requests.")
return nil
}
@@ -649,3 +650,46 @@ func (r RawData) MarshalJSON() ([]byte, error) {
type Alias RawData
return json.Marshal((*Alias)(&r))
}
// The columns of a patterns statement, which the querier reads by name.
// Any other column is a group-by key; the first is always service.name.
const (
PatternsColumnPattern = "__pattern"
PatternsColumnID = "__pattern_id"
PatternsColumnSampledCount = "__sampled_count"
PatternsColumnSampleRate = "__sample_rate"
PatternsColumnTotal = "__total"
PatternsColumnFirstSeen = "__first_seen"
PatternsColumnLastSeen = "__last_seen"
PatternsColumnSamples = "__samples"
PatternsColumnSeverities = "__severities"
PatternsColumnTrendTs = "__trend_ts"
PatternsColumnTrendCounts = "__trend_counts"
)
// PatternsData is the top log patterns of one logs query, counted on a
// sample of its logs.
type PatternsData struct {
QueryName string `json:"queryName" required:"true" description:"The name of the query that produced this result."`
SampleRate uint64 `json:"sampleRate" required:"true" description:"One in sampleRate of the matching logs was read and every count is scaled up by it. 1 means every log was read and the counts are exact."`
Total uint64 `json:"total" required:"true" description:"The number of logs the query matched in the time range, over every pattern."`
Patterns []*Pattern `json:"patterns" required:"true" description:"The patterns, most logs first."`
}
type Pattern struct {
ID string `json:"id" required:"true" description:"The pattern's ID, the decimal cityHash64 of its text."`
Pattern string `json:"pattern" required:"true" description:"The pattern text: the log message with the values that change replaced by placeholders such as <*>, <IP> or <UUID>. Filter on it with pattern('<pattern text>')."`
Labels []*Label `json:"labels,omitempty" description:"The group-by field values the pattern was counted for: always service.name first, then the query's own group by."`
Count uint64 `json:"count" required:"true" description:"The estimated number of logs of the pattern in the time range: the sampled count times sampleRate."`
Percent float64 `json:"percent" required:"true" description:"The pattern's count as a percentage of total."`
FirstSeen int64 `json:"firstSeen" required:"true" description:"The Unix time in milliseconds of the earliest sampled log of the pattern in the time range."`
LastSeen int64 `json:"lastSeen" required:"true" description:"The Unix time in milliseconds of the latest sampled log of the pattern in the time range."`
Samples []string `json:"samples" required:"true" description:"Up to three sampled log messages of the pattern."`
Severities []string `json:"severities" required:"true" description:"The most common severity texts of the pattern's logs, most common first."`
Trend []*PatternTrendPoint `json:"trend" required:"true" description:"The estimated number of logs of the pattern in each step of the time range, oldest first. Steps without a sampled log are left out."`
}
type PatternTrendPoint struct {
Timestamp int64 `json:"timestamp" required:"true" description:"The Unix time in milliseconds the step starts at."`
Count uint64 `json:"count" required:"true" description:"The estimated number of logs of the pattern in the step."`
}

View File

@@ -607,6 +607,10 @@ func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
return err
}
if err := r.validatePatterns(); err != nil {
return err
}
// raw/trace request types don't support metric queries;
// metrics are always aggregated and there is no raw form.
if r.RequestType == RequestTypeRaw || r.RequestType == RequestTypeRawStream || r.RequestType == RequestTypeTrace {
@@ -651,6 +655,10 @@ func (r *QueryRangeRequest) ValidateRequestScope() ([]ValidationOption, error) {
return nil, err
}
if err := r.validatePatterns(); err != nil {
return nil, err
}
if r.RequestType == RequestTypeRaw || r.RequestType == RequestTypeRawStream || r.RequestType == RequestTypeTrace {
for _, envelope := range r.CompositeQuery.Queries {
if envelope.GetSignal() == telemetrytypes.SignalMetrics {
@@ -979,6 +987,43 @@ func (r *QueryRangeRequest) validateBucketCounting() error {
return nil
}
// PatternsMaxLimit is the most patterns a pattern request returns.
const PatternsMaxLimit = 1000
// validatePatterns checks a pattern request: one enabled logs builder query,
// which the request counts by pattern itself, so it takes no aggregation,
// having, or function.
func (r *QueryRangeRequest) validatePatterns() error {
if r.RequestType != RequestTypePattern {
return nil
}
if len(r.CompositeQuery.Queries) != 1 {
return errors.NewInvalidInputf(errors.CodeInvalidInput,
"pattern requests take exactly one logs builder query, got %d queries", len(r.CompositeQuery.Queries))
}
envelope := r.CompositeQuery.Queries[0]
spec, ok := envelope.Spec.(QueryBuilderQuery[LogAggregation])
if !ok || envelope.Type != QueryTypeBuilder || spec.Signal != telemetrytypes.SignalLogs {
return errors.NewInvalidInputf(errors.CodeInvalidInput,
"pattern requests support only a logs builder query, got %q", envelope.Type.StringValue())
}
switch {
case spec.Disabled:
return errors.NewInvalidInputf(errors.CodeInvalidInput, "the query of a pattern request can't be disabled")
case len(spec.Aggregations) > 0:
return errors.NewInvalidInputf(errors.CodeInvalidInput,
"pattern requests count the logs of each pattern themselves, so the query takes no aggregations")
case spec.Having != nil && spec.Having.Expression != "":
return errors.NewInvalidInputf(errors.CodeInvalidInput, "pattern requests don't support having")
case len(spec.Functions) > 0:
return errors.NewInvalidInputf(errors.CodeInvalidInput, "pattern requests don't support functions")
case spec.Limit > PatternsMaxLimit:
return errors.NewInvalidInputf(errors.CodeInvalidInput,
"pattern requests return at most %d patterns, got limit %d", PatternsMaxLimit, spec.Limit)
}
return nil
}
// extractEnabledBucketOptions returns the bucket axis a query asks for, nil for
// a disabled one: a heatmap draws the enabled query alone, so what the rest
// carry is never read. A promql or clickhouse query has no say in its axis and
@@ -1107,6 +1152,9 @@ func GetValidationOptions(requestType RequestType) []ValidationOption {
return []ValidationOption{WithSkipSelectFieldValidation(), WithReduceToValidation()}
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace:
return []ValidationOption{WithSkipAggregationValidation(), WithSkipHavingValidation(), WithSkipAggregationOrderBy(), WithSkipGroupByValidation()}
case RequestTypePattern:
// a pattern request counts the logs itself, so it has no aggregation to check
return []ValidationOption{WithSkipAggregationValidation(), WithSkipHavingValidation(), WithSkipAggregationOrderBy(), WithSkipSelectFieldValidation()}
default:
return []ValidationOption{}
}

View File

@@ -1461,7 +1461,7 @@ func TestRequestType_IsAggregation(t *testing.T) {
}
func TestQueryRangeRequest_ValidateRequestType(t *testing.T) {
validRequestTypes := []string{"valid request types are `scalar`, `time_series`, `raw`, `raw_stream`, `trace`, `heatmap`, `distribution`"}
validRequestTypes := []string{"valid request types are `scalar`, `time_series`, `raw`, `raw_stream`, `trace`, `heatmap`, `distribution`, `pattern`"}
testCases := []struct {
name string

View File

@@ -54,6 +54,9 @@ const (
SchemaVersionV2Alpha1 = "v2alpha1"
)
// legacyRuleKeys are accepted from older clients but kept out of the schema; nothing reads them.
var legacyRuleKeys = []string{"source", "version"}
type RuleDataKind string
const (
@@ -73,9 +76,9 @@ type PostableRule struct {
Disabled bool `json:"disabled" description:"Whether the rule is paused. A disabled rule is neither evaluated nor notifies. Defaults to false."`
Source string `json:"source,omitempty" description:"The URL of the page the rule was created from. Informational; it is not used for evaluation."`
Version string `json:"version" description:"The query payload version. Nothing reads it."`
// legacyRuleKeys: stored and echoed as sent
Source string `json:"source,omitempty"`
Version string `json:"version,omitempty"`
Evaluation *EvaluationEnvelope `json:"evaluation" required:"true" description:"The evaluation window, as a kind and spec."`
SchemaVersion string `json:"schemaVersion" required:"true" enum:"v2alpha1" description:"The rule payload schema."`
@@ -263,13 +266,9 @@ func (r *PostableRule) DecodeStrict(ruleStr string) error {
var _ jsonschema.Preparer = PostableRule{}
// PrepareJSONSchema marks version and source deprecated: nothing reads them,
// they stay only so existing clients keep a stable schema.
func (PostableRule) PrepareJSONSchema(schema *jsonschema.Schema) error {
for _, name := range []string{"version", "source"} {
if property, ok := schema.Properties[name]; ok && property.TypeObject != nil {
property.TypeObject.WithDeprecated(true)
}
for _, key := range legacyRuleKeys {
delete(schema.Properties, key)
}
return nil
}

View File

@@ -28,7 +28,6 @@ func TestParseIntoRule(t *testing.T) {
"description": "Test description",
"ruleType": "threshold_rule",
"schemaVersion": "v2alpha1",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -68,7 +67,6 @@ func TestParseIntoRule(t *testing.T) {
content: []byte(`{
"alert": "PromQLRule",
"schemaVersion": "v2alpha1",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "promql",
@@ -138,7 +136,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "V2Alpha1Test",
"schemaVersion": "v2alpha1",
"version": "v5",
"ruleType": "threshold_rule",
"condition": {
"compositeQuery": {
@@ -205,7 +202,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "MixedFieldsTest",
"schemaVersion": "v2alpha1",
"version": "v5",
"ruleType": "threshold_rule",
"preferredChannels": ["slack"],
"condition": {
@@ -235,7 +231,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "MissingEvalTest",
"schemaVersion": "v2alpha1",
"version": "v5",
"ruleType": "threshold_rule",
"condition": {
"compositeQuery": {
@@ -277,7 +272,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "MissingNotifTest",
"schemaVersion": "v2alpha1",
"version": "v5",
"ruleType": "threshold_rule",
"condition": {
"compositeQuery": {
@@ -319,7 +313,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "MissingThresholdsTest",
"schemaVersion": "v2alpha1",
"version": "v5",
"ruleType": "threshold_rule",
"condition": {
"compositeQuery": {
@@ -359,7 +352,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "BadSchemaTest",
"schemaVersion": "v3",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -386,7 +378,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
initRule: PostableRule{},
content: []byte(`{
"alert": "MissingSchemaTest",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -440,7 +431,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
func TestParseIntoRuleThresholdGeneration(t *testing.T) {
content := []byte(`{
"alert": "TestThresholds",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -514,7 +504,6 @@ func TestParseIntoRuleMultipleThresholds(t *testing.T) {
"schemaVersion": "v2alpha1",
"alert": "MultiThresholdAlert",
"ruleType": "threshold_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",

View File

@@ -51,7 +51,6 @@ func TestV2RoundTripRolling(t *testing.T) {
"description": "watches cpu",
"ruleType": "promql_rule",
"schemaVersion": "v2alpha1",
"version": "v5",
"disabled": true,
"labels": ` + labels + `,
"annotations": ` + annotations + `,
@@ -159,7 +158,6 @@ func TestV2MinimalReadShape(t *testing.T) {
assert.JSONEq(t, `false`, string(ns["usePolicy"]))
assert.JSONEq(t, `false`, string(body["disabled"]))
assert.JSONEq(t, `""`, string(body["version"]))
var condition struct {
Thresholds struct {

View File

@@ -26,7 +26,6 @@ func validV2Alpha1Builder() string {
return `{
"alert": "TestAlert",
"alertType": "METRIC_BASED_ALERT",
"version": "v5",
"schemaVersion": "v2alpha1",
"ruleType": "threshold_rule",
"condition": {
@@ -64,7 +63,6 @@ func validV2Alpha1Promql() string {
return `{
"alert": "PromQLAlert",
"alertType": "METRIC_BASED_ALERT",
"version": "v5",
"schemaVersion": "v2alpha1",
"ruleType": "promql_rule",
"condition": {
@@ -156,20 +154,6 @@ func TestValidate_PostableRule_Common(t *testing.T) {
errSubstr: "alert",
},
// version is deprecated: missing/empty defaults to "v5", anything else is kept as sent
{
name: "missing version defaults to v5",
json: removeField(validV2Alpha1Builder(), "version"),
},
{
name: "any version is accepted",
json: patchJSON(validV2Alpha1Builder(), `{"version": "v4"}`),
},
{
name: "empty version defaults to v5",
json: patchJSON(validV2Alpha1Builder(), `{"version": ""}`),
},
// alert type, capital case to avoid breaking changes
// no types.valuer for now
{
@@ -230,13 +214,13 @@ func TestValidate_PostableRule_Common(t *testing.T) {
// rule condition should never be missing
{
name: "missing condition entirely",
json: `{"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1"}`,
json: `{"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1"}`,
wantErr: true,
errSubstr: "condition",
},
{
name: "null condition",
json: `{"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "condition": null}`,
json: `{"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "condition": null}`,
wantErr: true,
errSubstr: "condition",
},
@@ -664,7 +648,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "missing thresholds",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]}
@@ -678,7 +662,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "missing evaluation",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -692,7 +676,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "missing notificationSettings",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -708,7 +692,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
// {
// name: "v2alpha1 with condition.target",
// json: `{
// "alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
// "alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
// "condition": {
// "compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
// "thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90.0, "matchType": "1", "op": "1"}]},
@@ -723,7 +707,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
// {
// name: "v2alpha1 with condition.op",
// json: `{
// "alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
// "alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
// "condition": {
// "compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
// "thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90.0, "matchType": "1", "op": "1"}]},
@@ -738,7 +722,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
// {
// name: "v2alpha1 with condition.matchType",
// json: `{
// "alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
// "alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
// "condition": {
// "compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
// "thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90.0, "matchType": "1", "op": "1"}]},
@@ -759,7 +743,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
// {
// name: "v2alpha1 with ALL v1 fields : multiple errors",
// json: `{
// "alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
// "alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
// "preferredChannels": ["slack"],
// "condition": {
// "compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -773,7 +757,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "v2alpha1 promql rule without thresholds is not ok",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "promql_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "promql_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "promql", "panelType": "graph", "queries": [{"type": "promql", "spec": {"name": "A", "query": "up == 0", "disabled": false}}]}
@@ -788,7 +772,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "renotify enabled with zero interval",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -803,7 +787,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "renotify disabled with zero interval is ok",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -816,7 +800,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "invalid renotify alert state",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -833,7 +817,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "threshold with empty name",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -848,7 +832,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "threshold with null target",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -863,7 +847,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "threshold with invalid op",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -878,7 +862,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "threshold with invalid matchType",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName":"cpu","spaceAggregation":"p50"}], "stepInterval": "5m"}}]},
@@ -893,7 +877,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "threshold with literal op above is valid",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName":"cpu","spaceAggregation":"p50"}], "stepInterval": "5m"}}]},
@@ -906,7 +890,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "unknown threshold kind",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName":"cpu","spaceAggregation":"p50"}], "stepInterval": "5m"}}]},
@@ -923,7 +907,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "rolling eval with zero window",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName":"cpu","spaceAggregation":"p50"}], "stepInterval": "5m"}}]},
@@ -938,7 +922,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "rolling eval with zero frequency",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName":"cpu","spaceAggregation":"p50"}], "stepInterval": "5m"}}]},
@@ -953,7 +937,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "unknown evaluation kind",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName":"cpu","spaceAggregation":"p50"}], "stepInterval": "5m"}}]},
@@ -970,7 +954,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
{
name: "promql query with empty query string",
json: `{
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "promql_rule",
"alert": "Test", "alertType": "METRIC_BASED_ALERT", "schemaVersion": "v2alpha1", "ruleType": "promql_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "promql", "panelType": "graph", "queries": [{"type": "promql", "spec": {"name": "A", "query": "", "disabled": false}}]}
@@ -1018,18 +1002,18 @@ func TestValidate_GarbagePayloads(t *testing.T) {
{"number", `42`},
{"array", `[]`},
{"just alert name", `{"alert": "x"}`},
{"condition is a string", `{"alert": "x", "version": "v5", "condition": "not_an_object"}`},
{"condition is a number", `{"alert": "x", "version": "v5", "condition": 42}`},
{"condition is an array", `{"alert": "x", "version": "v5", "condition": []}`},
{"queries is a string", `{"alert": "x", "version": "v5", "condition": {"compositeQuery": {"queries": "bad"}}}`},
{"queries is a number", `{"alert": "x", "version": "v5", "condition": {"compositeQuery": {"queries": 42}}}`},
{"thresholds is a string", `{"alert": "x", "version": "v5", "schemaVersion": "v2alpha1", "condition": {"compositeQuery": {"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]}, "thresholds": "bad"}}`},
{"evaluation is a string", `{"alert": "x", "version": "v5", "schemaVersion": "v2alpha1", "condition": {"compositeQuery": {"queries": []}}, "evaluation": "bad"}`},
{"notificationSettings is a number", `{"alert": "x", "version": "v5", "schemaVersion": "v2alpha1", "condition": {"compositeQuery": {"queries": []}}, "notificationSettings": 42}`},
{"deeply nested garbage", `{"alert": "x", "version": "v5", "condition": {"compositeQuery": {"queryType": "builder", "queries": [{"type": "unknown_type", "spec": {}}]}}}`},
{"condition is a string", `{"alert": "x", "condition": "not_an_object"}`},
{"condition is a number", `{"alert": "x", "condition": 42}`},
{"condition is an array", `{"alert": "x", "condition": []}`},
{"queries is a string", `{"alert": "x", "condition": {"compositeQuery": {"queries": "bad"}}}`},
{"queries is a number", `{"alert": "x", "condition": {"compositeQuery": {"queries": 42}}}`},
{"thresholds is a string", `{"alert": "x", "schemaVersion": "v2alpha1", "condition": {"compositeQuery": {"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]}, "thresholds": "bad"}}`},
{"evaluation is a string", `{"alert": "x", "schemaVersion": "v2alpha1", "condition": {"compositeQuery": {"queries": []}}, "evaluation": "bad"}`},
{"notificationSettings is a number", `{"alert": "x", "schemaVersion": "v2alpha1", "condition": {"compositeQuery": {"queries": []}}, "notificationSettings": 42}`},
{"deeply nested garbage", `{"alert": "x", "condition": {"compositeQuery": {"queryType": "builder", "queries": [{"type": "unknown_type", "spec": {}}]}}}`},
{"version is a number", `{"alert": "x", "version": 5, "condition": {}}`},
{"labels is an array", `{"alert": "x", "version": "v5", "labels": [], "condition": {}}`},
{"op is a number", `{"alert": "x", "version": "v5", "condition": {"compositeQuery": {"queries": []}, "op": 1}}`},
{"labels is an array", `{"alert": "x", "labels": [], "condition": {}}`},
{"op is a number", `{"alert": "x", "condition": {"compositeQuery": {"queries": []}, "op": 1}}`},
}
for _, tt := range payloads {
@@ -1169,7 +1153,7 @@ func TestValidate_MarshalRoundTrip(t *testing.T) {
func TestValidate_MultipleErrors(t *testing.T) {
t.Run("v2alpha1 multiple suggestion errors returned together", func(t *testing.T) {
j := `{
"alert": "Test", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"alert": "Test", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]}
@@ -1224,7 +1208,7 @@ func TestValidate_AllSymbolAndShortOperators(t *testing.T) {
func TestValidate_V2Alpha1_CumulativeEvaluation(t *testing.T) {
mkRule := func(evalJSON string) string {
return `{
"alert": "Test", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule", "alertType": "METRIC_BASED_ALERT",
"alert": "Test", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule", "alertType": "METRIC_BASED_ALERT",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -1307,7 +1291,7 @@ func TestValidate_V2Alpha1_CumulativeEvaluation(t *testing.T) {
func TestValidate_V2Alpha1_MultipleThresholds(t *testing.T) {
t.Run("warning and critical thresholds", func(t *testing.T) {
j := `{
"alert": "Test", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule", "alertType": "METRIC_BASED_ALERT",
"alert": "Test", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule", "alertType": "METRIC_BASED_ALERT",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -1327,7 +1311,7 @@ func TestValidate_V2Alpha1_MultipleThresholds(t *testing.T) {
t.Run("thresholds with literal ops and matchTypes", func(t *testing.T) {
j := `{
"alert": "Test", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule", "alertType": "METRIC_BASED_ALERT",
"alert": "Test", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule", "alertType": "METRIC_BASED_ALERT",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},
@@ -1350,7 +1334,7 @@ func TestValidate_V2Alpha1_MultipleThresholds(t *testing.T) {
func TestValidate_V2Alpha1_NotificationStates(t *testing.T) {
mkRule := func(states string) string {
return `{
"alert": "Test", "version": "v5", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule", "alertType": "METRIC_BASED_ALERT",
"alert": "Test", "schemaVersion": "v2alpha1", "ruleType": "threshold_rule", "alertType": "METRIC_BASED_ALERT",
"condition": {
"selectedQueryName": "A",
"compositeQuery": {"queryType": "builder", "panelType": "graph", "queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}]},

View File

@@ -111,6 +111,8 @@ func (v *variableReplacementVisitor) Visit(tree antlr.ParseTree) any {
return v.VisitFullText(t)
case *grammar.SearchCallContext:
return v.VisitSearchCall(t)
case *grammar.PatternCallContext:
return v.VisitPatternCall(t)
case *grammar.FunctionCallContext:
return v.VisitFunctionCall(t)
case *grammar.FunctionParamListContext:
@@ -207,6 +209,8 @@ func (v *variableReplacementVisitor) VisitPrimary(ctx *grammar.PrimaryContext) a
return v.Visit(ctx.FunctionCall())
} else if ctx.SearchCall() != nil {
return v.Visit(ctx.SearchCall())
} else if ctx.PatternCall() != nil {
return v.Visit(ctx.PatternCall())
} else if ctx.FullText() != nil {
return v.Visit(ctx.FullText())
}
@@ -423,6 +427,18 @@ func (v *variableReplacementVisitor) VisitSearchCall(ctx *grammar.SearchCallCont
return "search" + result
}
func (v *variableReplacementVisitor) VisitPatternCall(ctx *grammar.PatternCallContext) any {
if ctx.ValueList() == nil {
return "pattern()"
}
// VisitValueList already parenthesizes the args and propagates the __all__ marker.
result := v.Visit(ctx.ValueList()).(string)
if result == specialSkipMarker {
return specialSkipMarker
}
return "pattern" + result
}
func (v *variableReplacementVisitor) VisitFunctionParamList(ctx *grammar.FunctionParamListContext) any {
params := ctx.AllFunctionParam()
parts := make([]string, 0, len(params))

View File

@@ -36,6 +36,7 @@ class RequestType:
TABLE = "table"
HEATMAP = "heatmap"
DISTRIBUTION = "distribution"
PATTERN = "pattern"
@dataclass

View File

@@ -64,6 +64,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -67,6 +67,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -66,6 +66,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -72,6 +72,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -72,6 +72,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -54,6 +54,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -78,6 +78,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -65,6 +65,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -53,6 +53,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -68,6 +68,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -99,6 +99,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -68,6 +68,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -83,6 +83,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -68,6 +68,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -53,6 +53,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -53,6 +53,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -63,6 +63,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -69,6 +69,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -61,6 +61,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -61,6 +61,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

View File

@@ -60,6 +60,5 @@
"alertStates": []
}
},
"version": "v5",
"schemaVersion": "v2alpha1"
}

Some files were not shown because too many files have changed in this diff Show More