mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-13 20:01:05 +00:00
Compare commits
5 Commits
issue_nerv
...
tvats-log-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fee8d86893 | ||
|
|
3acfb0c8c0 | ||
|
|
75994b0d6f | ||
|
|
3bfb8708e9 | ||
|
|
876d84ac53 |
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -176,7 +176,6 @@ function ChartPreview({
|
||||
},
|
||||
originalGraphType: graphType,
|
||||
},
|
||||
// alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey: [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -40,7 +40,6 @@ const make = (
|
||||
state: RuletypesAlertStateDTO.inactive,
|
||||
labels: { severity: 'info' },
|
||||
annotations: {},
|
||||
source: '',
|
||||
ruleType: RuletypesRuleTypeDTO.threshold_rule,
|
||||
schemaVersion: RuletypesRuleDTOSchemaVersion.v2alpha1,
|
||||
evaluation: {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
@@ -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{}
|
||||
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
84
pkg/querier/patterns_test.go
Normal file
84
pkg/querier/patterns_test.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
184
pkg/statementbuilder/logsstatementbuilder/patterns.go
Normal file
184
pkg/statementbuilder/logsstatementbuilder/patterns.go
Normal 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,
|
||||
})
|
||||
}
|
||||
84
pkg/statementbuilder/logsstatementbuilder/patterns_test.go
Normal file
84
pkg/statementbuilder/logsstatementbuilder/patterns_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
288
pkg/telemetryschema/logstelemetryschema/patterns.go
Normal file
288
pkg/telemetryschema/logstelemetryschema/patterns.go
Normal 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) + "'"
|
||||
}
|
||||
137
pkg/telemetryschema/logstelemetryschema/patterns_masks_test.go
Normal file
137
pkg/telemetryschema/logstelemetryschema/patterns_masks_test.go
Normal 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))
|
||||
}
|
||||
226
pkg/telemetryschema/logstelemetryschema/patterns_test.go
Normal file
226
pkg/telemetryschema/logstelemetryschema/patterns_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}},
|
||||
},
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."`
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"}}]},
|
||||
|
||||
@@ -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))
|
||||
|
||||
1
tests/fixtures/querier.py
vendored
1
tests/fixtures/querier.py
vendored
@@ -36,6 +36,7 @@ class RequestType:
|
||||
TABLE = "table"
|
||||
HEATMAP = "heatmap"
|
||||
DISTRIBUTION = "distribution"
|
||||
PATTERN = "pattern"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -64,6 +64,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -67,6 +67,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -66,6 +66,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -72,6 +72,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -72,6 +72,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -54,6 +54,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -78,6 +78,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -65,6 +65,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -53,6 +53,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -68,6 +68,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -99,6 +99,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -68,6 +68,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -83,6 +83,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -68,6 +68,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -53,6 +53,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -53,6 +53,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -63,6 +63,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -61,6 +61,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -61,6 +61,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,5 @@
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user