Compare commits

..

11 Commits

Author SHA1 Message Date
Jugal Kishore
9100d9aea6 feat(onboarding): add Pixeltable and Claude Cowork datasources (#13512)
#### Description

- Add Pixeltable and Claude Cowork cards to the onboarding data sources.
Pixeltable opens the trace-first guide. Claude Cowork opens the
log-event guide.
- Add a Pixeltable logo (`Logos/pixeltable.svg`). Pixeltable has no
vector logo, so I redrew its dot-matrix "P" on a 24x24 grid.
- Add the `python celery` search keyword to the Celery card. Searching
`python celery` returned no results before.
- Fix the built-in and AWS integration card links. The old
`/integrations?integration=...` links only opened the integrations list.
They now use `/integrations/builtin-<id>` and
`/integrations/aws?service=<id>`.

#### Issues closed by this PR

Closes SigNoz/signoz.io#4487
Closes SigNoz/signoz.io#4482
Closes SigNoz/signoz.io#3279
2026-11-12 07:45:54 +00:00
Naman Verma
3acfb0c8c0 chore: remove source and version from rules api schema (#13541)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

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

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

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

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

#### Issues closed by this PR

Closes SigNoz/pulse-pod#728

#### Additional Information

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

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

#### Issues closed by this PR

#### Screenshots / Screen Recordings

#### Additional Information

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

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

#### Issues closed by this PR

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

#### Additional Information

- The filter matches raw axios errors only. A call that goes through
`ErrorResponseHandlerV2`, `ErrorResponseHandlerForGeneratedAPIs` or
`convertToApiError` rethrows an `APIError`. Sentry still reports that
error when nobody catches it.
- The filter drops all 403 and `ERR_NETWORK` errors. A missing
permission check in the UI, or a CORS or base-path regression, no longer
shows in Sentry. This is a deliberate choice.
- The filter also drops 401 errors from the token refresh retry path.
- A separate PR adds an error state for the empty key suggestions in the
query search bar.
2026-11-11 18:32:35 +00:00
Nikhil Soni
88e9ad73e0 fix(quick-filters): reject high-cardinality fields as quick filters (#13385)
#### Description

- `PUT /api/v2/quick_filters/{source}` (and the legacy v1 update) now
returns 400 for fields whose values are unique per span/log, so a quick
filter on them has nothing to pick from: trace/span/parent-span IDs
(incl. legacy `traceID`/`spanID`/`parentSpanID`), `timestamp`, log
`body`, `gen_ai.tool.call.id`, free-text payloads
(gen_ai/OpenInference/Langfuse/Traceloop messages,
`exception.message`/`stacktrace`, `db.statement`/`db.query.text`), token
counts (`gen_ai.usage.*`, `llm.token_count.*`, `ai.usage.*`, …) and the
per-span `signoz.gen_ai.*.cost` keys. The list is hard-coded in
`quickfiltertypes`.
- `trace_id` is removed from the default traces quick filters, including
the seeds in migrations `030`/`031` (separate commit), so `147` is a
no-op on fresh installs.
- Migration `147` strips these fields from every stored quick filter;
other entries are kept byte-for-byte.

#### Issues closed by this PR

Closes #13157

#### Additional Information

- Follows the approach agreed in SigNoz/platform-pod#3282, replacing the
frontend-only blocklist from #13182.
- Changelog: `trace_id`, `span_id` and other high-cardinality fields
(token counts, costs, message payloads) are no longer supported as quick
filters and are removed from existing quick filter settings; filter by
them from the search bar instead.
- The quick filter settings panel still lists these fields as
suggestions; adding one now surfaces the API error. Hiding them is out
of scope.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 12:10:49 +00:00
Ashwin Bhatkal
7f5bd4d1cf fix(alerts): match the rule state filter to the created-by filter (#13460)
#### Description

- The alert rules State filter now uses the same select as the Created
by filter: checkbox options, one pill plus "+N", and the same background
and width. The placeholder reads "State" instead of "Filter by state".
- The Created by select moves into a shared `MultiSelectFilter` in
`components/ListFilter`, and both filters render through it. Created by
looks and behaves as before on the dashboards and alert rules lists.
- The State filter moves from `@signozhq/ui` `ComboboxSimple` to antd
`Select`, to match the antd Created by and Updated filters beside it.
`ComboboxSimple` can't run the query once on close or show checkbox
options yet (https://github.com/SigNoz/components/issues/483).
- New story on the alert rules page: the State dropdown open.

#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/720

#### Screenshots / Screen Recordings

Visual diff in dark and light:
https://claude.ai/artifact/UwA1RZA6VTDMEEbGpyVsvg

- 10 changed, all inside the State filter box on the alert rules list.
- 2 new: the State dropdown open.
- 8 pixel-identical: the dashboards list, where Created by is unchanged.
2026-11-11 10:44:29 +00:00
Nityananda Gohain
86e0e4840b feat(authz): enable FGA for llm pricing rule routes (#13260)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
- Get and delete check the path id. Bulk upsert checks each rule id and
falls back to the wildcard for creates and sync rows.
- Managed roles keep the same access as before: admin writes, editor and
viewer read. A migration backfills the tuples for existing orgs.
- Integration tests cover managed roles plus wildcard and rule-scoped
custom roles.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Closes https://github.com/SigNoz/nerve-pod/issues/463
2026-11-11 09:57:38 +00:00
Naman Verma
9ee433424d feat: migrate all rules to v2 schema (#13158)
#### Description

https://app.notion.com/p/signoz/Alert-Schema-Crosswalk-v1-v2alpha1-3cefcc6bcd198115acf3daf3fcedb409?source=copy_link

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

Closes https://github.com/SigNoz/pulse-pod/issues/225
Closes https://github.com/SigNoz/pulse-pod/issues/588
Closes https://github.com/SigNoz/pulse-pod/issues/362
Closes https://github.com/SigNoz/pulse-pod/issues/233
Closes https://github.com/SigNoz/uforge-pod/issues/5698
Closes https://github.com/SigNoz/pulse-pod/issues/696

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-11-11 07:37:41 +00:00
Swapnil Nakade
da8d66b5b9 feat(audit): emit audit logs per signoz semantic conventions (#13477)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
- using semantic conventions described in `signoz-semanatic-conventions`
repository for audit

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
contributes to https://github.com/SigNoz/keystone-pod/issues/188
2026-11-10 21:26:00 +00:00
Swapnil Nakade
f1d6c988c9 feat(audit): adding kafka audit exporter (#13357)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
- add `kafka` auditor provider that produces audit logs to a
kafka/redpanda topic as `otlp_proto`
- uses franz-go v1.21.7.
- no delivery timeout or retry cap: on franz-go < v1.22.2 these can ack
a record that was never written (twmb/franz-go#1484).
- added unit tests

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
closes https://github.com/SigNoz/keystone-pod/issues/2

<!--Please delete paragraphs that you did not use before submitting.-->
2026-11-10 13:38:39 +00:00
319 changed files with 5041 additions and 7958 deletions

4
.github/CODEOWNERS vendored
View File

@@ -194,8 +194,8 @@ go.mod @therealpandey
/frontend/src/pages/EditRules/ @SigNoz/pulse-frontend
/frontend/src/container/AlertHistory/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertRule/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertV2/ @SigNoz/pulse-frontend
/frontend/src/container/EditAlertV2/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlert/ @SigNoz/pulse-frontend
/frontend/src/container/EditAlert/ @SigNoz/pulse-frontend
/frontend/src/container/FormAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/ListAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/TriggeredAlerts/ @SigNoz/pulse-frontend

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/ee/auditor/fileauditor"
"github.com/SigNoz/signoz/ee/auditor/kafkaauditor"
"github.com/SigNoz/signoz/ee/auditor/otlphttpauditor"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
@@ -156,6 +157,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
if err := factories.Add(fileauditor.NewFactory(licensing, version.Info)); err != nil {
panic(err)
}
if err := factories.Add(kafkaauditor.NewFactory(licensing, version.Info)); err != nil {
panic(err)
}
return factories
},
func(ctx context.Context, providerSettings factory.ProviderSettings, flagger pkgflagger.Flagger, licensing licensing.Licensing, telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter, orgGetter organization.Getter, zeus zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string) {

View File

@@ -436,6 +436,7 @@ auditor:
# Specifies the auditor provider to use.
# noop: discards all audit events (community default).
# otlphttp: exports audit events via OTLP HTTP (enterprise).
# kafka: exports audit events to a Kafka topic as OTLP protobuf (enterprise).
provider: noop
# The async channel capacity for audit events. Events are dropped when full (fail-open).
buffer_size: 1000
@@ -461,6 +462,18 @@ auditor:
max_interval: 30s
# The total maximum time spent retrying.
max_elapsed_time: 60s
kafka:
# The seed broker addresses (host:port) used to discover the cluster.
brokers:
- localhost:9092
# The topic audit events are written to. It must already exist.
topic: ""
# The client id the producer reports to the brokers.
client_id: signoz
# The compression codec for produced batches: none, gzip, snappy, lz4 or zstd.
compression: lz4
# The maximum time to drain buffered events on shutdown. Undelivered events are dropped after it.
shutdown_timeout: 30s
##################### Cloud Integration #####################
cloudintegration:

View File

@@ -553,23 +553,23 @@ components:
- apiKey
type: object
AlertmanagertypesChannelJiraAuth:
oneOf:
- $ref: '#/components/schemas/AlertmanagertypesChannelJiraCloudAuth'
- $ref: '#/components/schemas/AlertmanagertypesChannelJiraSelfHostedAuth'
type: object
AlertmanagertypesChannelJiraCloudAuth:
properties:
apiToken:
description: The Atlassian API token paired with email. A secret; returned
by create, update and fetch by ID, never by list.
description: The Atlassian API token paired with email. Required when deployment
is cloud. A secret; returned by create, update and fetch by ID, never
by list.
format: password
type: string
email:
description: The Atlassian account email used for basic auth.
description: The Atlassian account email used for basic auth. Required when
deployment is cloud.
type: string
pat:
description: The Jira personal access token, sent as a Bearer header. Required
when deployment is selfhosted. A secret; returned by create, update and
fetch by ID, never by list.
format: password
type: string
required:
- email
- apiToken
type: object
AlertmanagertypesChannelJiraConfig:
properties:
@@ -642,16 +642,6 @@ components:
- issueType
- auth
type: object
AlertmanagertypesChannelJiraSelfHostedAuth:
properties:
pat:
description: The Jira personal access token, sent as a Bearer header. A
secret; returned by create, update and fetch by ID, never by list.
format: password
type: string
required:
- pat
type: object
AlertmanagertypesChannelKind:
enum:
- slack
@@ -3970,6 +3960,7 @@ components:
- logs-field
- traces-field
- metrics-field
- llm-pricing-rule
type: string
CoretypesObject:
properties:
@@ -11745,20 +11736,10 @@ components:
description: Whether the rule is paused. A disabled rule is neither evaluated
nor notifies. Defaults to false.
type: boolean
evalWindow:
description: The lookback window as a Go duration such as 5m. Only read
when schemaVersion is v1, defaulting to 5m; v2alpha1 rules set it in evaluation.spec
instead.
type: string
evaluation:
allOf:
- $ref: '#/components/schemas/RuletypesEvaluationEnvelope'
description: The evaluation window, as a kind and spec.
frequency:
description: How often the rule is evaluated, as a Go duration such as 1m.
Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set
it in evaluation.spec instead.
type: string
labels:
additionalProperties:
type: string
@@ -11770,13 +11751,6 @@ components:
allOf:
- $ref: '#/components/schemas/RuletypesNotificationSettings'
description: Grouping, re-notification and routing behaviour.
preferredChannels:
description: The displayName of each notification channel to notify, for
schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries
its own channels.
items:
type: string
type: array
ruleType:
allOf:
- $ref: '#/components/schemas/RuletypesRuleType'
@@ -11789,15 +11763,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
@@ -11886,20 +11851,10 @@ components:
description: Whether the rule is paused. A disabled rule is neither evaluated
nor notifies. Defaults to false.
type: boolean
evalWindow:
description: The lookback window as a Go duration such as 5m. Only read
when schemaVersion is v1, defaulting to 5m; v2alpha1 rules set it in evaluation.spec
instead.
type: string
evaluation:
allOf:
- $ref: '#/components/schemas/RuletypesEvaluationEnvelope'
description: The evaluation window, as a kind and spec.
frequency:
description: How often the rule is evaluated, as a Go duration such as 1m.
Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set
it in evaluation.spec instead.
type: string
id:
description: The unique identifier of the rule.
type: string
@@ -11914,13 +11869,6 @@ components:
allOf:
- $ref: '#/components/schemas/RuletypesNotificationSettings'
description: Grouping, re-notification and routing behaviour.
preferredChannels:
description: The displayName of each notification channel to notify, for
schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries
its own channels.
items:
type: string
type: array
ruleType:
allOf:
- $ref: '#/components/schemas/RuletypesRuleType'
@@ -11933,11 +11881,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'
@@ -11948,10 +11891,6 @@ components:
type: string
updatedBy:
type: string
version:
deprecated: true
description: The query payload version. Nothing reads it.
type: string
required:
- id
- state
@@ -11974,28 +11913,10 @@ components:
description: Whether to fire a nodata alert when the query returns no data.
Defaults to false.
type: boolean
algorithm:
description: 'anomaly_rule only: the anomaly detection algorithm. The supported
value is standard.'
type: string
compositeQuery:
allOf:
- $ref: '#/components/schemas/RuletypesAlertCompositeQuery'
description: The queries whose result is compared against the thresholds.
matchType:
allOf:
- $ref: '#/components/schemas/RuletypesMatchType'
description: 'schemaVersion v1 only: which points in the window must satisfy
op. Accepted as at_least_once, all_the_times, on_average, in_total or
last, their aliases avg and sum, or the legacy codes as strings, ''1''
to ''5''. Required for v1; v2alpha1 sets matchType on each threshold.'
op:
allOf:
- $ref: '#/components/schemas/RuletypesCompareOperator'
description: 'schemaVersion v1 only: how the value is compared with target.
Accepted as above, below, equal, not_equal, above_or_equal, below_or_equal
or outside_bounds, their symbols, or the legacy codes as strings, ''1''
to ''7''. Required for v1; v2alpha1 sets op on each threshold.'
requireMinPoints:
description: Whether a series is skipped until it has at least requiredNumPoints
points in the window. Defaults to false.
@@ -12013,17 +11934,6 @@ components:
description: The name of the query whose result is evaluated, such as A
or F1. Must match a query in compositeQuery.
type: string
target:
description: 'schemaVersion v1 only: the value compared against, in targetUnit.
Required for v1; v2alpha1 sets target on each threshold.'
format: double
nullable: true
type: number
targetUnit:
description: 'schemaVersion v1 only: the unit of target, such as percent
or ms. Converted to compositeQuery.unit before comparison when the two
differ.'
type: string
thresholds:
allOf:
- $ref: '#/components/schemas/RuletypesRuleThresholdData'
@@ -16150,9 +16060,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- llm-pricing-rule:list
- tokenizer:
- VIEWER
- llm-pricing-rule:list
summary: List pricing rules
tags:
- llmpricingrules
@@ -16197,9 +16107,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- llm-pricing-rule:update
- tokenizer:
- ADMIN
- llm-pricing-rule:update
summary: Create or update pricing rules
tags:
- llmpricingrules
@@ -16245,9 +16155,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- llm-pricing-rule:delete
- tokenizer:
- ADMIN
- llm-pricing-rule:delete
summary: Delete a pricing rule
tags:
- llmpricingrules
@@ -16303,9 +16213,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- llm-pricing-rule:read
- tokenizer:
- VIEWER
- llm-pricing-rule:read
summary: Get a pricing rule
tags:
- llmpricingrules
@@ -16358,9 +16268,11 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- llm-pricing-rule:list
- traces:read
- tokenizer:
- VIEWER
- llm-pricing-rule:list
- traces:read
summary: List unmapped models
tags:
- llmpricingrules
@@ -25530,56 +25442,6 @@ paths:
- rules
x-signoz-stability: alpha
/api/v2/rules:
get:
deprecated: true
description: 'This endpoint lists all alert rules with their current evaluation
state. Deprecated: use ListRulesV3, which supports filtering, sorting and
pagination.'
operationId: ListRules
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/RuletypesRule'
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List alert rules
tags:
- rules
x-signoz-stability: alpha
post:
deprecated: false
description: This endpoint creates a new alert rule
@@ -25669,7 +25531,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
@@ -25736,7 +25597,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
@@ -25809,7 +25669,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
@@ -25865,7 +25724,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.
@@ -25957,7 +25815,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`.
@@ -26026,7 +25883,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`
@@ -26125,7 +25981,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
@@ -26225,7 +26080,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
@@ -26315,7 +26169,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
@@ -26384,7 +26237,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
schema:
$ref: '#/components/schemas/RuletypesPostableRule'
responses:
@@ -26635,7 +26487,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
@@ -26702,7 +26553,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
@@ -26775,7 +26625,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
@@ -26831,7 +26680,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.
@@ -26923,7 +26771,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`.
@@ -26992,7 +26839,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`
@@ -27091,7 +26937,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
@@ -27191,7 +27036,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
@@ -27281,7 +27125,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
@@ -27350,7 +27193,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
schema:
$ref: '#/components/schemas/RuletypesPostableRule'
responses:
@@ -27502,7 +27344,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
@@ -27569,7 +27410,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
@@ -27642,7 +27482,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
@@ -27698,7 +27537,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.
@@ -27790,7 +27628,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`.
@@ -27859,7 +27696,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`
@@ -27958,7 +27794,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
@@ -28058,7 +27893,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
@@ -28148,7 +27982,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
@@ -28217,7 +28050,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
schema:
$ref: '#/components/schemas/RuletypesPostableRule'
responses:
@@ -29131,7 +28963,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
@@ -29198,7 +29029,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
@@ -29271,7 +29101,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
@@ -29327,7 +29156,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.
@@ -29419,7 +29247,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`.
@@ -29488,7 +29315,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`
@@ -29587,7 +29413,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
@@ -29687,7 +29512,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
@@ -29777,7 +29601,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
@@ -29846,7 +29669,6 @@ paths:
interval: 30m
ruleType: threshold_rule
schemaVersion: v2alpha1
version: v5
schema:
$ref: '#/components/schemas/RuletypesPostableRule'
responses:

View File

@@ -0,0 +1,31 @@
package kafkaauditor
import (
"context"
"log/slog"
"github.com/twmb/franz-go/pkg/kgo"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/audittypes"
)
// Produces the whole batch as a single record so the collector's otlp_proto unmarshaler reads it as one ExportLogsServiceRequest.
func (provider *provider) export(ctx context.Context, events []audittypes.AuditEvent) error {
logs := audittypes.NewPLogsFromAuditEvents(events, "signoz", provider.build.Version(), "signoz.audit")
value, err := provider.marshaler.MarshalLogs(logs)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "failed to marshal audit logs")
}
record := &kgo.Record{Topic: provider.config.Kafka.Topic, Value: value}
if err := provider.client.ProduceSync(ctx, record).FirstErr(); err != nil {
provider.settings.Logger().ErrorContext(ctx, "audit export failed", errors.Attr(err), slog.Int("dropped_log_records", len(events)))
return errors.Wrapf(err, errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "failed to produce audit logs to topic %q", provider.config.Kafka.Topic)
}
return nil
}

View File

@@ -0,0 +1,132 @@
package kafkaauditor
import (
"context"
"log/slog"
"github.com/twmb/franz-go/pkg/kgo"
"github.com/twmb/franz-go/plugin/kslog"
"go.opentelemetry.io/collector/pdata/plog"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/auditorserver"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/version"
)
var _ auditor.Auditor = (*provider)(nil)
type provider struct {
settings factory.ScopedProviderSettings
config auditor.Config
licensing licensing.Licensing
build version.Build
server *auditorserver.Server
marshaler plog.ProtoMarshaler
client *kgo.Client
}
func NewFactory(licensing licensing.Licensing, build version.Build) factory.ProviderFactory[auditor.Auditor, auditor.Config] {
return factory.NewProviderFactory(factory.MustNewName("kafka"), func(ctx context.Context, providerSettings factory.ProviderSettings, config auditor.Config) (auditor.Auditor, error) {
return newProvider(ctx, providerSettings, config, licensing, build)
})
}
func newProvider(_ context.Context, providerSettings factory.ProviderSettings, config auditor.Config, licensing licensing.Licensing, build version.Build) (auditor.Auditor, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/auditor/kafkaauditor")
// Acks from all in-sync replicas, idempotent writes and unbounded record retries are franz-go defaults and are kept as is.
// A delivery timeout or retry limit lets franz-go < v1.22.2 ack a record that was never written (twmb/franz-go#1484).
client, err := kgo.NewClient(
kgo.SeedBrokers(config.Kafka.Brokers...),
kgo.ClientID(config.Kafka.ClientID),
kgo.ProducerBatchCompression(compressionCodec(config.Kafka.Compression)),
kgo.ProducerLinger(0),
kgo.WithLogger(kslog.New(settings.Logger())),
)
if err != nil {
return nil, err
}
provider := &provider{
settings: settings,
config: config,
licensing: licensing,
build: build,
marshaler: plog.ProtoMarshaler{},
client: client,
}
server, err := auditorserver.New(settings,
auditorserver.Config{
BufferSize: config.BufferSize,
BatchSize: config.BatchSize,
FlushInterval: config.FlushInterval,
},
provider.export,
)
if err != nil {
client.Close()
return nil, err
}
provider.server = server
return provider, nil
}
func (provider *provider) Start(ctx context.Context) error {
return provider.server.Start(ctx)
}
func (provider *provider) Audit(ctx context.Context, event audittypes.AuditEvent) {
if event.PrincipalAttributes.PrincipalOrgID.IsZero() {
provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero")
return
}
if _, err := provider.licensing.GetActive(ctx, event.PrincipalAttributes.PrincipalOrgID); err != nil {
return
}
provider.server.Add(ctx, event)
}
func (provider *provider) Healthy() <-chan struct{} {
return provider.server.Healthy()
}
func (provider *provider) Stop(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, provider.config.Kafka.ShutdownTimeout)
defer cancel()
stoppedC := make(chan error, 1)
go func() { stoppedC <- provider.server.Stop(ctx) }()
select {
case err := <-stoppedC:
provider.client.Close()
return err
case <-ctx.Done():
provider.settings.Logger().WarnContext(ctx, "audit drain timed out, closing kafka client", slog.Duration("shutdown_timeout", provider.config.Kafka.ShutdownTimeout))
// Closing fails buffered and in-flight records, so the blocked export and the rest of the drain return immediately.
provider.client.Close()
return <-stoppedC
}
}
func compressionCodec(name string) kgo.CompressionCodec {
switch name {
case "gzip":
return kgo.GzipCompression()
case "snappy":
return kgo.SnappyCompression()
case "lz4":
return kgo.Lz4Compression()
case "zstd":
return kgo.ZstdCompression()
default:
return kgo.NoCompression()
}
}

View File

@@ -0,0 +1,227 @@
package kafkaauditor
import (
"context"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/twmb/franz-go/pkg/kerr"
"github.com/twmb/franz-go/pkg/kfake"
"github.com/twmb/franz-go/pkg/kgo"
"go.opentelemetry.io/collector/pdata/plog"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/version"
)
func newTestCluster(t *testing.T, topics ...string) *kfake.Cluster {
t.Helper()
cluster, err := kfake.NewCluster(kfake.NumBrokers(1), kfake.SeedTopics(1, topics...))
require.NoError(t, err)
t.Cleanup(cluster.Close)
return cluster
}
func newTestProvider(t *testing.T, brokers []string, topic string, compression string) *provider {
t.Helper()
config := auditor.Config{
Provider: "kafka",
BufferSize: 10,
BatchSize: 5,
FlushInterval: 50 * time.Millisecond,
Kafka: auditor.KafkaConfig{
Brokers: brokers,
Topic: topic,
ClientID: "signoz-test",
Compression: compression,
ShutdownTimeout: 30 * time.Second,
},
}
auditor, err := newProvider(context.Background(), instrumentationtest.New().ToProviderSettings(), config, nil, version.Info)
require.NoError(t, err)
return auditor.(*provider)
}
func newTestEvent(resource coretypes.Resource, action coretypes.Verb, body string) audittypes.AuditEvent {
return audittypes.AuditEvent{
Timestamp: time.Now(),
Body: body,
EventName: audittypes.NewEventName(action),
AuditAttributes: audittypes.AuditAttributes{
Action: action,
Outcome: audittypes.OutcomeSuccess,
},
ResourceAttributes: audittypes.ResourceAttributes{
Resource: resource,
},
}
}
// Polls the topic until the consumed records hold wantLogRecords audit log records in total.
func consumeRecords(t *testing.T, brokers []string, topic string, wantLogRecords int) []*kgo.Record {
t.Helper()
client, err := kgo.NewClient(
kgo.SeedBrokers(brokers...),
kgo.ConsumeTopics(topic),
kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),
)
require.NoError(t, err)
defer client.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var records []*kgo.Record
gotLogRecords := 0
for gotLogRecords < wantLogRecords {
fetches := client.PollFetches(ctx)
require.NoError(t, ctx.Err(), "timed out waiting for %d log records, got %d", wantLogRecords, gotLogRecords)
require.Empty(t, fetches.Errors())
for _, record := range fetches.Records() {
logs, err := (&plog.ProtoUnmarshaler{}).UnmarshalLogs(record.Value)
require.NoError(t, err)
gotLogRecords += logs.LogRecordCount()
records = append(records, record)
}
}
return records
}
func TestExport(t *testing.T) {
testCases := []struct {
name string
compression string
compressionType uint8
}{
{name: "CompressionNone", compression: "none", compressionType: 0},
{name: "CompressionGzip", compression: "gzip", compressionType: 1},
{name: "CompressionSnappy", compression: "snappy", compressionType: 2},
{name: "CompressionLz4", compression: "lz4", compressionType: 3},
{name: "CompressionZstd", compression: "zstd", compressionType: 4},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
cluster := newTestCluster(t, "tenant_audit")
provider := newTestProvider(t, cluster.ListenAddrs(), "tenant_audit", testCase.compression)
defer provider.client.Close()
events := []audittypes.AuditEvent{
newTestEvent(coretypes.ResourceUser, coretypes.VerbUpdate, "user updated"),
newTestEvent(coretypes.ResourceMetaResourceDashboard, coretypes.VerbCreate, "dashboard created"),
newTestEvent(coretypes.ResourceMetaResourceDashboard, coretypes.VerbDelete, "dashboard deleted"),
}
require.NoError(t, provider.export(context.Background(), events))
records := consumeRecords(t, cluster.ListenAddrs(), "tenant_audit", len(events))
require.Len(t, records, 1)
assert.Equal(t, "tenant_audit", records[0].Topic)
assert.Equal(t, testCase.compressionType, records[0].Attrs.CompressionType())
logs, err := (&plog.ProtoUnmarshaler{}).UnmarshalLogs(records[0].Value)
require.NoError(t, err)
assert.Equal(t, len(events), logs.LogRecordCount())
serviceName, ok := logs.ResourceLogs().At(0).Resource().Attributes().Get("service.name")
require.True(t, ok)
assert.Equal(t, "signoz", serviceName.Str())
})
}
}
func TestExport_RecordTooLarge(t *testing.T) {
cluster := newTestCluster(t, "large_audit")
provider := newTestProvider(t, cluster.ListenAddrs(), "large_audit", "none")
defer provider.client.Close()
event := newTestEvent(coretypes.ResourceUser, coretypes.VerbCreate, strings.Repeat("a", 2<<20))
err := provider.export(context.Background(), []audittypes.AuditEvent{event})
require.Error(t, err)
assert.True(t, errors.Is(err, kerr.MessageTooLarge))
}
func TestExport_UnknownTopic(t *testing.T) {
cluster := newTestCluster(t)
provider := newTestProvider(t, cluster.ListenAddrs(), "missing_audit", "lz4")
defer provider.client.Close()
err := provider.export(context.Background(), []audittypes.AuditEvent{newTestEvent(coretypes.ResourceUser, coretypes.VerbCreate, "user created")})
require.Error(t, err)
assert.True(t, errors.Is(err, kerr.UnknownTopicOrPartition))
}
func TestExport_BrokerDown_RetriesUntilContextDone(t *testing.T) {
cluster := newTestCluster(t, "down_audit")
brokers := cluster.ListenAddrs()
cluster.Close()
provider := newTestProvider(t, brokers, "down_audit", "lz4")
defer provider.client.Close()
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
start := time.Now()
err := provider.export(ctx, []audittypes.AuditEvent{newTestEvent(coretypes.ResourceUser, coretypes.VerbCreate, "user created")})
require.Error(t, err)
assert.True(t, errors.Is(err, context.DeadlineExceeded))
assert.GreaterOrEqual(t, time.Since(start), 500*time.Millisecond)
}
func TestStop_DrainsQueuedEvents(t *testing.T) {
cluster := newTestCluster(t, "drain_audit")
provider := newTestProvider(t, cluster.ListenAddrs(), "drain_audit", "lz4")
done := make(chan error, 1)
go func() { done <- provider.Start(context.Background()) }()
<-provider.Healthy()
for range 7 {
provider.server.Add(context.Background(), newTestEvent(coretypes.ResourceUser, coretypes.VerbUpdate, "user updated"))
}
require.NoError(t, provider.Stop(context.Background()))
require.NoError(t, <-done)
records := consumeRecords(t, cluster.ListenAddrs(), "drain_audit", 7)
assert.NotEmpty(t, records)
}
func TestStop_BrokerDown_ClosesAfterShutdownTimeout(t *testing.T) {
cluster := newTestCluster(t, "stuck_audit")
brokers := cluster.ListenAddrs()
cluster.Close()
provider := newTestProvider(t, brokers, "stuck_audit", "lz4")
provider.config.Kafka.ShutdownTimeout = 500 * time.Millisecond
done := make(chan error, 1)
go func() { done <- provider.Start(context.Background()) }()
<-provider.Healthy()
for range 7 {
provider.server.Add(context.Background(), newTestEvent(coretypes.ResourceUser, coretypes.VerbUpdate, "user updated"))
}
start := time.Now()
require.NoError(t, provider.Stop(context.Background()))
require.NoError(t, <-done)
assert.GreaterOrEqual(t, time.Since(start), 500*time.Millisecond)
assert.Less(t, time.Since(start), 5*time.Second)
}

View File

@@ -34,8 +34,7 @@ type AnomalyRule struct {
provider anomaly.Provider
version string
logger *slog.Logger
logger *slog.Logger
seasonality anomaly.Seasonality
}
@@ -63,7 +62,6 @@ func NewAnomalyRule(
r := AnomalyRule{
BaseRule: baseRule,
querier: querier,
version: p.Version,
logger: logger.With(slog.String("rule.id", id)),
}
@@ -105,15 +103,15 @@ func (r *AnomalyRule) Type() ruletypes.RuleType {
}
func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) *qbtypes.QueryRangeRequest {
r.logger.InfoContext(
ctx, "prepare query range request", slog.Int64("ts", ts.UnixMilli()),
slog.Int64("eval.window_ms", r.EvalWindow().Milliseconds()),
slog.Int64("eval.delay_ms", r.EvalDelay().Milliseconds()),
)
startTs, endTs := r.Timestamps(ts)
start, end := startTs.UnixMilli(), endTs.UnixMilli()
r.logger.InfoContext(
ctx, "prepare query range request", slog.Int64("ts", ts.UnixMilli()),
slog.Int64("eval.window_ms", end-start),
slog.Int64("eval.delay_ms", r.EvalDelay().Milliseconds()),
)
req := &qbtypes.QueryRangeRequest{
Start: uint64(start),
End: uint64(end),
@@ -420,12 +418,10 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
func (r *AnomalyRule) String() string {
ar := ruletypes.PostableRule{
AlertName: valuer.NewDisplayName(r.Name()),
RuleCondition: r.Condition(),
EvalWindow: r.EvalWindow(),
Labels: r.Labels().Map(),
Annotations: r.Annotations().Map(),
PreferredChannels: r.PreferredChannels(),
AlertName: valuer.NewDisplayName(r.Name()),
RuleCondition: r.Condition(),
Labels: r.Labels().Map(),
Annotations: r.Annotations().Map(),
}
byt, err := json.Marshal(ar)

View File

@@ -54,9 +54,6 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
Frequency: valuer.MustParseTextDuration("1m"),
}},
RuleCondition: &ruletypes.RuleCondition{
CompareOperator: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
Target: &target,
CompositeQuery: &ruletypes.AlertCompositeQuery{
QueryType: ruletypes.QueryTypeBuilder,
Queries: []qbtypes.QueryEnvelope{{
@@ -157,10 +154,7 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
Frequency: valuer.MustParseTextDuration("1m"),
}},
RuleCondition: &ruletypes.RuleCondition{
CompareOperator: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
AlertOnAbsent: true,
Target: &target,
AlertOnAbsent: true,
CompositeQuery: &ruletypes.AlertCompositeQuery{
QueryType: ruletypes.QueryTypeBuilder,
Queries: []qbtypes.QueryEnvelope{{

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,6 @@ import type {
GetRuleHistoryTopContributorsParams,
GetRuleHistoryTopContributorsPathParameters,
ListRuleViews200,
ListRules200,
ListRulesV3200,
ListRulesV3Params,
PatchRuleByID200,
@@ -427,85 +426,6 @@ export const useUpdateRuleView = <
> => {
return useMutation(getUpdateRuleViewMutationOptions(options));
};
/**
* This endpoint lists all alert rules with their current evaluation state. Deprecated: use ListRulesV3, which supports filtering, sorting and pagination.
* @deprecated
* @summary List alert rules
*/
export const listRules = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListRules200>({
url: `/api/v2/rules`,
method: 'GET',
signal,
});
};
export const getListRulesQueryKey = () => {
return [`/api/v2/rules`] as const;
};
export const getListRulesQueryOptions = <
TData = Awaited<ReturnType<typeof listRules>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof listRules>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListRulesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRules>>> = ({
signal,
}) => listRules(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listRules>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListRulesQueryResult = NonNullable<
Awaited<ReturnType<typeof listRules>>
>;
export type ListRulesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary List alert rules
*/
export function useListRules<
TData = Awaited<ReturnType<typeof listRules>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof listRules>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListRulesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @deprecated
* @summary List alert rules
*/
export const invalidateListRules = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListRulesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a new alert rule
* @summary Create alert rule

View File

@@ -829,33 +829,26 @@ export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTy
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfigDTOKind {
jira = 'jira',
}
export interface AlertmanagertypesChannelJiraCloudAuthDTO {
export interface AlertmanagertypesChannelJiraAuthDTO {
/**
* @type string
* @format password
* @description The Atlassian API token paired with email. A secret; returned by create, update and fetch by ID, never by list.
* @description The Atlassian API token paired with email. Required when deployment is cloud. A secret; returned by create, update and fetch by ID, never by list.
*/
apiToken: string;
apiToken?: string;
/**
* @type string
* @description The Atlassian account email used for basic auth.
* @description The Atlassian account email used for basic auth. Required when deployment is cloud.
*/
email: string;
}
export interface AlertmanagertypesChannelJiraSelfHostedAuthDTO {
email?: string;
/**
* @type string
* @format password
* @description The Jira personal access token, sent as a Bearer header. A secret; returned by create, update and fetch by ID, never by list.
* @description The Jira personal access token, sent as a Bearer header. Required when deployment is selfhosted. A secret; returned by create, update and fetch by ID, never by list.
*/
pat: string;
pat?: string;
}
export type AlertmanagertypesChannelJiraAuthDTO =
| AlertmanagertypesChannelJiraCloudAuthDTO
| AlertmanagertypesChannelJiraSelfHostedAuthDTO;
export enum AlertmanagertypesJiraDeploymentDTO {
cloud = 'cloud',
selfhosted = 'selfhosted',
@@ -3611,6 +3604,7 @@ export enum CoretypesKindDTO {
'logs-field' = 'logs-field',
'traces-field' = 'traces-field',
'metrics-field' = 'metrics-field',
'llm-pricing-rule' = 'llm-pricing-rule',
}
export enum CoretypesTypeDTO {
user = 'user',
@@ -12966,23 +12960,10 @@ export interface RuletypesRuleConditionDTO {
* @description Whether to fire a nodata alert when the query returns no data. Defaults to false.
*/
alertOnAbsent?: boolean;
/**
* @type string
* @description anomaly_rule only: the anomaly detection algorithm. The supported value is standard.
*/
algorithm?: string;
/**
* @description The queries whose result is compared against the thresholds.
*/
compositeQuery: RuletypesAlertCompositeQueryDTO;
/**
* @description schemaVersion v1 only: which points in the window must satisfy op. Accepted as at_least_once, all_the_times, on_average, in_total or last, their aliases avg and sum, or the legacy codes as strings, '1' to '5'. Required for v1; v2alpha1 sets matchType on each threshold.
*/
matchType?: RuletypesMatchTypeDTO;
/**
* @description schemaVersion v1 only: how the value is compared with target. Accepted as above, below, equal, not_equal, above_or_equal, below_or_equal or outside_bounds, their symbols, or the legacy codes as strings, '1' to '7'. Required for v1; v2alpha1 sets op on each threshold.
*/
op?: RuletypesCompareOperatorDTO;
/**
* @type boolean
* @description Whether a series is skipped until it has at least requiredNumPoints points in the window. Defaults to false.
@@ -13002,17 +12983,6 @@ export interface RuletypesRuleConditionDTO {
* @description The name of the query whose result is evaluated, such as A or F1. Must match a query in compositeQuery.
*/
selectedQueryName: string;
/**
* @type number,null
* @format double
* @description schemaVersion v1 only: the value compared against, in targetUnit. Required for v1; v2alpha1 sets target on each threshold.
*/
target?: number | null;
/**
* @type string
* @description schemaVersion v1 only: the unit of target, such as percent or ms. Converted to compositeQuery.unit before comparison when the two differ.
*/
targetUnit?: string;
/**
* @description The thresholds to evaluate, as a kind and spec.
*/
@@ -13048,20 +13018,10 @@ export interface RuletypesPostableRuleDTO {
* @description Whether the rule is paused. A disabled rule is neither evaluated nor notifies. Defaults to false.
*/
disabled?: boolean;
/**
* @type string
* @description The lookback window as a Go duration such as 5m. Only read when schemaVersion is v1, defaulting to 5m; v2alpha1 rules set it in evaluation.spec instead.
*/
evalWindow?: string;
/**
* @description The evaluation window, as a kind and spec.
*/
evaluation: RuletypesEvaluationEnvelopeDTO;
/**
* @type string
* @description How often the rule is evaluated, as a Go duration such as 1m. Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set it in evaluation.spec instead.
*/
frequency?: string;
/**
* @type object
* @description Labels attached to every alert the rule fires. Keys match [a-zA-Z_.][a-zA-Z0-9_.]*; values may use Go templates such as {{$labels.service_name}}. severity is the conventional routing label.
@@ -13071,11 +13031,6 @@ export interface RuletypesPostableRuleDTO {
* @description Grouping, re-notification and routing behaviour.
*/
notificationSettings: RuletypesNotificationSettingsDTO;
/**
* @type array
* @description The displayName of each notification channel to notify, for schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries its own channels.
*/
preferredChannels?: string[];
/**
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql queries and is forced to promql_rule for promql queries.
*/
@@ -13086,16 +13041,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 {
@@ -13160,20 +13105,10 @@ export interface RuletypesRuleDTO {
* @description Whether the rule is paused. A disabled rule is neither evaluated nor notifies. Defaults to false.
*/
disabled?: boolean;
/**
* @type string
* @description The lookback window as a Go duration such as 5m. Only read when schemaVersion is v1, defaulting to 5m; v2alpha1 rules set it in evaluation.spec instead.
*/
evalWindow?: string;
/**
* @description The evaluation window, as a kind and spec.
*/
evaluation: RuletypesEvaluationEnvelopeDTO;
/**
* @type string
* @description How often the rule is evaluated, as a Go duration such as 1m. Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set it in evaluation.spec instead.
*/
frequency?: string;
/**
* @type string
* @description The unique identifier of the rule.
@@ -13188,11 +13123,6 @@ export interface RuletypesRuleDTO {
* @description Grouping, re-notification and routing behaviour.
*/
notificationSettings: RuletypesNotificationSettingsDTO;
/**
* @type array
* @description The displayName of each notification channel to notify, for schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries its own channels.
*/
preferredChannels?: string[];
/**
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql queries and is forced to promql_rule for promql queries.
*/
@@ -13203,11 +13133,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.
*/
@@ -13221,11 +13146,6 @@ export interface RuletypesRuleDTO {
* @type string
*/
updatedBy?: string;
/**
* @type string
* @description The query payload version. Nothing reads it.
*/
version?: string;
}
export enum RuletypesThresholdKindDTO {
@@ -16783,17 +16703,6 @@ export type UpdateRuleView200 = {
status: string;
};
export type ListRules200 = {
/**
* @type array
*/
data: RuletypesRuleDTO[];
/**
* @type string
*/
status: string;
};
export type CreateRule201 = {
data: RuletypesRuleDTO;
/**

View File

@@ -11,8 +11,6 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios';
// generated API Instance
const generatedAPIAxiosInstance = axios.create({
baseURL: ENVIRONMENT.baseURL,
// The backend binds repeated keys (`states=a&states=b`); axios' default sends `states[]=`.
paramsSerializer: { indexes: null },
});
let generatedAPIQueryKeyHeaderContext: Record<string, unknown> | undefined;

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect x="1.25" y="1.25" width="21.5" height="21.5" rx="3.5" fill="none" stroke="#7A8291" stroke-width="1.5"/><g fill="#7A8291"><circle cx="7" cy="4.5" r="1.05"/><circle cx="9.5" cy="4.5" r="1.05"/><circle cx="12" cy="4.5" r="1.05"/><circle cx="14.5" cy="4.5" r="1.05"/><circle cx="7" cy="7" r="1.05"/><circle cx="17" cy="7" r="1.05"/><circle cx="7" cy="9.5" r="1.05"/><circle cx="17" cy="9.5" r="1.05"/><circle cx="7" cy="12" r="1.05"/><circle cx="9.5" cy="12" r="1.05"/><circle cx="12" cy="12" r="1.05"/><circle cx="14.5" cy="12" r="1.05"/><circle cx="7" cy="14.5" r="1.05"/><circle cx="7" cy="17" r="1.05"/><circle cx="7" cy="19.5" r="1.05"/></g></svg>

After

Width:  |  Height:  |  Size: 726 B

View File

@@ -1,18 +1,13 @@
import { ConfigProvider, Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Select } from 'antd';
import cx from 'classnames';
import type { UpdatedWindow } from '../dsl/types';
import type { CreatorOption } from '../types';
import { UPDATED_OPTIONS } from './constants';
import MultiSelectFilter from './MultiSelectFilter';
import styles from './FilterZone.module.scss';
// The option checkbox already marks selection, so selected rows stay regular weight.
const CREATOR_SELECT_THEME = {
components: { Select: { optionSelectedFontWeight: 400 } },
};
interface Props {
createdBy: string[];
updated: UpdatedWindow;
@@ -52,36 +47,16 @@ function FilterChips({
return (
<div className={styles.chips}>
<ConfigProvider theme={CREATOR_SELECT_THEME}>
<Select
mode="multiple"
showSearch
allowClear
className={cx(styles.select, styles.selectWide)}
placeholder="Created by"
value={createdBy}
options={creatorOptionsData}
optionFilterProp="label"
maxTagCount={1}
menuItemSelectedIcon={null}
data-testid={`${testIdPrefix}-filter-created-by`}
disabled={disabled}
onClear={onClearCreatedBy}
optionRender={(option): JSX.Element => (
<div className={styles.creatorOption}>
<div className={styles.creatorCheck}>
<Checkbox
color="primary"
value={createdBy.includes(option.value as string)}
/>
</div>
<span className={styles.creatorLabel}>{option.label}</span>
</div>
)}
onChange={(value): void => onCreatedByChange(value as string[])}
onDropdownVisibleChange={runOnClose}
/>
</ConfigProvider>
<MultiSelectFilter
value={createdBy}
options={creatorOptionsData}
placeholder="Created by"
testId={`${testIdPrefix}-filter-created-by`}
disabled={disabled}
onClear={onClearCreatedBy}
onChange={onCreatedByChange}
onDropdownVisibleChange={runOnClose}
/>
<Select
showSearch
className={cx(styles.select, styles.selectNarrow)}

View File

@@ -86,21 +86,21 @@
}
}
// Created-by option: a presentational checkbox + label (antd handles the toggle).
.creatorOption {
// Multi-select option: a presentational checkbox + label (antd handles the toggle).
.checkOption {
display: flex;
align-items: center;
gap: 8px;
}
.creatorCheck {
.checkOptionBox {
// Flex, not block: a line box would shift with the check mark's baseline.
display: flex;
flex-shrink: 0;
pointer-events: none;
}
.creatorLabel {
.checkOptionLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -0,0 +1,72 @@
import { ConfigProvider, Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import cx from 'classnames';
import styles from './FilterZone.module.scss';
// The option checkbox already marks selection, so selected rows stay regular weight.
const CHECKBOX_SELECT_THEME = {
components: { Select: { optionSelectedFontWeight: 400 } },
};
export interface MultiSelectFilterOption {
value: string;
label: string;
}
interface Props {
value: string[];
options: MultiSelectFilterOption[];
placeholder: string;
testId: string;
onChange: (values: string[]) => void;
onClear?: () => void;
onDropdownVisibleChange?: (open: boolean) => void;
disabled?: boolean;
}
function MultiSelectFilter({
value,
options,
placeholder,
testId,
onChange,
onClear,
onDropdownVisibleChange,
disabled = false,
}: Props): JSX.Element {
return (
<ConfigProvider theme={CHECKBOX_SELECT_THEME}>
<Select
mode="multiple"
showSearch
allowClear
className={cx(styles.select, styles.selectWide)}
placeholder={placeholder}
value={value}
options={options}
optionFilterProp="label"
maxTagCount={1}
menuItemSelectedIcon={null}
data-testid={testId}
disabled={disabled}
onClear={onClear}
optionRender={(option): JSX.Element => (
<div className={styles.checkOption}>
<div className={styles.checkOptionBox}>
<Checkbox
color="primary"
value={value.includes(option.value as string)}
/>
</div>
<span className={styles.checkOptionLabel}>{option.label}</span>
</div>
)}
onChange={(next): void => onChange(next as string[])}
onDropdownVisibleChange={onDropdownVisibleChange}
/>
</ConfigProvider>
);
}
export default MultiSelectFilter;

View File

@@ -53,7 +53,6 @@ export enum QueryParams {
variables = 'variables',
version = 'version',
source = 'source',
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
isTestAlert = 'isTestAlert',
yAxisUnit = 'yAxisUnit',
ruleName = 'ruleName',

View File

@@ -26,7 +26,6 @@ export const REACT_QUERY_KEY = {
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
CLONE_ALERT_RULE: 'CLONE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
GET_TRACE_V4_WATERFALL: 'GET_TRACE_V4_WATERFALL',
GET_TRACE_AGGREGATIONS: 'GET_TRACE_AGGREGATIONS',

View File

@@ -111,8 +111,8 @@ jest.mock('constants/formats/alertFormatCategories', () => ({
]),
}));
jest.mock('container/CreateAlertV2/utils', () => ({
...jest.requireActual('container/CreateAlertV2/utils'),
jest.mock('container/CreateAlert/utils', () => ({
...jest.requireActual('container/CreateAlert/utils'),
}));
const TEST_STRINGS = {

View File

@@ -1,4 +1,4 @@
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
import { createMockAlertContextState } from 'container/CreateAlert/EvaluationSettings/__tests__/testUtils';
import { render, screen, userEvent, within } from 'tests/test-utils';
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';

View File

@@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react';
import type { DefaultOptionType } from 'antd/es/select';
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
import { createMockAlertContextState } from 'container/CreateAlert/EvaluationSettings/__tests__/testUtils';
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
import * as appHooks from 'providers/App/App';
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';

View File

@@ -9,7 +9,7 @@ import ROUTES from 'constants/routes';
import {
AlertThresholdMatchType,
AlertThresholdOperator,
} from 'container/CreateAlertV2/context/types';
} from 'container/CreateAlert/context/types';
import { getSelectedQueryOptions } from 'container/FormAlertRules/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { ArrowRight } from '@signozhq/icons';

View File

@@ -9,12 +9,12 @@ import CreateAlertHeader from './CreateAlertHeader';
import Footer from './Footer';
import NotificationSettings from './NotificationSettings';
import QuerySection from './QuerySection';
import { CreateAlertV2Props } from './types';
import { CreateAlertProps } from './types';
import { Spinner } from './utils';
import './CreateAlertV2.styles.scss';
import './CreateAlert.styles.scss';
function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element {
function CreateAlert({ alertType }: CreateAlertProps): JSX.Element {
const queryToRedirect = buildInitialAlertDef(alertType);
const currentQueryToRedirect = mapQueryDataFromApi(
queryToRedirect.condition.compositeQuery,
@@ -36,8 +36,8 @@ function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element {
);
}
CreateAlertV2.defaultProps = {
CreateAlert.defaultProps = {
initialQuery: initialQueriesMap.metrics,
};
export default CreateAlertV2;
export default CreateAlert;

View File

@@ -1,14 +1,7 @@
import { useCallback, useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import logEvent from 'api/common/logEvent';
import classNames from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { RotateCcw } from '@signozhq/icons';
import { useAlertRuleOptional } from 'providers/Alert';
import { Labels } from 'types/api/alerts/def';
@@ -22,8 +15,6 @@ function CreateAlertHeader(): JSX.Element {
const alertRuleContext = useAlertRuleOptional();
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const groupByLabels = useMemo(() => {
const labels = new Array<string>();
@@ -46,14 +37,6 @@ function CreateAlertHeader(): JSX.Element {
[groupByLabels],
);
const handleSwitchToClassicExperience = useCallback(() => {
logEvent('Alert: Switch to classic experience button clicked', {});
urlQuery.set(QueryParams.showClassicCreateAlertsPage, 'true');
const url = `${ROUTES.ALERTS_NEW}?${urlQuery.toString()}`;
safeNavigate(url, { replace: true });
}, [safeNavigate, urlQuery]);
return (
<div
className={classNames('alert-header', { 'edit-alert-header': isEditMode })}
@@ -61,15 +44,6 @@ function CreateAlertHeader(): JSX.Element {
{!isEditMode && (
<div className="alert-header__tab-bar">
<div className="alert-header__tab">New Alert Rule</div>
<Button
prefix={<RotateCcw size={12} />}
onClick={handleSwitchToClassicExperience}
variant="solid"
color="secondary"
size="sm"
>
Switch to Classic Experience
</Button>
</div>
)}
<div className="alert-header__content">

View File

@@ -1,9 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
import * as useSafeNavigateHook from 'hooks/useSafeNavigate';
import { defaultPostableAlertRuleV2 } from 'container/CreateAlert/constants';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlert/utils';
import { mockAppLocation } from 'tests/router-mocks';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -11,11 +8,6 @@ import * as rulesHook from '../../../../api/generated/services/rules';
import { CreateAlertProvider } from '../../context';
import CreateAlertHeader from '../CreateAlertHeader';
const mockSafeNavigate = jest.fn();
jest.spyOn(useSafeNavigateHook, 'useSafeNavigate').mockReturnValue({
safeNavigate: mockSafeNavigate,
});
jest.spyOn(rulesHook, 'useCreateRule').mockReturnValue({
mutate: jest.fn(),
isLoading: false,
@@ -101,37 +93,4 @@ describe('CreateAlertHeader', () => {
screen.getByPlaceholderText(ENTER_ALERT_RULE_NAME_PLACEHOLDER),
).toHaveValue('TEST_ALERT');
});
it('should navigate to classic experience when button is clicked', () => {
renderCreateAlertHeader();
const switchToClassicExperienceButton = screen.getByText(
'Switch to Classic Experience',
);
expect(switchToClassicExperienceButton).toBeInTheDocument();
fireEvent.click(switchToClassicExperienceButton);
const params = new URLSearchParams();
params.set(QueryParams.showClassicCreateAlertsPage, 'true');
expect(mockSafeNavigate).toHaveBeenCalledWith(
`${ROUTES.ALERTS_NEW}?${params.toString()}`,
{ replace: true },
);
});
it('should not render "switch to classic experience" button when isEditMode is true', () => {
render(
<CreateAlertProvider
isEditMode
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
initialAlertState={getCreateAlertLocalStateFromAlertDef(
defaultPostableAlertRuleV2,
)}
>
<CreateAlertHeader />
</CreateAlertProvider>,
);
expect(
screen.queryByText('Switch to Classic Experience'),
).not.toBeInTheDocument();
});
});

View File

@@ -1,9 +1,9 @@
import { useMemo } from 'react';
import { Button } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useCreateAlertState } from 'container/CreateAlertV2/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlertV2/context/constants';
import { IEditCustomScheduleProps } from 'container/CreateAlertV2/EvaluationSettings/types';
import { useCreateAlertState } from 'container/CreateAlert/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlert/context/constants';
import { IEditCustomScheduleProps } from 'container/CreateAlert/EvaluationSettings/types';
import { Calendar1, Pencil, Trash } from '@signozhq/icons';
function EditCustomSchedule({

View File

@@ -2,8 +2,8 @@ import { useEffect, useMemo, useState } from 'react';
import { Button, DatePicker, Input, Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import classNames from 'classnames';
import { useCreateAlertState } from 'container/CreateAlertV2/context';
import { AdvancedOptionsState } from 'container/CreateAlertV2/context/types';
import { useCreateAlertState } from 'container/CreateAlert/context';
import { AdvancedOptionsState } from 'container/CreateAlert/context/types';
import dayjs from 'dayjs';
import { Code, PencilLine } from '@signozhq/icons';

View File

@@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as alertState from 'container/CreateAlertV2/context';
import * as alertState from 'container/CreateAlert/context';
import AdvancedOptions from '../AdvancedOptions';
import { createMockAlertContextState } from './testUtils';

View File

@@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as alertState from 'container/CreateAlertV2/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlertV2/context/constants';
import * as alertState from 'container/CreateAlert/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlert/context/constants';
import { TIMEZONE_DATA } from '../constants';
import EditCustomSchedule from '../EvaluationCadence/EditCustomSchedule';

View File

@@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as alertState from 'container/CreateAlertV2/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlertV2/context/constants';
import * as alertState from 'container/CreateAlert/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlert/context/constants';
import { TIMEZONE_DATA } from '../constants';
import EvaluationCadence from '../EvaluationCadence';

View File

@@ -1,7 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as alertState from 'container/CreateAlertV2/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlertV2/context/constants';
import { AdvancedOptionsState } from 'container/CreateAlertV2/context/types';
import * as alertState from 'container/CreateAlert/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlert/context/constants';
import { AdvancedOptionsState } from 'container/CreateAlert/context/types';
import EvaluationCadenceDetails from '../EvaluationCadence/EvaluationCadenceDetails';
import { createMockAlertContextState } from './testUtils';

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import * as alertState from 'container/CreateAlertV2/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlertV2/context/constants';
import * as alertState from 'container/CreateAlert/context';
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlert/context/constants';
import { TIMEZONE_DATA } from '../constants';
import EvaluationCadencePreview, {

View File

@@ -1,11 +1,11 @@
import { render, screen } from '@testing-library/react';
import * as alertState from 'container/CreateAlertV2/context';
import * as alertState from 'container/CreateAlert/context';
import EvaluationSettings from '../EvaluationSettings';
import { createMockAlertContextState } from './testUtils';
jest.mock('container/CreateAlertV2/utils', () => ({
...jest.requireActual('container/CreateAlertV2/utils'),
jest.mock('container/CreateAlert/utils', () => ({
...jest.requireActual('container/CreateAlert/utils'),
}));
const mockSetEvaluationWindow = jest.fn();

View File

@@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { EvaluationWindowState } from 'container/CreateAlertV2/context/types';
import { EvaluationWindowState } from 'container/CreateAlert/context/types';
import {
EVALUATION_WINDOW_TIMEFRAME,

View File

@@ -4,11 +4,11 @@ import {
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from 'container/CreateAlertV2/context/constants';
} from 'container/CreateAlert/context/constants';
import {
EvaluationWindowState,
ICreateAlertContextProps,
} from 'container/CreateAlertV2/context/types';
} from 'container/CreateAlert/context/types';
import { AlertTypes } from 'types/api/alerts/alertTypes';
export const createMockAlertContextState = (

View File

@@ -20,7 +20,7 @@ jest.mock('dayjs', () => {
});
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { EvaluationWindowState } from 'container/CreateAlertV2/context/types';
import { EvaluationWindowState } from 'container/CreateAlert/context/types';
import dayjs, { Dayjs } from 'dayjs';
import { rrulestr } from 'rrule';

View File

@@ -2,8 +2,8 @@ import { fireEvent, render, screen } from '@testing-library/react';
import {
AlertThresholdMatchType,
AlertThresholdOperator,
} from 'container/CreateAlertV2/context/types';
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
} from 'container/CreateAlert/context/types';
import { createMockAlertContextState } from 'container/CreateAlert/EvaluationSettings/__tests__/testUtils';
import * as createAlertState from '../../context';
import Footer from '../Footer';

View File

@@ -6,15 +6,15 @@ import {
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from 'container/CreateAlertV2/context/constants';
} from 'container/CreateAlert/context/constants';
import {
AdvancedOptionsState,
AlertThresholdOperator,
EvaluationWindowState,
NotificationSettingsState,
Seasonality,
} from 'container/CreateAlertV2/context/types';
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
} from 'container/CreateAlert/context/types';
import { createMockAlertContextState } from 'container/CreateAlert/EvaluationSettings/__tests__/testUtils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { EQueryType } from 'types/common/dashboard';
@@ -556,8 +556,6 @@ describe('Footer utils', () => {
},
ruleType: 'threshold_rule',
schemaVersion: 'v2alpha1',
source: 'http://localhost/',
version: 'v5',
});
});

View File

@@ -1,12 +1,12 @@
import { RuletypesRuleTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
AlertTypes,
BasicThreshold,
PostableAlertRuleV2,
} from 'types/api/alerts/alertTypesV2';
} from 'types/api/alerts/alertTypes';
import { EQueryType } from 'types/common/dashboard';
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
@@ -266,7 +266,7 @@ export function buildCreateThresholdAlertRulePayload(
// Evaluation
const evaluationProps = getEvaluationProps(evaluationWindow, advancedOptions);
let ruleType: string = AlertDetectionTypes.THRESHOLD_ALERT;
let ruleType: string = RuletypesRuleTypeDTO.threshold_rule;
if (query.queryType === EQueryType.PROM) {
ruleType = 'promql_rule';
}
@@ -292,9 +292,7 @@ export function buildCreateThresholdAlertRulePayload(
summary: notificationSettings.description,
},
notificationSettings: notificationSettingsProps,
version: 'v5',
schemaVersion: 'v2alpha1',
source: window?.location.toString(),
};
}
@@ -321,7 +319,7 @@ export function buildCreateAnomalyAlertRulePayload(
return {
...payload,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
ruleType: RuletypesRuleTypeDTO.anomaly_rule,
alertType: AlertTypes.METRICS_BASED_ALERT,
condition: {
...payload.condition,

View File

@@ -1,12 +1,12 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ALL_SELECTED_VALUE } from 'container/CreateAlertV2/constants';
import * as createAlertContext from 'container/CreateAlertV2/context';
import { ALL_SELECTED_VALUE } from 'container/CreateAlert/constants';
import * as createAlertContext from 'container/CreateAlert/context';
import {
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from 'container/CreateAlertV2/context/constants';
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
} from 'container/CreateAlert/context/constants';
import { createMockAlertContextState } from 'container/CreateAlert/EvaluationSettings/__tests__/testUtils';
import MultipleNotifications from '../MultipleNotifications';

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as createAlertContext from 'container/CreateAlertV2/context';
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
import * as createAlertContext from 'container/CreateAlert/context';
import { createMockAlertContextState } from 'container/CreateAlert/EvaluationSettings/__tests__/testUtils';
import NotificationMessage from '../NotificationMessage';

View File

@@ -1,11 +1,11 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as createAlertContext from 'container/CreateAlertV2/context';
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
import * as createAlertContext from 'container/CreateAlert/context';
import { createMockAlertContextState } from 'container/CreateAlert/EvaluationSettings/__tests__/testUtils';
import NotificationSettings from '../NotificationSettings';
jest.mock(
'container/CreateAlertV2/NotificationSettings/MultipleNotifications',
'container/CreateAlert/NotificationSettings/MultipleNotifications',
() => ({
__esModule: true,
default: (): JSX.Element => (
@@ -14,7 +14,7 @@ jest.mock(
}),
);
jest.mock(
'container/CreateAlertV2/NotificationSettings/NotificationMessage',
'container/CreateAlert/NotificationSettings/NotificationMessage',
() => ({
__esModule: true,
default: (): JSX.Element => (
@@ -23,8 +23,8 @@ jest.mock(
}),
);
jest.mock('container/CreateAlertV2/utils', () => ({
...jest.requireActual('container/CreateAlertV2/utils'),
jest.mock('container/CreateAlert/utils', () => ({
...jest.requireActual('container/CreateAlert/utils'),
}));
const initialNotificationSettings =

View File

@@ -6,7 +6,7 @@ import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryParams } from 'constants/query';
import { useCreateAlertState } from 'container/CreateAlertV2/context';
import { useCreateAlertState } from 'container/CreateAlert/context';
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
import PlotTag from 'components/PlotTag/PlotTag';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';

View File

@@ -174,12 +174,10 @@ function QuerySection(): JSX.Element {
runQuery={runQueryHandler}
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
alertDef={alertDef}
panelType={PANEL_TYPES.TIME_SERIES}
isAnomalyDetection={isAnomalyDetection}
key={currentQuery.queryType}
ruleId=""
hideTitle
/>
</div>
);

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