Compare commits

..

46 Commits

Author SHA1 Message Date
Jatinderjit Singh
494552c530 remove unused function evaluateLabelExpression 2026-05-19 17:41:36 +05:30
Jatinderjit Singh
f7e7485f97 fix(tests): resolve envprovider env isolation, factory name length, and ShouldSkip signature
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 17:41:36 +05:30
Jatinderjit Singh
493b337494 fix lset type and update openapi spec 2026-05-19 17:41:36 +05:30
Jatinderjit Singh
8dc458e6ba implement Down migration to drop label_expression column 2026-05-19 17:41:36 +05:30
Jatinderjit Singh
e7176de589 remove redundant LabelSet->map conversion 2026-05-19 17:41:36 +05:30
Jatinderjit Singh
0500388d4c Move label expression evaluation into ShouldSkip
ShouldSkip now owns all three suppression checks in sequence:
rule ID match → schedule active → label expression.
IsActive passes nil labels so the expression check is skipped
(no instance labels available for UI status).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 17:41:36 +05:30
Jatinderjit Singh
d2010b01ab Remove redundant || undefined from labelExpression assignment 2026-05-19 17:41:36 +05:30
Jatinderjit Singh
3929138c87 Add label expression support to planned downtime
Alert instances can now be scoped by label expression
(e.g. env == "prod"), scoping suppression below the rule level.
A window with no rule IDs and a label expression silences any
alert whose labels match, regardless of which rule fired it;
when rule IDs are also present, the expression is evaluated only
within the matched rules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 17:41:36 +05:30
Jatinderjit Singh
f08782adeb test: add e2e muting tests for maintenance window behaviour 2026-05-19 17:37:06 +05:30
Jatinderjit Singh
b81f0dc8e5 cleanup test 2026-05-19 16:38:21 +05:30
Jatinderjit Singh
a97feaceb0 chore: regenerate mocks via make gen-mocks
Picks up new MockHandler for the Handler interface in pkg/alertmanager
and regenerates MockMaintenanceStore with canonical mockery formatting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:25:17 +05:30
Jatinderjit Singh
165218dedb test: use mockery-generated mock for MaintenanceStore in muter tests
Replace hand-written fakeMaintenanceStore with a mockery-generated
MockMaintenanceStore, consistent with the alertmanagertest pattern.
Also adds MaintenanceStore to .mockery.yml so the mock stays in sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:11:48 +05:30
Jatinderjit Singh
6ee3f75bf6 Go lint fixes 2026-05-19 15:02:47 +05:30
Jatinderjit Singh
32e5bf2f17 fix NewMaintenanceStore in tests 2026-05-19 14:51:18 +05:30
Jatinderjit Singh
dbebb76bda Re-add marker 2026-05-19 14:29:26 +05:30
Jatinderjit Singh
13547f29e4 Update schema changes 2026-05-19 14:10:03 +05:30
Jatinderjit Singh
cd3e4bcb87 test: add unit tests for MaintenanceMuter
Covers Mutes/MutedBy semantics (empty label, rule match, empty-RuleIDs
matches-all, future windows, multi-window) and the result cache
(single-fetch within TTL, stale-cache fallback on store error,
re-fetch after expiry, concurrency safety).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 13:57:04 +05:30
Jatinderjit Singh
b5ae86c3f7 refactor: move maintenance (planned downtime) to alertmanager packages
Types move from pkg/types/ruletypes/ to pkg/types/alertmanagertypes/:
- maintenance.go, recurrence.go, schedule.go (+ tests)

Store impl moves from pkg/ruler/rulestore/sqlrulestore/ to
pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore/.

Maintenance windows mute alerts, so they belong with alertmanager
rather than the rule types.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 13:57:04 +05:30
Jatinderjit Singh
6143b9bac2 code cleanup 2026-05-19 13:53:29 +05:30
Jatinderjit Singh
09ac78b42e feat: surface maintenance-suppressed alerts via mutedBy in GetAlerts
Alerts suppressed by an active maintenance window were being correctly
muted in the notification pipeline but appeared as state=active in the
v2 GetAlerts response, since MaintenanceMuter.Mutes had no marker
side-effect (unlike inhibitor/silencer).

Add MaintenanceMuter.MutedBy returning the matching window IDs, and
plumb a mutedByFunc callback through NewGettableAlertsFromAlertProvider
into AlertToOpenAPIAlert. The upstream v2 API forces state=suppressed
when mutedBy is non-empty, so the frontend's existing state-based
rendering picks it up without further changes.

Use the dedicated mutedBy field rather than SilencedBy to avoid
violating the "complete set of silence IDs" contract that anything
querying silences by ID would rely on.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
84ca7c0bd9 remove redundant MemMarker wrapper 2026-05-19 13:53:29 +05:30
Jatinderjit Singh
08c763ba0a refactor: move MaintenanceMuter to Server and pass it to pipelineBuilder.New
- Remove muter from pipelineBuilder struct and newPipelineBuilder();
  pass it as a parameter to New() instead, consistent with inhibitor/silencer
- Store muter on Server so GetAlerts can call Mutes() alongside the
  inhibitor and silencer, ensuring maintenance-suppressed alerts show
  the correct muted status in API responses

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
27182a0275 refactor: always initialize maintenanceStore; remove nil guards
Tests now use a real sqlrulestore-backed MaintenanceMuter instead of
passing nil. With nil no longer a valid input, remove the nil guards
in server.go and pipeline_builder.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
2a59ec62ca refactor: hoist MuteStage construction out of the receiver loop
MuteStage holds no per-receiver state, so one instance shared across
all receivers is sufficient — matching how is/ss are handled upstream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
ec07c80e70 refactor: replace maintenanceMuteStage with notify.NewMuteStage
MaintenanceMuter already satisfies types.Muter, and pipelineBuilder has
its own pb.metrics, so the hand-rolled maintenanceMuteStage wrapper is
redundant. Use notify.NewMuteStage(pb.muter, pb.metrics) directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
32747dcb52 rename buildReceiverStage -> createReceiverStage 2026-05-19 13:53:29 +05:30
Jatinderjit Singh
b517e97612 refactor: remove dead orgID param from task constructors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
7b99f4475c refactor: pass MaintenanceMuter directly to pipelineBuilder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
7a8826531e chore: replace SPDX tag with full Apache 2.0 license boilerplate
The full license text is unambiguously compliant with Apache 2.0 Section 4(a),
which requires giving recipients "a copy of this License".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
6ca316df01 chore: add license header to pipeline_builder.go
Copied code originates from Apache-2.0 licensed Prometheus Alertmanager;
add dual copyright + SPDX identifier following the repo's convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
e5ba44f257 refactor: move maintenance mute stage into custom pipelineBuilder
Copy notify.PipelineBuilder locally so we can inject mms between the
silence stage and the receiver stage (GossipSettle → Inhibit →
TimeActive → TimeMute → Silence → mms → Receiver), matching the
correct suppression order the team requires.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
49a6f879a2 refactor: wrap routing pipeline once instead of per-route injection
Replace the per-route-entry loop with a single MultiStage wrap so
maintenance suppression runs once per dispatch group before routing.
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
4127705a0c add maintenanceMuteStage to move planned maintenance to alertmanager
Rules previously skipped rule.Eval() entirely during maintenance windows.
This change moves suppression to MaintenanceMuter, injected as a Stage
in the alertmanager notification pipeline. Now rules always evaluate and
everys suppression is handled by alertmanager.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:29 +05:30
Jatinderjit Singh
b48851e487 fix: maintenance ignores recurrence when fixed times also set (#11121)
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
Release Drafter / update_release_draft (push) Has been cancelled
* fix: maintenance ignores recurrence when fixed times also set

* send empty start/end dates in frontend for recurring windows

* handle zero start and end times in schedule

* Revert "send empty start/end dates in frontend for recurring windows"

This reverts commit 87bc3fae274ccfd9ce98aeae5ac379fadf657df3.

* Remove start and end time from recurrence

* fix display timezone

* remove redundant param `shouldKeepLocalTime`

* handle empty initial start time

* fix CI issues

* Revert "fix CI issues"

This reverts commit 772e6486bb03ec836ebdce436e820aa0d1defdda.

* Revert "handle empty initial start time"

This reverts commit 82e7c72a338b019dea57def1c61795ca749aacc0.

* Revert "remove redundant param `shouldKeepLocalTime`"

This reverts commit ed942426745b8b534cdc47dc8b885beef0d6c2f1.

* Revert "fix display timezone"

This reverts commit 9b2a61674e883f2b47f5bd52413e257ef6f861d3.

* Revert "Remove start and end time from recurrence"

This reverts commit ab0df8e22d6099772eec79af11d2453a9d95e157.

* Revert "Revert "send empty start/end dates in frontend for recurring windows""

This reverts commit 15a4166d3740877b601f16ba208dd3c291b387f2.

* Revert "handle zero start and end times in schedule"

This reverts commit 58a5aecb82f1aa4f8d5549e391f1f2c5c7574be2.

* Revert "send empty start/end dates in frontend for recurring windows"

This reverts commit 0470cc7a84f6e9f91cccd73d7841b884342031d4.

* log maintenance window for schedule-recurrence timestamp mismatch

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-05-19 06:52:22 +00:00
Ashwin Bhatkal
279a71c5b3 fix(dashboard): align Delete dashboard with other action menu items (#11352)
The Delete dashboard entry in the dashboards action menu was rendered with
a `<Flex justify="center">` and a custom `TableLinkText` span. This caused
the icon and label to be center-aligned, sized differently, and spaced
differently from the four sibling entries (View, Open in New Tab, Copy
Link, Export JSON) which use an antd `<Button>` with `.action-btn` styling.

Switch the Delete entry to the same antd `<Button>` structure as the rest
of the menu so the icon size, icon-to-text spacing, and left alignment
all match. While here, collapse the `section-1` / `section-2` wrappers
into a single `.actionContent` and move the action-menu styles into a
co-located CSS module (`DashboardActions.module.scss`) with a `deleteBtn`
modifier that carries the divider and the danger color via the
`--danger-background` semantic token.
2026-05-19 04:11:57 +00:00
Vinicius Lourenço
7e63e35113 fix(form-alert-rules): confirm modal broken (#11347)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / staging (push) Has been cancelled
2026-05-18 22:53:47 +00:00
SagarRajput-7
d5a50fe456 feat(role-fga): added feature flag gate on roles fga - create and details page (#11350)
* feat(role-fga): added feature flag gate on roles fga - create and details page

* feat(role-fga): updated tests

* feat(role-fga): added is role gate fetching logic including feature flag loading

* feat(role-fga): fix the rolesselect search not working for the dropdown options

* feat(role-fga): updated tests and refactor
2026-05-18 20:56:39 +00:00
Vikrant Gupta
885b41356a chore(authz): add authz feature flag (#11341) 2026-05-18 15:29:05 +00:00
Ashwin Bhatkal
b653c69e29 fix(frontend): unblock pnpm generate:api after orval v8 upgrade (#11346)
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
Release Drafter / update_release_draft (push) Has been cancelled
Orval v8's pre-generation validator (@scalar/openapi-parser) treats every
`$ref` key as a JSON Reference. Our spec embeds Perses' `common.JSONRef`
struct, which has a property literally named `$ref`, so validation aborts
with `INVALID_REFERENCE`. Set `input.unsafeDisableValidation: true` to
bypass — codegen itself handles the spec correctly, and the spec is
backend-generated and CI-gated.

Closes SigNoz/engineering-pod#4963
2026-05-18 14:33:23 +00:00
Abhi kumar
7d2f8b291e chore: added changes for sorting tooltip content (#11320) 2026-05-18 13:53:19 +00:00
Aditya Singh
3bea4484f9 Enable new trace details page (#11296)
* feat: span details floating drawer added

* feat: span details folder rename

* feat: replace draggable package

* feat: fix pinning. fix drag on top

* feat: add bound to drags while floating

* feat: add collapsible sections in trace details

* feat: use resizable for waterfall table as well

* feat: copy link change and url clear on span close

* feat: fix span details headr

* feat: key value label style fixes

* feat: linked spans

* feat: style fixes

* feat: setup types and interface for waterfall v3

v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service

* refactor: move type conversion logic to types pkg

* chore: add reason for using snake case in response

* fix: update span.attributes to map of string to any

To support otel format of diffrent types of attributes

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* refactor: convert waterfall api to modules format

* chore: add same test cases as for old waterfall api

* chore: avoid sorting on every traversal

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* fix: rename timestamp to milli for readability

* fix: add timeout to module context

* fix: use typed paramter field in logs

* feat: api integration

* feat: add limit

* feat: minor change

* feat: supress click

* chore: generate openapi spec for v3 waterfall

* feat: fix test

* feat: fix test

* feat: lint fix

* feat: span details ux

* feat: analytics

* feat: add icons

* feat: added loading to flamegraph and timeout to webworker

* feat: sync error and loading state for flamegraph for n/w and computation logic

* feat: auto scroll horizontally to span

* feat: show total span count

* feat: disable anaytics span tab for now

* feat: add span details loader

* feat: prevent api call on closing span detail

* fix: remove timeout since waterfall take longer

* fix: use int16 for status code as per db schema

* fix: update openapi specs

* feat: make filter and search work with flamegraph

* feat: filter ui fix

* feat: remove trace header

* feat: new filter ui

* feat: setup types and interface for waterfall v3

v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service

* refactor: move type conversion logic to types pkg

* chore: add reason for using snake case in response

* fix: update span.attributes to map of string to any

To support otel format of diffrent types of attributes

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* refactor: convert waterfall api to modules format

* chore: add same test cases as for old waterfall api

* chore: avoid sorting on every traversal

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* fix: rename timestamp to milli for readability

* fix: add timeout to module context

* fix: use typed paramter field in logs

* chore: generate openapi spec for v3 waterfall

* fix: remove timeout since waterfall take longer

* fix: use int16 for status code as per db schema

* fix: update openapi specs

* feat: api integration

* feat: automatically scroll left on vertical scroll

* feat: reduce time

* feat: set limit to 100k for flamegraph

* feat: show child count in waterfall

* fix: align timeline and span length in flamegraph and waterfall

* feat: fix flamegraph and waterfall bg color

* feat: show caution on sampled flamegraph

* feat: api integration v3

* feat: disable scroll to view for collapse and uncollapse

* feat: setup types and interface for waterfall v3

v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service

* refactor: move type conversion logic to types pkg

* chore: add reason for using snake case in response

* fix: update span.attributes to map of string to any

To support otel format of diffrent types of attributes

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* refactor: convert waterfall api to modules format

* chore: add same test cases as for old waterfall api

* chore: avoid sorting on every traversal

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* fix: rename timestamp to milli for readability

* fix: add timeout to module context

* fix: use typed paramter field in logs

* chore: generate openapi spec for v3 waterfall

* fix: remove timeout since waterfall take longer

* fix: use int16 for status code as per db schema

* fix: update openapi specs

* refactor: break down GetWaterfall method for readability

* chore: avoid returning nil, nil

* refactor: move type creation and constants to types package

- Move DB/table/cache/windowing constants to tracedetailtypes package
- Add NewWaterfallTrace and NewWaterfallResponse constructors in types
- Use constructors in module.go instead of inline struct literals
- Reorder waterfall.go so public functions precede private ones

* refactor: extract ClickHouse queries into a store abstraction

Move GetTraceSummary and GetTraceSpans out of module.go into a
traceStore interface backed by clickhouseTraceStore in store.go.
The module struct now holds a traceStore instead of a raw
telemetrystore.TelemetryStore, keeping DB access separate from
business logic.

* refactor: move error to types as well

* refactor: separate out store calls and computations

* refactor: breakdown GetSelectedSpans for readability

* refactor: return 404 on missing trace and other cleanup

* refactor: use same method for cache key creation

* chore: remove unused duration nano field

* chore: use sqlbuilder in clickhouse store where possible

* feat: dropdown added to span details

* feat: fix color duplications

* feat: no data screen

* feat: old trace btn added

* feat: minor fix

* feat: rename copy to copy value

* feat: delete unused file

* feat: use semantic tokens

* feat: use semantic tokens

* feat: add crosshair

* feat: fix test

* feat: disable crosshair in waterfall

* feat: fix colors

* feat: minor fix

* feat: add status codes

* feat: load all spans in waterfall under limit

* feat: uncollapse spans on select from flamegraph

* feat: style fix

* feat: add service name

* feat: open in new tab

* feat: add trace details header

* feat: add trace details header styles

* feat: add trace details header styles

* feat: minor changes

* feat: floating fields set

* feat: filters init

* feat: filter toggle added

* feat: fix color

* fix: scroll to span in frontend mode

* feat: delete waterfall go

* feat: minor change

* feat: minor change

* feat: lint fix

* feat: analytics spans

* feat: color by field

* feat: save color by pref in user pref

* feat: migrate v2 pinned attr

* feat: preview fields

* feat: minor refactors

* feat: minor refactors

* feat: v3 behind feature flag

* feat: minor refactors

* feat: packages remove

* feat: packages remove

* feat: remove common component

* feat: remove antd component usage

* feat: leaf node indent fix

* feat: fix mouse wheel in json view

* feat: update signoz ui

* feat: remove feature flag

* feat: fixed the waterfall span hover card

* feat: fix hidden filters

* feat: trace details always visible

* feat: correct status code

* fix: pagination calls in waterfall

* feat: fix failing test

* feat: show error count

* feat: fix waterfall child sibling indent

* feat: change how we show span hover data in waterfall

* feat: fix logs in span details styles

* feat: minor fixes

* feat: make trace id copyable

* feat: add status message to highlight section

* feat: persist user choosing old view

* feat: add more fields in color by

* feat: add llm as fast filter

* feat: show api error correctly

* feat: update test cases

* feat: revert route change

* feat: revert route change

* feat: replace antd btns

* feat: allow removing all fields in preview

* feat: send selected span when flamegraph is sampled

* feat: only scroll when span is not in view

* feat: auto expand on highlight errors

* feat: move analytics panel

* feat: additional check

* feat: minor fix

* feat: minor fix

* feat: dont use antd button and tooltip

* feat: dont use antd button and tooltip

* feat: update icons

* feat: minor change

* feat: minor change

* feat: move to zustand

* feat: update test cases

* feat: update border color

* feat: add icons

* feat: support filter on parent keys

* feat: add links to non filterable keys

* feat: minor fix

* feat: use pinned attributes accross views

* feat: update tests

* feat: hide v3

* feat: migrate to css modules

* feat: fix minor style

* feat: fix test

* feat: enable new trace details

* feat: remove unnecessary waterfall api calls if span already in the list

* feat: minor change

---------

Co-authored-by: Nikhil Soni <nikhil.soni@signoz.io>
2026-05-18 13:51:33 +00:00
SagarRajput-7
87ceba2d84 feat(role-sa-fga): role sa fga followup changes (#11330)
* feat(role-sa-fga): updated roles detail permission panel with the new allowedVerb gate

* feat(role-sa-fga): added anonymous in roles, sa routes to allow user access without managed role

* feat(role-sa-fga): gated roles create and details page behind a valid license check

* feat(role-sa-fga): added test and some refactor
2026-05-18 12:21:38 +00:00
Manika Malhotra
445dc3b290 chore(onboarding): shuffle ordering of interest in SigNoz based on version (#11336)
* chore(onboarding): shuffle ordering of interest in SigNoz based on version

* fix: formatting
2026-05-18 12:12:48 +00:00
Tushar Vats
76b35b9d8f fix: order by ignored in formula query (#10950)
* fix: order by ignored in formula query

* fix: order by ignored in formula query

* fix: added intergation test

* fix: revert integarion test changes

* fix: added an independent integration test

* fix: make py-fmt

* fix: removed comment

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: Pandey <vibhupandey28@gmail.com>
2026-05-18 11:38:40 +00:00
Tushar Vats
b860cce31d fix: enforce minimum step interval for v3 promql queries (#11293) 2026-05-18 11:27:52 +00:00
Tushar Vats
1bd4ca88de fix: cache memory leak (#10967)
* fix: added cost() to cloneable interface

* fix: added a new metrics and converted into counters

* fix: address comments

* fix: simplify test

* fix: use assert instead of require
2026-05-18 10:50:27 +00:00
121 changed files with 3523 additions and 1583 deletions

View File

@@ -8,6 +8,14 @@ packages:
filename: "alertmanager.go"
structname: 'Mock{{.InterfaceName}}'
pkgname: '{{.SrcPackageName}}test'
github.com/SigNoz/signoz/pkg/types/alertmanagertypes:
interfaces:
MaintenanceStore:
config:
dir: '{{.InterfaceDir}}/alertmanagertypestest'
filename: "maintenance.go"
structname: 'Mock{{.InterfaceName}}'
pkgname: '{{.SrcPackageName}}test'
github.com/SigNoz/signoz/pkg/tokenizer:
config:
all: true

View File

@@ -96,6 +96,55 @@ components:
- createdAt
- updatedAt
type: object
AlertmanagertypesMaintenanceKind:
enum:
- fixed
- recurring
type: string
AlertmanagertypesMaintenanceStatus:
enum:
- active
- upcoming
- expired
type: string
AlertmanagertypesPlannedMaintenance:
properties:
alertIds:
items:
type: string
nullable: true
type: array
createdAt:
format: date-time
type: string
createdBy:
type: string
description:
type: string
id:
type: string
kind:
$ref: '#/components/schemas/AlertmanagertypesMaintenanceKind'
labelExpression:
type: string
name:
type: string
schedule:
$ref: '#/components/schemas/AlertmanagertypesSchedule'
status:
$ref: '#/components/schemas/AlertmanagertypesMaintenanceStatus'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
- name
- schedule
- status
- kind
type: object
AlertmanagertypesPostableChannel:
oneOf:
- required:
@@ -212,6 +261,25 @@ components:
required:
- name
type: object
AlertmanagertypesPostablePlannedMaintenance:
properties:
alertIds:
items:
type: string
nullable: true
type: array
description:
type: string
labelExpression:
type: string
name:
type: string
schedule:
$ref: '#/components/schemas/AlertmanagertypesSchedule'
required:
- name
- schedule
type: object
AlertmanagertypesPostableRoutePolicy:
properties:
channels:
@@ -237,6 +305,60 @@ components:
- channels
- name
type: object
AlertmanagertypesRecurrence:
properties:
duration:
type: string
endTime:
format: date-time
nullable: true
type: string
repeatOn:
items:
$ref: '#/components/schemas/AlertmanagertypesRepeatOn'
nullable: true
type: array
repeatType:
$ref: '#/components/schemas/AlertmanagertypesRepeatType'
startTime:
format: date-time
type: string
required:
- startTime
- duration
- repeatType
type: object
AlertmanagertypesRepeatOn:
enum:
- sunday
- monday
- tuesday
- wednesday
- thursday
- friday
- saturday
type: string
AlertmanagertypesRepeatType:
enum:
- daily
- weekly
- monthly
type: string
AlertmanagertypesSchedule:
properties:
endTime:
format: date-time
type: string
recurrence:
$ref: '#/components/schemas/AlertmanagertypesRecurrence'
startTime:
format: date-time
type: string
timezone:
type: string
required:
- timezone
type: object
AuthtypesAttributeMapping:
properties:
email:
@@ -5137,17 +5259,6 @@ components:
message:
type: string
type: object
RuletypesMaintenanceKind:
enum:
- fixed
- recurring
type: string
RuletypesMaintenanceStatus:
enum:
- active
- upcoming
- expired
type: string
RuletypesMatchType:
enum:
- at_least_once
@@ -5175,59 +5286,6 @@ components:
- table
- graph
type: string
RuletypesPlannedMaintenance:
properties:
alertIds:
items:
type: string
nullable: true
type: array
createdAt:
format: date-time
type: string
createdBy:
type: string
description:
type: string
id:
type: string
kind:
$ref: '#/components/schemas/RuletypesMaintenanceKind'
name:
type: string
schedule:
$ref: '#/components/schemas/RuletypesSchedule'
status:
$ref: '#/components/schemas/RuletypesMaintenanceStatus'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
- name
- schedule
- status
- kind
type: object
RuletypesPostablePlannedMaintenance:
properties:
alertIds:
items:
type: string
nullable: true
type: array
description:
type: string
name:
type: string
schedule:
$ref: '#/components/schemas/RuletypesSchedule'
required:
- name
- schedule
type: object
RuletypesPostableRule:
properties:
alert:
@@ -5280,29 +5338,6 @@ components:
- clickhouse_sql
- promql
type: string
RuletypesRecurrence:
properties:
duration:
type: string
endTime:
format: date-time
nullable: true
type: string
repeatOn:
items:
$ref: '#/components/schemas/RuletypesRepeatOn'
nullable: true
type: array
repeatType:
$ref: '#/components/schemas/RuletypesRepeatType'
startTime:
format: date-time
type: string
required:
- startTime
- duration
- repeatType
type: object
RuletypesRenotify:
properties:
alertStates:
@@ -5314,22 +5349,6 @@ components:
interval:
type: string
type: object
RuletypesRepeatOn:
enum:
- sunday
- monday
- tuesday
- wednesday
- thursday
- friday
- saturday
type: string
RuletypesRepeatType:
enum:
- daily
- weekly
- monthly
type: string
RuletypesRollingWindow:
properties:
evalWindow:
@@ -5449,21 +5468,6 @@ components:
- promql_rule
- anomaly_rule
type: string
RuletypesSchedule:
properties:
endTime:
format: date-time
type: string
recurrence:
$ref: '#/components/schemas/RuletypesRecurrence'
startTime:
format: date-time
type: string
timezone:
type: string
required:
- timezone
type: object
RuletypesScheduleType:
enum:
- hourly
@@ -8024,7 +8028,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
type: array
status:
type: string
@@ -8067,7 +8071,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
responses:
"201":
content:
@@ -8075,7 +8079,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
status:
type: string
required:
@@ -8178,7 +8182,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
status:
type: string
required:
@@ -8232,7 +8236,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
responses:
"204":
description: No Content

View File

@@ -80,6 +80,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
fineGrainedAuthz := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseFineGrainedAuthz, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureUseFineGrainedAuthz.String()),
Active: fineGrainedAuthz,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -13,7 +13,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
@@ -49,7 +48,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, tr)
// create ch rule task for evaluation
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
@@ -73,7 +72,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, pr)
// create promql rule task for evaluation
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
// create anomaly rule
@@ -96,7 +95,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, ar)
// create anomaly rule task for evaluation
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
} else {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
@@ -210,9 +209,9 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
}
// newTask returns an appropriate group for the rule type
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID valuer.UUID) baserules.Task {
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc) baserules.Task {
if taskType == baserules.TaskTypeCh {
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify)
}
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify)
}

View File

@@ -10,6 +10,13 @@ export default defineConfig({
signoz: {
input: {
target: '../docs/api/openapi.yml',
// Perses' `common.JSONRef` (used by `DashboardGridItem.content`) has a
// field tagged `json:"$ref"`, so our spec contains a property literally
// named `$ref`.
// Orval v8's validator (`@scalar/openapi-parser`) treats every `$ref` key
// as a JSON Reference and aborts with `INVALID_REFERENCE` when the value isn't a URI string.
// Safe to disable: yes, the spec is generated by `cmd/openapi.go` and gated by backend CI, not hand-edited.
unsafeDisableValidation: true,
},
output: {
target: './src/api/generated/services',

View File

@@ -144,18 +144,18 @@ const routes: AppRoutes[] = [
// /trace-old serves V3 (URL-only access). Flip the two `component`
// values back to release V3.
{
path: ROUTES.TRACE_DETAIL,
path: ROUTES.TRACE_DETAIL_OLD,
exact: true,
component: TraceDetail,
isPrivate: true,
key: 'TRACE_DETAIL',
key: 'TRACE_DETAIL_OLD',
},
{
path: ROUTES.TRACE_DETAIL_OLD,
path: ROUTES.TRACE_DETAIL,
exact: true,
component: TraceDetailV3,
isPrivate: true,
key: 'TRACE_DETAIL_OLD',
key: 'TRACE_DETAIL',
},
{
path: ROUTES.SETTINGS,

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {
@@ -19,6 +18,7 @@ import type {
} from 'react-query';
import type {
AlertmanagertypesPostablePlannedMaintenanceDTO,
CreateDowntimeSchedule201,
DeleteDowntimeScheduleByIDPathParameters,
GetDowntimeScheduleByID200,
@@ -26,7 +26,6 @@ import type {
ListDowntimeSchedules200,
ListDowntimeSchedulesParams,
RenderErrorResponseDTO,
RuletypesPostablePlannedMaintenanceDTO,
UpdateDowntimeScheduleByIDPathParameters,
} from '../sigNoz.schemas';
@@ -136,14 +135,14 @@ export const invalidateListDowntimeSchedules = async (
* @summary Create downtime schedule
*/
export const createDowntimeSchedule = (
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDowntimeSchedule201>({
url: `/api/v1/downtime_schedules`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostablePlannedMaintenanceDTO,
data: alertmanagertypesPostablePlannedMaintenanceDTO,
signal,
});
};
@@ -155,13 +154,13 @@ export const getCreateDowntimeScheduleMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
TContext
> => {
const mutationKey = ['createDowntimeSchedule'];
@@ -175,7 +174,7 @@ export const getCreateDowntimeScheduleMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> }
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -189,7 +188,7 @@ export type CreateDowntimeScheduleMutationResult = NonNullable<
Awaited<ReturnType<typeof createDowntimeSchedule>>
>;
export type CreateDowntimeScheduleMutationBody =
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
| undefined;
export type CreateDowntimeScheduleMutationError =
ErrorType<RenderErrorResponseDTO>;
@@ -204,13 +203,13 @@ export const useCreateDowntimeSchedule = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
TContext
> => {
return useMutation(getCreateDowntimeScheduleMutationOptions(options));
@@ -404,14 +403,14 @@ export const invalidateGetDowntimeScheduleByID = async (
*/
export const updateDowntimeScheduleByID = (
{ id }: UpdateDowntimeScheduleByIDPathParameters,
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/downtime_schedules/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostablePlannedMaintenanceDTO,
data: alertmanagertypesPostablePlannedMaintenanceDTO,
signal,
});
};
@@ -425,7 +424,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
},
TContext
>;
@@ -434,7 +433,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
},
TContext
> => {
@@ -451,7 +450,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -466,7 +465,7 @@ export type UpdateDowntimeScheduleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>
>;
export type UpdateDowntimeScheduleByIDMutationBody =
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
| undefined;
export type UpdateDowntimeScheduleByIDMutationError =
ErrorType<RenderErrorResponseDTO>;
@@ -483,7 +482,7 @@ export const useUpdateDowntimeScheduleByID = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
},
TContext
>;
@@ -492,7 +491,7 @@ export const useUpdateDowntimeScheduleByID = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
},
TContext
> => {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
export interface AlertmanagertypesChannelDTO {
/**
@@ -135,6 +134,113 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
updatedBy?: string | null;
}
export enum AlertmanagertypesMaintenanceKindDTO {
fixed = 'fixed',
recurring = 'recurring',
}
export enum AlertmanagertypesMaintenanceStatusDTO {
active = 'active',
upcoming = 'upcoming',
expired = 'expired',
}
export enum AlertmanagertypesRepeatOnDTO {
sunday = 'sunday',
monday = 'monday',
tuesday = 'tuesday',
wednesday = 'wednesday',
thursday = 'thursday',
friday = 'friday',
saturday = 'saturday',
}
export enum AlertmanagertypesRepeatTypeDTO {
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export interface AlertmanagertypesRecurrenceDTO {
/**
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: Date | null;
/**
* @type array,null
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: Date;
}
export interface AlertmanagertypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: Date;
recurrence?: AlertmanagertypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: Date;
/**
* @type string
*/
timezone: string;
}
export interface AlertmanagertypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: AlertmanagertypesMaintenanceKindDTO;
/**
* @type string
*/
labelExpression?: string;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
status: AlertmanagertypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
}
export interface ConfigAuthorizationDTO {
/**
* @type string
@@ -1598,6 +1704,26 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
wechat_configs?: ConfigWechatConfigDTO[];
};
export interface AlertmanagertypesPostablePlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
labelExpression?: string;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
}
export interface AlertmanagertypesPostableRoutePolicyDTO {
/**
* @type array,null
@@ -6117,15 +6243,6 @@ export interface RuletypesGettableTestRuleDTO {
message?: string;
}
export enum RuletypesMaintenanceKindDTO {
fixed = 'fixed',
recurring = 'recurring',
}
export enum RuletypesMaintenanceStatusDTO {
active = 'active',
upcoming = 'upcoming',
expired = 'expired',
}
export interface RuletypesRenotifyDTO {
/**
* @type array
@@ -6157,116 +6274,6 @@ export interface RuletypesNotificationSettingsDTO {
usePolicy?: boolean;
}
export enum RuletypesRepeatOnDTO {
sunday = 'sunday',
monday = 'monday',
tuesday = 'tuesday',
wednesday = 'wednesday',
thursday = 'thursday',
friday = 'friday',
saturday = 'saturday',
}
export enum RuletypesRepeatTypeDTO {
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export interface RuletypesRecurrenceDTO {
/**
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: Date | null;
/**
* @type array,null
*/
repeatOn?: RuletypesRepeatOnDTO[] | null;
repeatType: RuletypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: Date;
}
export interface RuletypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: Date;
recurrence?: RuletypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: Date;
/**
* @type string
*/
timezone: string;
}
export interface RuletypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: RuletypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: RuletypesScheduleDTO;
status: RuletypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
}
export interface RuletypesPostablePlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
schedule: RuletypesScheduleDTO;
}
export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
@@ -7794,7 +7801,7 @@ export type ListDowntimeSchedules200 = {
/**
* @type array
*/
data: RuletypesPlannedMaintenanceDTO[];
data: AlertmanagertypesPlannedMaintenanceDTO[];
/**
* @type string
*/
@@ -7802,7 +7809,7 @@ export type ListDowntimeSchedules200 = {
};
export type CreateDowntimeSchedule201 = {
data: RuletypesPlannedMaintenanceDTO;
data: AlertmanagertypesPlannedMaintenanceDTO;
/**
* @type string
*/
@@ -7816,7 +7823,7 @@ export type GetDowntimeScheduleByIDPathParameters = {
id: string;
};
export type GetDowntimeScheduleByID200 = {
data: RuletypesPlannedMaintenanceDTO;
data: AlertmanagertypesPlannedMaintenanceDTO;
/**
* @type string
*/

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,7 +3,6 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -144,6 +144,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
loading={loading}
notFoundContent={notFoundContent}
options={options}
optionFilterProp="label"
optionRender={(option): JSX.Element => (
<Checkbox
checked={value.includes(option.value as string)}
@@ -162,6 +163,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
return (
<Select
id={id}
showSearch
value={value || undefined}
onChange={onChange}
placeholder={placeholder}
@@ -170,6 +172,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
loading={loading}
notFoundContent={notFoundContent}
options={options}
optionFilterProp="label"
getPopupContainer={getPopupContainer}
disabled={disabled}
/>

View File

@@ -10,4 +10,5 @@ export enum FeatureKeys {
ONBOARDING_V3 = 'onboarding_v3',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
USE_JSON_BODY = 'use_json_body',
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
}

View File

@@ -5,7 +5,8 @@ import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { BellDot, CircleAlert, ExternalLink, Save } from '@signozhq/icons';
import { Button, FormInstance, Modal, SelectProps } from 'antd';
import { Button, FormInstance, SelectProps } from 'antd';
import { ConfirmDialog } from '@signozhq/ui/dialog';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
@@ -162,6 +163,7 @@ function FormAlertRules({
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false);
useEffect(() => {
if (!isEqual(currentQuery.unit, yAxisUnit)) {
@@ -577,19 +579,16 @@ function FormAlertRules({
});
// invalidate rule in cache
ruleCache.invalidateQueries([
await ruleCache.invalidateQueries([
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
`${ruleId}`,
]);
// eslint-disable-next-line sonarjs/no-identical-functions
setTimeout(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, 2000);
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
} catch (e) {
const apiError = convertToApiError(e as AxiosError<RenderErrorResponseDTO>);
logData = {
@@ -625,24 +624,9 @@ function FormAlertRules({
urlQuery,
]);
const onSaveHandler = useCallback(async () => {
const content = (
<Typography.Text>
{' '}
{t('confirm_save_content_part1')}{' '}
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
{t('confirm_save_content_part2')}
</Typography.Text>
);
Modal.confirm({
icon: <CircleAlert size="md" />,
title: t('confirm_save_title'),
centered: true,
content,
onOk: saveRule,
className: 'create-alert-modal',
});
}, [t, saveRule, currentQuery]);
const onSaveHandler = useCallback(() => {
setIsConfirmSaveOpen(true);
}, []);
const onTestRuleHandler = useCallback(async () => {
if (!isFormValid()) {
@@ -988,6 +972,27 @@ function FormAlertRules({
</ButtonContainer>
</MainFormContainer>
</div>
<ConfirmDialog
open={isConfirmSaveOpen}
onOpenChange={setIsConfirmSaveOpen}
title={t('confirm_save_title')}
titleIcon={<CircleAlert size={14} />}
confirmText="OK"
confirmColor="primary"
onConfirm={async (): Promise<boolean> => {
await saveRule();
return true;
}}
onCancel={() => setIsConfirmSaveOpen(false)}
width="narrow"
>
<Typography.Text>
{t('confirm_save_content_part1')}{' '}
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
{t('confirm_save_content_part2')}
</Typography.Text>
</ConfirmDialog>
</>
);
}

View File

@@ -0,0 +1,36 @@
.actionContent {
display: flex;
flex-direction: column;
}
.actionBtn {
display: flex;
padding: 8px;
height: unset;
align-items: center;
gap: 6px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
:global(.ant-icon-btn) {
margin-inline-end: 0px;
}
}
.deleteBtn {
composes: actionBtn;
color: var(--danger-background) !important;
border-top: 1px solid var(--l1-border);
}
.deleteBtn:hover {
background-color: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
}
.deleteModal :global(.ant-modal-confirm-body) {
align-items: center;
}

View File

@@ -745,52 +745,6 @@
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 0px;
.dashboard-action-content {
.section-1 {
display: flex;
flex-direction: column;
.action-btn {
display: flex;
padding: 8px;
height: unset;
align-items: center;
gap: 6px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
.ant-icon-btn {
margin-inline-end: 0px;
}
}
}
.section-2 {
display: flex;
flex-direction: column;
border-top: 1px solid var(--l1-border);
.ant-typography {
display: flex;
padding: 12px 8px;
align-items: center;
gap: 6px;
color: var(--bg-cherry-400) !important;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
}
}
}
}
}

View File

@@ -102,6 +102,7 @@ import {
filterDashboards,
} from './utils';
import styles from './DashboardActions.module.scss';
import './DashboardList.styles.scss';
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -436,57 +437,53 @@ function DashboardsList(): JSX.Element {
{action && (
<Popover
content={
<div className="dashboard-action-content">
<section className="section-1">
<Button
type="text"
className="action-btn"
icon={<Expand size={12} />}
onClick={onClickHandler}
>
View
</Button>
<Button
type="text"
className="action-btn"
icon={<SquareArrowOutUpRight size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
openInNewTab(getLink());
}}
>
Open in New Tab
</Button>
<Button
type="text"
className="action-btn"
icon={<Link2 size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(getAbsoluteUrl(getLink()));
}}
>
Copy Link
</Button>
<Button
type="text"
className="action-btn"
icon={<FileJson size={12} />}
onClick={handleJsonExport}
>
Export JSON
</Button>
</section>
<section className="section-2">
<DeleteButton
name={dashboard.name}
id={dashboard.id}
isLocked={dashboard.isLocked}
createdBy={dashboard.createdBy}
/>
</section>
<div className={styles.actionContent}>
<Button
type="text"
className={styles.actionBtn}
icon={<Expand size={12} />}
onClick={onClickHandler}
>
View
</Button>
<Button
type="text"
className={styles.actionBtn}
icon={<SquareArrowOutUpRight size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
openInNewTab(getLink());
}}
>
Open in New Tab
</Button>
<Button
type="text"
className={styles.actionBtn}
icon={<Link2 size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(getAbsoluteUrl(getLink()));
}}
>
Copy Link
</Button>
<Button
type="text"
className={styles.actionBtn}
icon={<FileJson size={12} />}
onClick={handleJsonExport}
>
Export JSON
</Button>
<DeleteButton
name={dashboard.name}
id={dashboard.id}
isLocked={dashboard.isLocked}
createdBy={dashboard.createdBy}
/>
</div>
}
placement="bottomRight"

View File

@@ -1,9 +0,0 @@
.delete-modal {
.ant-modal-confirm-body {
align-items: center;
}
}
.delete-btn:hover {
background-color: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
}

View File

@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { CircleAlert, Trash2 } from '@signozhq/icons';
import { Flex, Modal, Tooltip } from 'antd';
import { Button, Modal, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
@@ -12,10 +12,8 @@ import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import styles from '../DashboardActions.module.scss';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
import './DeleteButton.styles.scss';
interface DeleteButtonProps {
createdBy: string;
@@ -85,7 +83,7 @@ export function DeleteButton({
},
},
centered: true,
className: 'delete-modal',
className: styles.deleteModal,
});
}, [
modal,
@@ -109,10 +107,16 @@ export function DeleteButton({
return '';
};
const isDisabled = isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor);
return (
<>
<Tooltip placement="left" title={getDeleteTooltipContent()}>
<TableLinkText
<Button
type="text"
className={styles.deleteBtn}
icon={<Trash2 size={12} />}
disabled={isDisabled}
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
@@ -120,13 +124,9 @@ export function DeleteButton({
openConfirmationDialog();
}
}}
className="delete-btn"
disabled={isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor)}
>
<Flex align="center" justify="center" gap={4}>
<Trash2 size={14} /> Delete dashboard
</Flex>
</TableLinkText>
Delete Dashboard
</Button>
</Tooltip>
{contextHolder}

View File

@@ -1,8 +0,0 @@
import styled from 'styled-components';
export const TableLinkText = styled.span<{ disabled: boolean }>`
color: var(--destructive);
cursor: ${({ disabled }): string => (disabled ? 'not-allowed' : 'pointer')};
${({ disabled }): string => (disabled ? 'opacity: 0.5;' : '')}
padding: var(--spacing-3) var(--spacing-4);
`;

View File

@@ -1,10 +1,11 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Input } from '@signozhq/ui/input';
import { Input as AntdInput } from 'antd';
import logEvent from 'api/common/logEvent';
import { ArrowRight } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import { OnboardingQuestionHeader } from '../OnboardingQuestionHeader';
@@ -32,11 +33,31 @@ const interestedInOptions: Record<string, string> = {
openSourceTooling: 'Prefer open-source tooling',
};
function seededShuffle<T>(array: T[], seed: string): T[] {
const result = [...array];
let num = 0;
for (let i = 0; i < seed.length; i++) {
num = Math.imul(num + seed.charCodeAt(i), 2654435761);
num = Math.abs(num);
}
for (let i = result.length - 1; i > 0; i--) {
num = Math.abs(Math.imul(num, 1664525) + 1013904223);
const j = num % (i + 1);
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
export function AboutSigNozQuestions({
signozDetails,
setSignozDetails,
onNext,
}: AboutSigNozQuestionsProps): JSX.Element {
const { versionData } = useAppContext();
const [interestInSignoz, setInterestInSignoz] = useState<string[]>(
signozDetails?.interestInSignoz || [],
);
@@ -48,6 +69,12 @@ export function AboutSigNozQuestions({
);
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
const shuffledOptionKeys = useMemo(
() =>
seededShuffle(Object.keys(interestedInOptions), versionData?.version ?? ''),
[versionData?.version],
);
useEffect((): void => {
if (
discoverSignoz !== '' &&
@@ -115,7 +142,7 @@ export function AboutSigNozQuestions({
<div className="form-group">
<div className="question">What got you interested in SigNoz?</div>
<div className="checkbox-grid">
{Object.keys(interestedInOptions).map((option: string) => (
{shuffledOptionKeys.map((option: string) => (
<div key={option} className="checkbox-item">
<Checkbox
id={`checkbox-${option}`}

View File

@@ -9,7 +9,7 @@ import {
useListDowntimeSchedules,
} from 'api/generated/services/downtimeschedules';
import { useListRules } from 'api/generated/services/rules';
import type { RuletypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
import type { AlertmanagertypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
import dayjs from 'dayjs';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
@@ -48,9 +48,9 @@ export function PlannedDowntime(): JSX.Element {
const urlQuery = useUrlQuery();
const [initialValues, setInitialValues] =
useState<Partial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>>(
defautlInitialValues,
);
useState<
Partial<AlertmanagertypesPlannedMaintenanceDTO & { editMode: boolean }>
>(defautlInitialValues);
const downtimeSchedules = useListDowntimeSchedules();
const alertOptions = React.useMemo(

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Check } from '@signozhq/icons';
import { Check, Info } from '@signozhq/icons';
import {
Button,
DatePicker,
@@ -11,6 +11,7 @@ import {
Select,
SelectProps,
Spin,
Tooltip,
} from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { DefaultOptionType } from 'antd/es/select';
@@ -20,9 +21,9 @@ import {
updateDowntimeScheduleByID,
} from 'api/generated/services/downtimeschedules';
import type {
RuletypesPlannedMaintenanceDTO,
RuletypesPostablePlannedMaintenanceDTO,
RuletypesRecurrenceDTO,
AlertmanagertypesPlannedMaintenanceDTO,
AlertmanagertypesPostablePlannedMaintenanceDTO,
AlertmanagertypesRecurrenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
@@ -68,17 +69,18 @@ interface PlannedDowntimeFormData {
name: string;
startTime: dayjs.Dayjs | string;
endTime: dayjs.Dayjs | string;
recurrence?: RuletypesRecurrenceDTO | null;
recurrence?: AlertmanagertypesRecurrenceDTO | null;
alertRules: DefaultOptionType[];
recurrenceSelect?: RuletypesRecurrenceDTO;
recurrenceSelect?: AlertmanagertypesRecurrenceDTO;
timezone?: string;
labelExpression?: string;
}
const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
interface PlannedDowntimeFormProps {
initialValues: Partial<
RuletypesPlannedMaintenanceDTO & {
AlertmanagertypesPlannedMaintenanceDTO & {
editMode: boolean;
}
>;
@@ -141,11 +143,12 @@ export function PlannedDowntimeForm(
const saveHanlder = useCallback(
async (values: PlannedDowntimeFormData) => {
const shouldKeepLocalTime = !isEditMode;
const data: RuletypesPostablePlannedMaintenanceDTO = {
const data: AlertmanagertypesPostablePlannedMaintenanceDTO = {
alertIds: values.alertRules
.map((alert) => alert.value)
.filter((alert) => alert !== undefined) as string[],
name: values.name,
labelExpression: values.labelExpression,
schedule: {
startTime: new Date(
handleTimeConversion(
@@ -166,7 +169,7 @@ export function PlannedDowntimeForm(
),
)
: undefined,
recurrence: values.recurrence as RuletypesRecurrenceDTO,
recurrence: values.recurrence as AlertmanagertypesRecurrenceDTO,
},
};
@@ -232,7 +235,7 @@ export function PlannedDowntimeForm(
const payloadValues = {
...values,
recurrence: recurrenceData as RuletypesRecurrenceDTO | undefined,
recurrence: recurrenceData as AlertmanagertypesRecurrenceDTO | undefined,
};
await saveHanlder(payloadValues);
};
@@ -290,13 +293,14 @@ export function PlannedDowntimeForm(
repeatType: (!isScheduleRecurring(initialValues?.schedule)
? recurrenceOptions.doesNotRepeat.value
: initialValues.schedule?.recurrence
?.repeatType) as RuletypesRecurrenceDTO['repeatType'],
?.repeatType) as AlertmanagertypesRecurrenceDTO['repeatType'],
duration: String(
getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
?.value ?? '',
),
} as RuletypesRecurrenceDTO,
} as AlertmanagertypesRecurrenceDTO,
timezone: initialValues.schedule?.timezone as string,
labelExpression: initialValues.labelExpression || '',
};
return formData;
}, [initialValues, alertOptions]);
@@ -608,6 +612,22 @@ export function PlannedDowntimeForm(
</Select>
</Form.Item>
</div>
<Form.Item
label={
<span>
Label Expression&nbsp;
<Tooltip title='Filter by alert labels. Examples: env == "prod", region == "us-east-1" && severity == "critical"'>
<Info size={13} />
</Tooltip>
</span>
}
name="labelExpression"
>
<Input.TextArea
placeholder='e.g. env == "prod" && region == "us-east-1"'
autoSize={{ minRows: 2, maxRows: 4 }}
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<ModalButtonWrapper>
<Button

View File

@@ -7,8 +7,8 @@ import type { DefaultOptionType } from 'antd/es/select';
import type {
ListDowntimeSchedules200,
RenderErrorResponseDTO,
RuletypesPlannedMaintenanceDTO,
RuletypesRecurrenceDTO,
AlertmanagertypesPlannedMaintenanceDTO,
AlertmanagertypesRecurrenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import cx from 'classnames';
@@ -137,7 +137,7 @@ export function CollapseListContent({
created_by_name?: string;
created_by_email?: string;
timeframe: [string | undefined | null, string | undefined | null];
repeats?: RuletypesRecurrenceDTO | null;
repeats?: AlertmanagertypesRecurrenceDTO | null;
updated_at?: string;
updated_by_name?: string;
alertOptions?: DefaultOptionType[];
@@ -212,7 +212,7 @@ export function CollapseListContent({
export function CustomCollapseList(
props: DowntimeSchedulesTableData & {
setInitialValues: React.Dispatch<
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
React.SetStateAction<Partial<AlertmanagertypesPlannedMaintenanceDTO>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: string, name: string) => void;
@@ -247,7 +247,7 @@ export function CustomCollapseList(
const endTime = getEndTime({
kind,
schedule,
} as Partial<RuletypesPlannedMaintenanceDTO>);
} as Partial<AlertmanagertypesPlannedMaintenanceDTO>);
return (
<>
@@ -284,7 +284,7 @@ export function CustomCollapseList(
typeof endTime === 'string' ? endTime : endTime?.toString(),
]}
repeats={
schedule?.recurrence as RuletypesRecurrenceDTO | null | undefined
schedule?.recurrence as AlertmanagertypesRecurrenceDTO | null | undefined
}
updated_at={updatedAt ? dayjs(updatedAt).toISOString() : ''}
updated_by_name={defaultTo(updatedBy, '')}
@@ -301,9 +301,10 @@ export function CustomCollapseList(
);
}
export type DowntimeSchedulesTableData = RuletypesPlannedMaintenanceDTO & {
alertOptions: DefaultOptionType[];
};
export type DowntimeSchedulesTableData =
AlertmanagertypesPlannedMaintenanceDTO & {
alertOptions: DefaultOptionType[];
};
export function PlannedDowntimeList({
downtimeSchedules,
@@ -320,7 +321,7 @@ export function PlannedDowntimeList({
>;
alertOptions: DefaultOptionType[];
setInitialValues: React.Dispatch<
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
React.SetStateAction<Partial<AlertmanagertypesPlannedMaintenanceDTO>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: string, name: string) => void;

View File

@@ -5,8 +5,8 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import type {
DeleteDowntimeScheduleByIDPathParameters,
RenderErrorResponseDTO,
RuletypesPlannedMaintenanceDTO,
RuletypesRecurrenceDTO,
AlertmanagertypesPlannedMaintenanceDTO,
AlertmanagertypesRecurrenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import { AxiosError } from 'axios';
@@ -60,7 +60,7 @@ export const getAlertOptionsFromIds = (
);
export const recurrenceInfo = (
recurrence?: RuletypesRecurrenceDTO | null,
recurrence?: AlertmanagertypesRecurrenceDTO | null,
): string => {
if (!recurrence) {
return 'No';
@@ -81,7 +81,7 @@ export const recurrenceInfo = (
};
export const defautlInitialValues: Partial<
RuletypesPlannedMaintenanceDTO & { editMode: boolean }
AlertmanagertypesPlannedMaintenanceDTO & { editMode: boolean }
> = {
name: '',
description: '',
@@ -228,7 +228,7 @@ export const getEndTime = ({
kind,
schedule,
}: Partial<
RuletypesPlannedMaintenanceDTO & {
AlertmanagertypesPlannedMaintenanceDTO & {
editMode: boolean;
}
>): string | dayjs.Dayjs => {
@@ -242,7 +242,7 @@ export const getEndTime = ({
};
export const isScheduleRecurring = (
schedule?: RuletypesPlannedMaintenanceDTO['schedule'] | null,
schedule?: AlertmanagertypesPlannedMaintenanceDTO['schedule'] | null,
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);
function convertUtcOffsetToTimezoneOffset(offsetMinutes: number): string {

View File

@@ -1,15 +1,15 @@
import type {
RuletypesPlannedMaintenanceDTO,
RuletypesScheduleDTO,
AlertmanagertypesScheduleDTO,
AlertmanagertypesPlannedMaintenanceDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
RuletypesMaintenanceKindDTO,
RuletypesMaintenanceStatusDTO,
AlertmanagertypesMaintenanceKindDTO,
AlertmanagertypesMaintenanceStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
export const buildSchedule = (
schedule: Partial<RuletypesScheduleDTO>,
): RuletypesScheduleDTO => ({
schedule: Partial<AlertmanagertypesScheduleDTO>,
): AlertmanagertypesScheduleDTO => ({
timezone: schedule?.timezone ?? '',
startTime: schedule?.startTime,
endTime: schedule?.endTime,
@@ -17,8 +17,8 @@ export const buildSchedule = (
});
export const createMockDowntime = (
overrides: Partial<RuletypesPlannedMaintenanceDTO>,
): RuletypesPlannedMaintenanceDTO => ({
overrides: Partial<AlertmanagertypesPlannedMaintenanceDTO>,
): AlertmanagertypesPlannedMaintenanceDTO => ({
id: overrides.id ?? '0',
name: overrides.name ?? '',
description: overrides.description ?? '',
@@ -32,6 +32,6 @@ export const createMockDowntime = (
createdBy: overrides.createdBy ?? '',
updatedAt: overrides.updatedAt,
updatedBy: overrides.updatedBy ?? '',
kind: overrides.kind ?? RuletypesMaintenanceKindDTO.recurring,
status: overrides.status ?? RuletypesMaintenanceStatusDTO.active,
kind: overrides.kind ?? AlertmanagertypesMaintenanceKindDTO.recurring,
status: overrides.status ?? AlertmanagertypesMaintenanceStatusDTO.active,
});

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useHistory, useLocation } from 'react-router-dom';
import { Redirect, useHistory, useLocation } from 'react-router-dom';
import { Trash2 } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { toast } from '@signozhq/ui/sonner';
@@ -21,6 +21,7 @@ import {
buildRoleUpdatePermission,
} from 'hooks/useAuthZ/permissions/role.permissions';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
import type { AuthzResources } from '../utils';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
@@ -52,8 +53,10 @@ function RoleDetailsPage(): JSX.Element {
const queryClient = useQueryClient();
const { showErrorModal } = useErrorModal();
const { isRolesEnabled, isLoading: isRolesGateLoading } =
useRolesFeatureGate();
const authzResources = permissionsType.data as unknown as AuthzResources;
const authzResources: AuthzResources = permissionsType.data;
// Extract roleId from URL pathname since useParams doesn't work in nested routing
const roleIdMatch = pathname.match(ROLE_ID_REGEX);
@@ -158,6 +161,22 @@ function RoleDetailsPage(): JSX.Element {
},
});
if (isRolesGateLoading) {
return (
<div className="role-details-page">
<Skeleton
active
paragraph={{ rows: 8 }}
className="role-details-skeleton"
/>
</div>
);
}
if (!isRolesEnabled) {
return <Redirect to={ROUTES.ROLES_SETTINGS} />;
}
if (!hasReadPermission && readPerms !== null) {
return <PermissionDeniedFullPage permissionName="role:read" />;
}

View File

@@ -5,7 +5,9 @@ import {
} from 'mocks-server/__mockdata__/roles';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { Route, Switch } from 'react-router-dom';
import {
defaultFeatureFlags,
fireEvent,
render,
screen,
@@ -13,8 +15,10 @@ import {
waitFor,
within,
} from 'tests/test-utils';
import { FeatureKeys } from 'constants/features';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import {
invalidLicense,
mockUseAuthZDenyAll,
mockUseAuthZGrantAll,
} from 'tests/authz-test-utils';
@@ -230,6 +234,56 @@ describe('RoleDetailsPage', () => {
).resolves.toBeInTheDocument();
});
it('redirects to the roles list when license is not valid', async () => {
render(
<Switch>
<Route path="/settings/roles/:roleId">
<RoleDetailsPage />
</Route>
<Route path="/settings/roles" exact>
<div data-testid="roles-list-redirect-target" />
</Route>
</Switch>,
undefined,
{
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
appContextOverrides: { activeLicense: invalidLicense },
},
);
await expect(
screen.findByTestId('roles-list-redirect-target'),
).resolves.toBeInTheDocument();
});
it('redirects to the roles list when fine-grained authz flag is inactive', async () => {
render(
<Switch>
<Route path="/settings/roles/:roleId">
<RoleDetailsPage />
</Route>
<Route path="/settings/roles" exact>
<div data-testid="roles-list-redirect-target" />
</Route>
</Switch>,
undefined,
{
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
appContextOverrides: {
featureFlags: defaultFeatureFlags.map((f) =>
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
? { ...f, active: false }
: f,
),
},
},
);
await expect(
screen.findByTestId('roles-list-redirect-target'),
).resolves.toBeInTheDocument();
});
describe('permission side panel', () => {
beforeEach(() => {
// Both hooks mocked so data renders synchronously — no React Query scheduler or MSW round-trip.

View File

@@ -9,6 +9,7 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import { RoleListPermission } from 'hooks/useAuthZ/permissions/role.permissions';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
import useUrlQuery from 'hooks/useUrlQuery';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { useTimezone } from 'providers/Timezone';
@@ -30,6 +31,8 @@ interface RolesListingTableProps {
function RolesListingTable({
searchQuery,
}: RolesListingTableProps): JSX.Element {
const { isRolesEnabled } = useRolesFeatureGate();
const { permissions: listPerms, isLoading: isAuthZLoading } = useAuthZ([
RoleListPermission,
]);
@@ -203,19 +206,27 @@ function RolesListingTable({
const renderRow = (role: AuthtypesRoleDTO): JSX.Element => (
<div
key={role.id}
className="roles-table-row roles-table-row--clickable"
role="button"
tabIndex={0}
onClick={(): void => {
if (role.id) {
navigateToRole(role.id, role.name);
}
}}
onKeyDown={(e): void => {
if ((e.key === 'Enter' || e.key === ' ') && role.id) {
navigateToRole(role.id, role.name);
}
}}
className={`roles-table-row${isRolesEnabled ? ' roles-table-row--clickable' : ''}`}
role={isRolesEnabled ? 'button' : undefined}
tabIndex={isRolesEnabled ? 0 : undefined}
onClick={
isRolesEnabled
? (): void => {
if (role.id) {
navigateToRole(role.id, role.name);
}
}
: undefined
}
onKeyDown={
isRolesEnabled
? (e): void => {
if ((e.key === 'Enter' || e.key === ' ') && role.id) {
navigateToRole(role.id, role.name);
}
}
: undefined
}
>
<div className="roles-table-cell roles-table-cell--name">
{role.name ?? '—'}

View File

@@ -4,6 +4,7 @@ import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import { RoleCreatePermission } from 'hooks/useAuthZ/permissions/role.permissions';
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
import CreateRoleModal from './RolesComponents/CreateRoleModal';
import RolesListingTable from './RolesComponents/RolesListingTable';
@@ -13,6 +14,7 @@ import './RolesSettings.styles.scss';
function RolesSettings(): JSX.Element {
const [searchQuery, setSearchQuery] = useState('');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const { isRolesEnabled } = useRolesFeatureGate();
return (
<div className="roles-settings" data-testid="roles-settings">
@@ -38,17 +40,19 @@ function RolesSettings(): JSX.Element {
value={searchQuery}
onChange={(e): void => setSearchQuery(e.target.value)}
/>
<AuthZTooltip checks={[RoleCreatePermission]}>
<Button
variant="solid"
color="primary"
className="role-settings-toolbar-button"
onClick={(): void => setIsCreateModalOpen(true)}
>
<Plus size={14} />
Custom role
</Button>
</AuthZTooltip>
{isRolesEnabled && (
<AuthZTooltip checks={[RoleCreatePermission]}>
<Button
variant="solid"
color="primary"
className="role-settings-toolbar-button"
onClick={(): void => setIsCreateModalOpen(true)}
>
<Plus size={14} />
Custom role
</Button>
</AuthZTooltip>
)}
</div>
<RolesListingTable searchQuery={searchQuery} />
</div>

View File

@@ -4,9 +4,15 @@ import {
} from 'mocks-server/__mockdata__/roles';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { fireEvent, render, screen } from 'tests/test-utils';
import {
defaultFeatureFlags,
fireEvent,
render,
screen,
} from 'tests/test-utils';
import { FeatureKeys } from 'constants/features';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
import { invalidLicense, mockUseAuthZGrantAll } from 'tests/authz-test-utils';
import RolesSettings from '../RolesSettings';
@@ -176,6 +182,50 @@ describe('RolesSettings', () => {
}
});
it('hides the create button and disables row clicks when fine-grained authz flag is inactive', async () => {
render(<RolesSettings />, undefined, {
appContextOverrides: {
featureFlags: defaultFeatureFlags.map((f) =>
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
? { ...f, active: false }
: f,
),
},
});
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /custom role/i }),
).not.toBeInTheDocument();
const rows = document.querySelectorAll('.roles-table-row');
rows.forEach((row) => {
expect(row).not.toHaveClass('roles-table-row--clickable');
expect(row.getAttribute('role')).not.toBe('button');
});
});
it('hides the create button and disables row clicks when license is not valid', async () => {
render(<RolesSettings />, undefined, {
appContextOverrides: { activeLicense: invalidLicense },
});
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
// Create button must be absent
expect(
screen.queryByRole('button', { name: /custom role/i }),
).not.toBeInTheDocument();
// Rows must not carry the clickable class or button role
const rows = document.querySelectorAll('.roles-table-row');
rows.forEach((row) => {
expect(row).not.toHaveClass('roles-table-row--clickable');
expect(row.getAttribute('role')).not.toBe('button');
});
});
it('handles invalid dates gracefully by showing fallback', async () => {
const invalidRole = {
id: 'edge-0009',

View File

@@ -1,5 +1,4 @@
import type {
CoretypesResourceRefDTO,
CoretypesObjectGroupDTO,
CoretypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
@@ -8,11 +7,7 @@ import type {
PermissionConfig,
ResourceDefinition,
} from '../PermissionSidePanel/PermissionSidePanel.types';
type AuthzResources = {
resources: CoretypesResourceRefDTO[];
relations: Record<string, string[]>;
};
import type { AuthzResources } from '../utils';
import { PermissionScope } from '../PermissionSidePanel/PermissionSidePanel.types';
import {
buildConfig,
@@ -41,12 +36,14 @@ jest.mock('../RoleDetails/constants', () => {
const dashboardResource: AuthzResources['resources'][number] = {
kind: 'dashboard',
type: 'metaresource' as CoretypesTypeDTO,
type: 'metaresource',
allowedVerbs: ['create', 'read', 'update', 'delete', 'list'],
};
const alertResource: AuthzResources['resources'][number] = {
kind: 'alert',
type: 'metaresource' as CoretypesTypeDTO,
type: 'metaresource',
allowedVerbs: ['create', 'read', 'update', 'delete', 'list'],
};
const baseAuthzResources: AuthzResources = {
@@ -57,6 +54,16 @@ const baseAuthzResources: AuthzResources = {
},
};
// API payload resource refs — only kind+type, no allowedVerbs (matches CoretypesResourceRefDTO shape)
const dashboardResourceRef = {
kind: 'dashboard',
type: 'metaresource' as CoretypesTypeDTO,
};
const alertResourceRef = {
kind: 'alert',
type: 'metaresource' as CoretypesTypeDTO,
};
const resourceDefs: ResourceDefinition[] = [
{
id: 'metaresource:dashboard',
@@ -107,7 +114,7 @@ describe('buildPatchPayload', () => {
});
expect(result.additions).toStrictEqual([
{ resource: dashboardResource, selectors: [ID_B] },
{ resource: dashboardResourceRef, selectors: [ID_B] },
]);
expect(result.deletions).toBeNull();
});
@@ -142,7 +149,7 @@ describe('buildPatchPayload', () => {
});
expect(result.deletions).toStrictEqual([
{ resource: dashboardResource, selectors: [ID_B] },
{ resource: dashboardResourceRef, selectors: [ID_B] },
]);
expect(result.additions).toBeNull();
});
@@ -207,10 +214,10 @@ describe('buildPatchPayload', () => {
});
expect(result.deletions).toStrictEqual([
{ resource: dashboardResource, selectors: ['*'] },
{ resource: dashboardResourceRef, selectors: ['*'] },
]);
expect(result.additions).toStrictEqual([
{ resource: dashboardResource, selectors: [ID_A, ID_B] },
{ resource: dashboardResourceRef, selectors: [ID_A, ID_B] },
]);
});
@@ -241,7 +248,7 @@ describe('buildPatchPayload', () => {
});
expect(result.deletions).toStrictEqual([
{ resource: dashboardResource, selectors: ['*'] },
{ resource: dashboardResourceRef, selectors: ['*'] },
]);
expect(result.additions).toBeNull();
});
@@ -264,7 +271,7 @@ describe('buildPatchPayload', () => {
});
expect(result.deletions).toStrictEqual([
{ resource: dashboardResource, selectors: ['*'] },
{ resource: dashboardResourceRef, selectors: ['*'] },
]);
expect(result.additions).toBeNull();
});
@@ -287,7 +294,7 @@ describe('buildPatchPayload', () => {
});
expect(result.additions).toStrictEqual([
{ resource: dashboardResource, selectors: ['*'] },
{ resource: dashboardResourceRef, selectors: ['*'] },
]);
expect(result.deletions).toBeNull();
});
@@ -313,7 +320,7 @@ describe('buildPatchPayload', () => {
});
expect(result.deletions).toStrictEqual([
{ resource: dashboardResource, selectors: [ID_A, ID_B] },
{ resource: dashboardResourceRef, selectors: [ID_A, ID_B] },
]);
expect(result.additions).toBeNull();
});
@@ -339,7 +346,7 @@ describe('buildPatchPayload', () => {
});
expect(result.additions).toStrictEqual([
{ resource: dashboardResource, selectors: [ID_A] },
{ resource: dashboardResourceRef, selectors: [ID_A] },
]);
expect(result.deletions).toBeNull();
});
@@ -385,7 +392,7 @@ describe('buildPatchPayload', () => {
});
expect(result.additions).toStrictEqual([
{ resource: alertResource, selectors: [ID_B] },
{ resource: alertResourceRef, selectors: [ID_B] },
]);
expect(result.deletions).toBeNull();
});
@@ -394,7 +401,7 @@ describe('buildPatchPayload', () => {
describe('objectsToPermissionConfig', () => {
it('maps a wildcard selector to ALL scope', () => {
const objects: CoretypesObjectGroupDTO[] = [
{ resource: dashboardResource, selectors: ['*'] },
{ resource: dashboardResourceRef, selectors: ['*'] },
];
const result = objectsToPermissionConfig(objects, resourceDefs);
@@ -407,7 +414,7 @@ describe('objectsToPermissionConfig', () => {
it('maps specific selectors to ONLY_SELECTED scope with the IDs', () => {
const objects: CoretypesObjectGroupDTO[] = [
{ resource: dashboardResource, selectors: [ID_A, ID_B] },
{ resource: dashboardResourceRef, selectors: [ID_A, ID_B] },
];
const result = objectsToPermissionConfig(objects, resourceDefs);
@@ -566,4 +573,41 @@ describe('deriveResourcesForRelation', () => {
deriveResourcesForRelation(baseAuthzResources, 'nonexistent'),
).toHaveLength(0);
});
describe('allowedVerbs filtering', () => {
it('excludes resources whose allowedVerbs does not include the relation', () => {
const authz: AuthzResources = {
resources: [
{
kind: 'dashboard',
type: 'metaresource',
allowedVerbs: ['create', 'read', 'update', 'delete', 'list'],
},
{
kind: 'alert',
type: 'metaresource',
allowedVerbs: ['create', 'read', 'update', 'delete', 'list', 'attach'],
},
],
relations: { attach: ['metaresource'] },
};
const result = deriveResourcesForRelation(authz, 'attach');
expect(result).toHaveLength(1);
expect(result[0].id).toBe('metaresource:alert');
});
it('requires both type-relation match and allowedVerbs — neither condition alone is sufficient', () => {
const authz: AuthzResources = {
resources: [
{ kind: 'dashboard', type: 'metaresource', allowedVerbs: ['read'] },
{ kind: 'role', type: 'role', allowedVerbs: ['create'] },
],
relations: { create: ['metaresource'] },
};
expect(deriveResourcesForRelation(authz, 'create')).toHaveLength(0);
});
});
});

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Badge } from '@signozhq/ui/badge';
import type {
CoretypesResourceRefDTO,
CoretypesObjectGroupDTO,
CoretypesResourceRefDTO,
CoretypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { capitalize } from 'lodash-es';
@@ -21,7 +22,11 @@ import {
} from './RoleDetails/constants';
export type AuthzResources = {
resources: ReadonlyArray<CoretypesResourceRefDTO>;
resources: ReadonlyArray<{
kind: string;
type: string;
allowedVerbs: readonly string[];
}>;
relations: Readonly<Record<string, ReadonlyArray<string>>>;
};
@@ -69,7 +74,9 @@ export function deriveResourcesForRelation(
}
const supportedTypes = authzResources.relations[relation] ?? [];
return authzResources.resources
.filter((r) => supportedTypes.includes(r.type))
.filter(
(r) => supportedTypes.includes(r.type) && r.allowedVerbs.includes(relation),
)
.map((r) => ({
id: `${r.type}:${r.kind}`,
kind: r.kind,
@@ -141,7 +148,7 @@ export function buildPatchPayload({
}
const resourceDef: CoretypesResourceRefDTO = {
kind: found.kind,
type: found.type,
type: found.type as CoretypesTypeDTO,
};
const initialScope = initial?.scope ?? PermissionScope.NONE;

View File

@@ -0,0 +1,27 @@
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
export const useRolesFeatureGate = (): {
isRolesEnabled: boolean;
isLoading: boolean;
} => {
const {
activeLicense,
featureFlags,
isFetchingActiveLicense,
isFetchingFeatureFlags,
} = useAppContext();
const isValidLicense = activeLicense?.status === LicenseStatus.VALID;
const isFineGrainedAuthzEnabled =
featureFlags?.find((f) => f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ)
?.active ?? false;
return {
isRolesEnabled: isValidLicense && isFineGrainedAuthzEnabled,
isLoading:
(isFetchingActiveLicense && !activeLicense) ||
(isFetchingFeatureFlags && !featureFlags),
};
};

View File

@@ -189,7 +189,7 @@ describe('Tooltip utils', () => {
];
}
it('builds tooltip content in series-index order with isActive flag set correctly', () => {
it('builds tooltip content sorted by value descending with isActive flag set correctly', () => {
const data: AlignedData = [[0], [10], [20], [30]];
const series = createSeriesConfig();
const dataIndexes = [null, 0, 0, 0];
@@ -206,21 +206,21 @@ describe('Tooltip utils', () => {
});
expect(result).toHaveLength(2);
// Series are returned in series-index order (A=index 1 before B=index 2)
// Sorted by value descending: B (20) before A (10)
expect(result[0]).toMatchObject<Partial<TooltipContentItem>>({
label: 'A',
value: 10,
tooltipValue: 'formatted-10',
color: '#ff0000',
isActive: false,
});
expect(result[1]).toMatchObject<Partial<TooltipContentItem>>({
label: 'B',
value: 20,
tooltipValue: 'formatted-20',
color: 'color-2',
isActive: true,
});
expect(result[1]).toMatchObject<Partial<TooltipContentItem>>({
label: 'A',
value: 10,
tooltipValue: 'formatted-10',
color: '#ff0000',
isActive: false,
});
});
it('skips series with null data index or non-finite values', () => {
@@ -274,7 +274,7 @@ describe('Tooltip utils', () => {
expect(result[1].value).toBe(30);
});
it('returns items in series-index order', () => {
it('returns items sorted by value descending', () => {
// Series values in non-sorted order: 3, 1, 4, 2
const data: AlignedData = [[0], [3], [1], [4], [2]];
const series: Series[] = [
@@ -297,7 +297,7 @@ describe('Tooltip utils', () => {
decimalPrecision,
});
expect(result.map((item) => item.value)).toStrictEqual([3, 1, 4, 2]);
expect(result.map((item) => item.value)).toStrictEqual([4, 3, 2, 1]);
});
});
});

View File

@@ -142,5 +142,7 @@ export function buildTooltipContent({
}
}
items.sort((a, b) => b.value - a.value);
return items;
}

View File

@@ -473,6 +473,7 @@ export const SpanDuration = memo(function SpanDuration({
const columnDefHelper = createColumnHelper<SpanV3>();
const ROW_HEIGHT = 28;
const WATERFALL_BOTTOM_PADDING = 24;
const DEFAULT_SIDEBAR_WIDTH = 450;
const MIN_SIDEBAR_WIDTH = 240;
const MAX_SIDEBAR_WIDTH = 900;
@@ -740,53 +741,69 @@ function Success(props: ISuccessProps): JSX.Element {
);
}, [spans, sidebarWidth]);
// Scroll to the interested span only when it isn't already on screen.
// Covers every entry point uniformly: deep-link, flamegraph click,
// filter prev/next, browser back/forward all scroll only if needed;
// waterfall row clicks and chevron expand/collapse don't yank the viewport
// because the affected row is by definition already visible.
// Scroll a span to viewport center if it isn't already visible. Shared by
// the two effects below — one keyed on interestedSpanId (chevron, boundary
// pagination, deep-link to unloaded), the other on selectedSpan (in-window
// URL navigation that doesn't mutate interestedSpanId).
const scrollSpanIntoView = useCallback(
(span: SpanV3, spansList: SpanV3[]): void => {
if (!virtualizerRef.current) {
return;
}
const idx = spansList.findIndex((s) => s.span_id === span.span_id);
if (idx === -1) {
return;
}
const scrollEl = scrollContainerRef.current;
const scrollTop = scrollEl?.scrollTop ?? 0;
const viewportHeight = scrollEl?.clientHeight ?? 0;
const viewportStartIdx = Math.floor(scrollTop / ROW_HEIGHT);
const viewportEndIdx =
Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) - 1;
const isOnScreen =
viewportHeight > 0 && idx >= viewportStartIdx && idx <= viewportEndIdx;
if (isOnScreen) {
return;
}
setTimeout(() => {
virtualizerRef.current?.scrollToIndex(idx, {
align: 'center',
behavior: 'auto',
});
const sidebarScrollEl = scrollContainerRef.current?.querySelector(
'.resizable-box__content',
);
if (sidebarScrollEl) {
const targetScrollLeft = Math.max(0, span.level * CONNECTOR_WIDTH - 40);
(sidebarScrollEl as HTMLElement).scrollLeft = targetScrollLeft;
}
}, 100);
},
[],
);
useEffect(() => {
if (interestedSpanId.spanId !== '' && virtualizerRef.current) {
if (interestedSpanId.spanId !== '') {
const idx = spans.findIndex(
(span) => span.span_id === interestedSpanId.spanId,
);
if (idx !== -1) {
const visible = virtualizerRef.current.getVirtualItems();
const isOnScreen =
visible.length > 0 &&
idx >= visible[0].index &&
idx <= visible[visible.length - 1].index;
if (!isOnScreen) {
setTimeout(() => {
virtualizerRef.current?.scrollToIndex(idx, {
align: 'center',
behavior: 'auto',
});
// Auto-scroll sidebar horizontally to show the span name
const span = spans[idx];
const sidebarScrollEl = scrollContainerRef.current?.querySelector(
'.resizable-box__content',
);
if (sidebarScrollEl) {
const targetScrollLeft = Math.max(0, span.level * CONNECTOR_WIDTH - 40);
sidebarScrollEl.scrollLeft = targetScrollLeft;
}
}, 400);
}
scrollSpanIntoView(spans[idx], spans);
setSelectedSpan(spans[idx]);
}
} else {
setSelectedSpan((prev) => {
if (!prev) {
return spans[0];
}
return prev;
});
setSelectedSpan((prev) => prev ?? spans[0]);
}
}, [interestedSpanId, setSelectedSpan, spans]);
}, [interestedSpanId, setSelectedSpan, spans, scrollSpanIntoView]);
// Covers URL-driven navigation to an already-loaded span (flamegraph /
// filter / browser back) that the interestedSpanId-keyed effect doesn't see.
useEffect(() => {
if (selectedSpan) {
scrollSpanIntoView(selectedSpan, spans);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSpan, scrollSpanIntoView]);
const virtualItems = virtualizer.getVirtualItems();
const leftRows = leftTable.getRowModel().rows;
@@ -846,7 +863,7 @@ function Success(props: ISuccessProps): JSX.Element {
<div
className={styles.splitBody}
style={{
minHeight: virtualizer.getTotalSize(),
minHeight: virtualizer.getTotalSize() + WATERFALL_BOTTOM_PADDING,
height: '100%',
}}
>

View File

@@ -74,17 +74,21 @@ function TraceDetailsV3(): JSX.Element {
onClose: handleSpanDetailsClose,
});
const allSpansRef = useRef<SpanV3[]>([]);
// Refetch only when the URL target isn't already loaded. Keeps row clicks
// and other in-window URL navigation from triggering a backend window slide.
useEffect(() => {
const spanId = urlQuery.get('spanId') || '';
// Only update interestedSpanId when a new span is selected,
// not when it's cleared (panel close) — avoids unnecessary API refetch
if (!spanId) {
return;
}
setInterestedSpanId({
spanId,
isUncollapsed: true,
});
const idx = allSpansRef.current.findIndex((s) => s.span_id === spanId);
if (idx !== -1) {
setSelectedSpan(allSpansRef.current[idx]);
return;
}
setInterestedSpanId({ spanId, isUncollapsed: true });
}, [urlQuery]);
// Hardcoded for now — fetch aggregations for all 3 candidate color-by fields
@@ -145,6 +149,10 @@ function TraceDetailsV3(): JSX.Element {
};
}
useEffect(() => {
allSpansRef.current = allSpans;
}, [allSpans]);
// Frontend mode: expand all parents by default when full data arrives
useEffect(() => {
if (isFullDataLoaded && allSpans.length > 0) {

View File

@@ -11,6 +11,13 @@ import type {
} from 'hooks/useAuthZ/types';
import { rest } from 'msw';
import type { RestHandler } from 'msw';
import {
LicenseEvent,
LicensePlatform,
type LicenseResModel,
LicenseState,
LicenseStatus,
} from 'types/api/licensesV3/getActive';
export const AUTHZ_CHECK_URL = `${ENVIRONMENT.baseURL || ''}/api/v1/authz/check`;
@@ -97,6 +104,40 @@ export function setupAuthzAllow(
});
}
export function buildLicense(
overrides?: Partial<LicenseResModel>,
): LicenseResModel {
return {
key: 'test-key',
status: LicenseStatus.VALID,
state: LicenseState.ACTIVATED,
platform: LicensePlatform.CLOUD,
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
status: '',
updated_at: '0',
},
plan: {
created_at: '0',
description: '',
is_active: true,
name: '',
updated_at: '0',
},
plan_id: '0',
free_until: '0',
updated_at: '0',
valid_from: 0,
valid_until: 0,
created_at: '0',
...overrides,
};
}
export const invalidLicense = buildLicense({ status: LicenseStatus.INVALID });
export function mockUseAuthZGrantAll(
permissions: BrandedPermission[],
_options?: UseAuthZOptions,

View File

@@ -105,6 +105,59 @@ jest.mock('react-i18next', () => ({
}),
}));
export const defaultFeatureFlags = [
{ name: FeatureKeys.SSO, active: true, usage: 0, usage_limit: -1, route: '' },
{
name: FeatureKeys.USE_SPAN_METRICS,
active: false,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.GATEWAY,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.PREMIUM_SUPPORT,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.ONBOARDING,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.CHAT_SUPPORT,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.USE_FINE_GRAINED_AUTHZ,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
];
export function getAppContextMock(
role: string,
appContextOverrides?: Partial<IAppContext>,
@@ -168,57 +221,7 @@ export function getAppContextMock(
hasEditPermission: role === USER_ROLES.ADMIN || role === USER_ROLES.EDITOR,
isFetchingUser: false,
userFetchError: null,
featureFlags: [
{
name: FeatureKeys.SSO,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.USE_SPAN_METRICS,
active: false,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.GATEWAY,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.PREMIUM_SUPPORT,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.ONBOARDING,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.CHAT_SUPPORT,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
featureFlags: defaultFeatureFlags,
isFetchingFeatureFlags: false,
featureFlagsFetchError: null,
hostsData: null,

View File

@@ -48,7 +48,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
HOME: ['ADMIN', 'EDITOR', 'VIEWER'],
ALERTS_NEW: ['ADMIN', 'EDITOR'],
ORG_SETTINGS: ['ADMIN'],
MY_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
MY_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
SERVICE_MAP: ['ADMIN', 'EDITOR', 'VIEWER'],
ALL_CHANNELS: ['ADMIN', 'EDITOR', 'VIEWER'],
INGESTION_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
@@ -72,7 +72,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
NOT_FOUND: ['ADMIN', 'VIEWER', 'EDITOR', 'ANONYMOUS'],
PASSWORD_RESET: ['ADMIN', 'EDITOR', 'VIEWER'],
SERVICE_METRICS: ['ADMIN', 'EDITOR', 'VIEWER'],
SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
SIGN_UP: ['ADMIN', 'EDITOR', 'VIEWER'],
TRACES_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
TRACE: ['ADMIN', 'EDITOR', 'VIEWER'],
@@ -98,10 +98,10 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
GET_STARTED_AZURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'],
WORKSPACE_SUSPENDED: ['ADMIN', 'EDITOR', 'VIEWER'],
ROLES_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
ROLE_DETAILS: ['ADMIN', 'EDITOR', 'VIEWER'],
ROLES_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
ROLE_DETAILS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
MEMBERS_SETTINGS: ['ADMIN'],
SERVICE_ACCOUNTS_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
SERVICE_ACCOUNTS_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
BILLING: ['ADMIN'],
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],

2
go.mod
View File

@@ -23,7 +23,6 @@ require (
github.com/go-playground/validator/v10 v10.27.0
github.com/go-redis/redismock/v9 v9.2.0
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/goccy/go-yaml v1.19.2
github.com/gojek/heimdall/v7 v7.0.3
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
@@ -132,6 +131,7 @@ require (
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/huandu/go-clone v1.7.3 // indirect

View File

@@ -0,0 +1,96 @@
package alertmanagerserver
import (
"context"
"log/slog"
"sync"
"time"
"github.com/prometheus/common/model"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
)
// MaintenanceMuter implements types.Muter for maintenance windows.
// It suppresses alerts whose ruleId label matches an active maintenance schedule.
// Results are cached for cacheTTL to avoid a DB query on every per-alert check.
type MaintenanceMuter struct {
maintenanceStore alertmanagertypes.MaintenanceStore
orgID string
logger *slog.Logger
mu sync.RWMutex
cached []*alertmanagertypes.PlannedMaintenance
cacheExpiry time.Time
}
const maintenanceCacheTTL = 30 * time.Second
func NewMaintenanceMuter(store alertmanagertypes.MaintenanceStore, orgID string, logger *slog.Logger) *MaintenanceMuter {
return &MaintenanceMuter{
maintenanceStore: store,
orgID: orgID,
logger: logger,
}
}
func (m *MaintenanceMuter) Mutes(ctx context.Context, lset model.LabelSet) bool {
ruleID := string(lset[ruletypes.AlertRuleIDLabel])
if ruleID == "" {
return false
}
now := time.Now()
for _, mw := range m.getMaintenances(ctx) {
if mw.ShouldSkip(ruleID, now, lset) {
return true
}
}
return false
}
// MutedBy returns the IDs of all active maintenance windows currently
// suppressing the alert identified by lset. It is used to populate the
// `mutedBy` field on the v2 API alert response so that maintenance-suppressed
// alerts surface as `state=suppressed` in GetAlerts responses.
func (m *MaintenanceMuter) MutedBy(ctx context.Context, lset model.LabelSet) []string {
ruleID := string(lset[ruletypes.AlertRuleIDLabel])
if ruleID == "" {
return nil
}
var ids []string
now := time.Now()
for _, mw := range m.getMaintenances(ctx) {
if mw.ShouldSkip(ruleID, now, lset) {
ids = append(ids, mw.ID.String())
}
}
return ids
}
func (m *MaintenanceMuter) getMaintenances(ctx context.Context) []*alertmanagertypes.PlannedMaintenance {
m.mu.RLock()
if time.Now().Before(m.cacheExpiry) {
cached := m.cached
m.mu.RUnlock()
return cached
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
// Double-check after acquiring write lock.
if time.Now().Before(m.cacheExpiry) {
return m.cached
}
mws, err := m.maintenanceStore.ListPlannedMaintenance(ctx, m.orgID)
if err != nil {
m.logger.ErrorContext(ctx, "failed to list planned maintenance windows; alerts will not be suppressed", slog.String("org_id", m.orgID))
return m.cached // return stale (potentially empty) cache on error
}
m.cached = mws
m.cacheExpiry = time.Now().Add(maintenanceCacheTTL)
return m.cached
}

View File

@@ -0,0 +1,234 @@
package alertmanagerserver
import (
"context"
"log/slog"
"sort"
"sync"
"testing"
"time"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes/alertmanagertypestest"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func newMuter(store alertmanagertypes.MaintenanceStore) *MaintenanceMuter {
return NewMaintenanceMuter(store, "org-1", slog.New(slog.DiscardHandler))
}
// activeFixed builds a fixed-time maintenance window that brackets now.
// ruleIDs scope the window; an empty slice matches every rule.
func activeFixed(ruleIDs ...string) *alertmanagertypes.PlannedMaintenance {
now := time.Now().UTC()
return &alertmanagertypes.PlannedMaintenance{
ID: valuer.GenerateUUID(),
Schedule: &alertmanagertypes.Schedule{
Timezone: "UTC",
StartTime: now.Add(-time.Hour),
EndTime: now.Add(time.Hour),
},
RuleIDs: ruleIDs,
}
}
// futureFixed builds a fixed-time maintenance window that starts in the future.
func futureFixed(ruleIDs ...string) *alertmanagertypes.PlannedMaintenance {
now := time.Now().UTC()
return &alertmanagertypes.PlannedMaintenance{
ID: valuer.GenerateUUID(),
Schedule: &alertmanagertypes.Schedule{
Timezone: "UTC",
StartTime: now.Add(time.Hour),
EndTime: now.Add(2 * time.Hour),
},
RuleIDs: ruleIDs,
}
}
func labelsFor(ruleID string) model.LabelSet {
return model.LabelSet{ruletypes.AlertRuleIDLabel: model.LabelValue(ruleID)}
}
func TestMutes_EmptyRuleIDLabel(t *testing.T) {
store := alertmanagertypestest.NewMockMaintenanceStore(t)
muter := newMuter(store)
assert.False(t, muter.Mutes(context.Background(), model.LabelSet{}))
// Short-circuit: no store lookup needed when the label is missing.
store.AssertNotCalled(t, "ListPlannedMaintenance")
}
func TestMutes_NoMaintenanceWindows(t *testing.T) {
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return([]*alertmanagertypes.PlannedMaintenance(nil), nil)
muter := newMuter(store)
assert.False(t, muter.Mutes(context.Background(), labelsFor("rule-1")))
}
func TestMutes_MaintenanceWindowWithRules(t *testing.T) {
mw := activeFixed("rule-1", "rule-2")
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return([]*alertmanagertypes.PlannedMaintenance{mw}, nil)
muter := newMuter(store)
assert.True(t, muter.Mutes(context.Background(), labelsFor("rule-1")))
assert.True(t, muter.Mutes(context.Background(), labelsFor("rule-2")))
assert.False(t, muter.Mutes(context.Background(), labelsFor("rule-3")))
}
func TestMutes_EmptyRuleIDsMatchesAllRules(t *testing.T) {
// A maintenance with no RuleIDs is treated as scoping every rule.
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return([]*alertmanagertypes.PlannedMaintenance{activeFixed()}, nil)
muter := newMuter(store)
assert.True(t, muter.Mutes(context.Background(), labelsFor("any-rule")))
}
func TestMutes_FutureWindowDoesNotMute(t *testing.T) {
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return([]*alertmanagertypes.PlannedMaintenance{futureFixed("rule-1")}, nil)
muter := newMuter(store)
assert.False(t, muter.Mutes(context.Background(), labelsFor("rule-1")))
}
func TestMutes_AnyOfMultipleWindowsMatches(t *testing.T) {
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return(
[]*alertmanagertypes.PlannedMaintenance{futureFixed("rule-1"), activeFixed("rule-1")}, nil,
)
muter := newMuter(store)
assert.True(t, muter.Mutes(context.Background(), labelsFor("rule-1")))
}
func TestMutedBy_EmptyRuleIDLabel(t *testing.T) {
store := alertmanagertypestest.NewMockMaintenanceStore(t)
muter := newMuter(store)
assert.Nil(t, muter.MutedBy(context.Background(), model.LabelSet{}))
store.AssertNotCalled(t, "ListPlannedMaintenance")
}
func TestMutedBy_NoMatches(t *testing.T) {
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return(
[]*alertmanagertypes.PlannedMaintenance{activeFixed("rule-1"), futureFixed("rule-1")}, nil,
)
muter := newMuter(store)
assert.Nil(t, muter.MutedBy(context.Background(), labelsFor("rule-other")))
}
func TestMutedBy_ReturnsIDsOfAllActiveMatchingWindows(t *testing.T) {
mw1 := activeFixed("rule-1")
mw2 := activeFixed() // matches all rules
mw3 := futureFixed("rule-1")
mw4 := activeFixed("rule-other")
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return(
[]*alertmanagertypes.PlannedMaintenance{mw1, mw2, mw3, mw4}, nil,
)
muter := newMuter(store)
ids := muter.MutedBy(context.Background(), labelsFor("rule-1"))
want := []string{mw1.ID.String(), mw2.ID.String()}
sort.Strings(want)
sort.Strings(ids)
assert.Equal(t, want, ids)
}
func TestCache_RepeatedCallsHitStoreOnce(t *testing.T) {
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return(
[]*alertmanagertypes.PlannedMaintenance{activeFixed("rule-1")}, nil,
)
muter := newMuter(store)
ctx := context.Background()
for i := 0; i < 5; i++ {
require.True(t, muter.Mutes(ctx, labelsFor("rule-1")))
}
store.AssertNumberOfCalls(t, "ListPlannedMaintenance", 1)
}
func TestCache_StoreErrorReturnsStaleCache(t *testing.T) {
mw := activeFixed("rule-1")
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return(
[]*alertmanagertypes.PlannedMaintenance{mw}, nil,
).Once()
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return(
([]*alertmanagertypes.PlannedMaintenance)(nil),
errors.New(errors.TypeInternal, errors.MustNewCode("internal_error"), "boom"),
).Once()
ctx := context.Background()
muter := newMuter(store)
// First call populates the cache from a working store.
require.True(t, muter.Mutes(ctx, labelsFor("rule-1")))
// Force cache to be considered expired so the next call re-fetches.
muter.mu.Lock()
muter.cacheExpiry = time.Time{}
muter.mu.Unlock()
// Store now errors. The muter should fall back to the previously cached value
// (i.e. still mute rule-1) rather than returning false.
assert.True(t, muter.Mutes(ctx, labelsFor("rule-1")),
"on store error, muter should keep using the last known cache to avoid losing suppression")
store.AssertNumberOfCalls(t, "ListPlannedMaintenance", 2)
}
func TestCache_ExpiredCacheRefetchesUpdatedData(t *testing.T) {
mw := activeFixed("rule-1")
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return(
[]*alertmanagertypes.PlannedMaintenance{mw}, nil,
).Once()
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return(
([]*alertmanagertypes.PlannedMaintenance)(nil), nil,
).Once()
ctx := context.Background()
muter := newMuter(store)
require.True(t, muter.Mutes(ctx, labelsFor("rule-1")))
// Expire the cache and let the store return an empty list.
muter.mu.Lock()
muter.cacheExpiry = time.Time{}
muter.mu.Unlock()
assert.False(t, muter.Mutes(ctx, labelsFor("rule-1")))
store.AssertNumberOfCalls(t, "ListPlannedMaintenance", 2)
}
func TestMutes_IsConcurrencySafe(t *testing.T) {
store := alertmanagertypestest.NewMockMaintenanceStore(t)
store.On("ListPlannedMaintenance", mock.Anything, "org-1").Return(
[]*alertmanagertypes.PlannedMaintenance{activeFixed("rule-1")}, nil,
)
muter := newMuter(store)
ctx := context.Background()
const goroutines = 32
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < 50; j++ {
_ = muter.Mutes(ctx, labelsFor("rule-1"))
_ = muter.MutedBy(ctx, labelsFor("rule-1"))
}
}()
}
wg.Wait()
// Even under contention the cache must collapse the load to a single fetch.
store.AssertNumberOfCalls(t, "ListPlannedMaintenance", 1)
}

View File

@@ -0,0 +1,109 @@
// Copyright (c) 2026 SigNoz, Inc.
// Copyright 2015 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package alertmanagerserver
import (
"time"
"github.com/prometheus/alertmanager/featurecontrol"
"github.com/prometheus/alertmanager/inhibit"
"github.com/prometheus/alertmanager/nflog/nflogpb"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/silence"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/client_golang/prometheus"
)
// pipelineBuilder is a local copy of notify.PipelineBuilder that injects
// the maintenance mute stage immediately before the receiver stage.
//
// We maintain our own copy so we can control exactly where in the pipeline
// the maintenance stage runs (between the silence stage and the receiver),
// which is not possible by wrapping the output of the upstream builder.
//
// Upstream pipeline order:
// GossipSettle → Inhibit → TimeActive → TimeMute → Silence → [mms] → Receiver.
type pipelineBuilder struct {
metrics *notify.Metrics
ff featurecontrol.Flagger
}
func newPipelineBuilder(
r prometheus.Registerer,
ff featurecontrol.Flagger,
) *pipelineBuilder {
return &pipelineBuilder{
metrics: notify.NewMetrics(r, ff),
ff: ff,
}
}
// New returns a map of receivers to Stages, mirroring notify.PipelineBuilder.New
// but inserting a maintenanceMuteStage between the silence stage and the receiver.
func (pb *pipelineBuilder) New(
receivers map[string][]notify.Integration,
wait func() time.Duration,
inhibitor *inhibit.Inhibitor,
silencer *silence.Silencer,
intervener *timeinterval.Intervener,
marker types.GroupMarker,
muter *MaintenanceMuter,
notificationLog notify.NotificationLog,
peer notify.Peer,
) notify.RoutingStage {
rs := make(notify.RoutingStage, len(receivers))
ms := notify.NewGossipSettleStage(peer)
is := notify.NewMuteStage(inhibitor, pb.metrics)
tas := notify.NewTimeActiveStage(intervener, marker, pb.metrics)
tms := notify.NewTimeMuteStage(intervener, marker, pb.metrics)
ss := notify.NewMuteStage(silencer, pb.metrics)
mms := notify.NewMuteStage(muter, pb.metrics)
for name := range receivers {
stages := notify.MultiStage{ms, is, tas, tms, ss, mms}
stages = append(stages, createReceiverStage(name, receivers[name], wait, notificationLog, pb.metrics))
rs[name] = stages
}
pb.metrics.InitializeFor(receivers)
return rs
}
// createReceiverStage is a copy of notify.createReceiverStage (unexported upstream).
func createReceiverStage(
name string,
integrations []notify.Integration,
wait func() time.Duration,
notificationLog notify.NotificationLog,
metrics *notify.Metrics,
) notify.Stage {
var fs notify.FanoutStage
for i := range integrations {
recv := &nflogpb.Receiver{
GroupName: name,
Integration: integrations[i].Name(),
Idx: uint32(integrations[i].Index()),
}
var s notify.MultiStage
s = append(s, notify.NewWaitStage(wait))
s = append(s, notify.NewDedupStage(&integrations[i], notificationLog, recv))
s = append(s, notify.NewRetryStage(integrations[i], name, metrics))
s = append(s, notify.NewSetNotifiesStage(notificationLog, recv))
fs = append(fs, s)
}
return fs
}

View File

@@ -28,12 +28,10 @@ import (
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
)
var (
// This is not a real file and will never be used. We need this placeholder to ensure maintenance runs on shutdown. See
// https://github.com/prometheus/server/blob/3ee2cd0f1271e277295c02b6160507b4d193dde2/silence/silence.go#L435-L438
// and https://github.com/prometheus/server/blob/3b06b97af4d146e141af92885a185891eb79a5b0/nflog/nflog.go#L362.
snapfnoop string = "snapfnoop"
)
// This is not a real snapshot file and will never be used. We need this placeholder to ensure maintenance runs on shutdown.
// See https://github.com/prometheus/alertmanager/blob/3ee2cd0f1271e277295c02b6160507b4d193dde2/silence/silence.go#L435-L438
// and https://github.com/prometheus/alertmanager/blob/3b06b97af4d146e141af92885a185891eb79a5b0/nflog/nflog.go#L362.
var snapfnoop string = "snapfnoop"
type Server struct {
// logger is the logger for the alertmanager
@@ -63,15 +61,25 @@ type Server struct {
silencer *silence.Silencer
silences *silence.Silences
timeIntervals map[string][]timeinterval.TimeInterval
pipelineBuilder *notify.PipelineBuilder
marker *alertmanagertypes.MemMarker
pipelineBuilder *pipelineBuilder
muter *MaintenanceMuter
marker *types.MemMarker
tmpl *template.Template
wg sync.WaitGroup
stopc chan struct{}
notificationManager nfmanager.NotificationManager
}
func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registerer, srvConfig Config, orgID string, stateStore alertmanagertypes.StateStore, nfManager nfmanager.NotificationManager) (*Server, error) {
func New(
ctx context.Context,
logger *slog.Logger,
registry prometheus.Registerer,
srvConfig Config,
orgID string,
stateStore alertmanagertypes.StateStore,
nfManager nfmanager.NotificationManager,
maintenanceStore alertmanagertypes.MaintenanceStore,
) (*Server, error) {
server := &Server{
logger: logger.With(slog.String("pkg", "go.signoz.io/pkg/alertmanager/alertmanagerserver")),
registry: registry,
@@ -84,7 +92,7 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
signozRegisterer := prometheus.WrapRegistererWithPrefix("signoz_", registry)
signozRegisterer = prometheus.WrapRegistererWith(prometheus.Labels{"org_id": server.orgID}, signozRegisterer)
// initialize marker
server.marker = alertmanagertypes.NewMarker(signozRegisterer)
server.marker = types.NewMarker(signozRegisterer)
// get silences for initial state
state, err := server.stateStore.Get(ctx, server.orgID)
@@ -160,7 +168,6 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
return c, server.stateStore.Set(ctx, storableSilences)
})
}()
// Start maintenance for notification logs
@@ -196,17 +203,25 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
return nil, err
}
server.pipelineBuilder = notify.NewPipelineBuilder(signozRegisterer, featurecontrol.NoopFlags{})
server.muter = NewMaintenanceMuter(maintenanceStore, orgID, server.logger)
server.pipelineBuilder = newPipelineBuilder(signozRegisterer, featurecontrol.NoopFlags{})
server.dispatcherMetrics = NewDispatcherMetrics(false, signozRegisterer)
return server, nil
}
func (server *Server) GetAlerts(ctx context.Context, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
return alertmanagertypes.NewGettableAlertsFromAlertProvider(server.alerts, server.alertmanagerConfig, server.marker.Status, func(labels model.LabelSet) {
server.inhibitor.Mutes(ctx, labels)
server.silencer.Mutes(ctx, labels)
}, params)
return alertmanagertypes.NewGettableAlertsFromAlertProvider(
server.alerts, server.alertmanagerConfig, server.marker.Status,
func(labels model.LabelSet) {
server.inhibitor.Mutes(ctx, labels)
server.silencer.Mutes(ctx, labels)
},
func(labels model.LabelSet) []string {
return server.muter.MutedBy(ctx, labels)
},
params,
)
}
func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanagertypes.PostableAlerts) error {
@@ -290,6 +305,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
server.silencer,
intervener,
server.marker,
server.muter,
server.nflog,
pipelinePeer,
)

View File

@@ -22,6 +22,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@@ -86,11 +87,25 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
err = notificationManager.SetNotificationConfig(orgID, "high-cpu-usage", &notifConfig)
require.NoError(t, err)
mwID := valuer.GenerateUUID()
maintenanceStore := alertmanagertypestest.NewMockMaintenanceStore(t)
maintenanceStore.On("ListPlannedMaintenance", mock.Anything, orgID).Return(
[]*alertmanagertypes.PlannedMaintenance{{
ID: mwID,
Schedule: &alertmanagertypes.Schedule{
Timezone: "UTC",
StartTime: time.Now().Add(-time.Hour),
EndTime: time.Now().Add(time.Hour),
},
RuleIDs: []string{"high-cpu-usage"},
}}, nil,
)
srvCfg := NewConfig()
stateStore := alertmanagertypestest.NewStateStore()
registry := prometheus.NewRegistry()
logger := slog.New(slog.DiscardHandler)
server, err := New(context.Background(), logger, registry, srvCfg, orgID, stateStore, notificationManager)
server, err := New(context.Background(), logger, registry, srvCfg, orgID, stateStore, notificationManager, maintenanceStore)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, orgID)
require.NoError(t, err)
@@ -151,6 +166,16 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
StartsAt: strfmt.DateTime(now.Add(-3 * time.Minute)),
EndsAt: strfmt.DateTime(time.Time{}), // Active alert
},
{
Alert: alertmanagertypes.AlertModel{
Labels: map[string]string{
"ruleId": "other-rule",
"alertname": "OtherAlert",
},
},
StartsAt: strfmt.DateTime(now.Add(-time.Minute)),
EndsAt: strfmt.DateTime(time.Time{}), // Active alert
},
}
err = server.PutAlerts(ctx, testAlerts)
@@ -166,10 +191,12 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
require.NoError(t, err)
alerts, err := server.GetAlerts(context.Background(), params)
require.NoError(t, err)
require.Len(t, alerts, 3, "Expected 3 active alerts")
require.Len(t, alerts, 4, "Expected 4 active alerts")
for _, alert := range alerts {
require.Equal(t, "high-cpu-usage", alert.Labels["ruleId"])
if alert.Labels["ruleId"] != "high-cpu-usage" {
continue
}
require.NotEmpty(t, alert.Labels["severity"])
require.Contains(t, []string{"critical", "warning"}, alert.Labels["severity"])
require.Equal(t, "prod-cluster", alert.Labels["cluster"])
@@ -221,4 +248,20 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
require.Equal(t, "{__receiver__=\"webhook\"}:{cluster=\"prod-cluster\", instance=\"server-02\", ruleId=\"high-cpu-usage\"}", alertGroups[1].GroupKey)
require.Equal(t, "{__receiver__=\"webhook\"}:{cluster=\"prod-cluster\", instance=\"server-03\", ruleId=\"high-cpu-usage\"}", alertGroups[2].GroupKey)
})
t.Run("verify_muting", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/alerts", nil)
require.NoError(t, err)
params, err := alertmanagertypes.NewGettableAlertsParams(req)
require.NoError(t, err)
alerts, err := server.GetAlerts(ctx, params)
require.NoError(t, err)
for _, alert := range alerts {
if alert.Labels["ruleId"] == "high-cpu-usage" {
require.Equal(t, []string{mwID.String()}, alert.Status.MutedBy)
} else {
require.Empty(t, alert.Status.MutedBy)
}
}
})
}

View File

@@ -10,7 +10,12 @@ import (
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfmanagertest"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes/alertmanagertypestest"
"github.com/go-openapi/strfmt"
@@ -23,9 +28,14 @@ import (
"github.com/stretchr/testify/require"
)
func newTestMaintenanceStore() alertmanagertypes.MaintenanceStore {
ss := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)
return sqlalertmanagerstore.NewMaintenanceStore(ss, factorytest.NewSettings())
}
func TestServerSetConfigAndStop(t *testing.T) {
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager, newTestMaintenanceStore())
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(alertmanagertypes.GlobalConfig{}, alertmanagertypes.RouteConfig{GroupInterval: 1 * time.Minute, RepeatInterval: 1 * time.Minute, GroupWait: 1 * time.Minute}, "1")
@@ -37,7 +47,7 @@ func TestServerSetConfigAndStop(t *testing.T) {
func TestServerTestReceiverTypeWebhook(t *testing.T) {
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager, newTestMaintenanceStore())
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(alertmanagertypes.GlobalConfig{}, alertmanagertypes.RouteConfig{GroupInterval: 1 * time.Minute, RepeatInterval: 1 * time.Minute, GroupWait: 1 * time.Minute}, "1")
@@ -85,7 +95,7 @@ func TestServerPutAlerts(t *testing.T) {
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager, newTestMaintenanceStore())
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
@@ -133,7 +143,7 @@ func TestServerTestAlert(t *testing.T) {
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager, newTestMaintenanceStore())
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
@@ -238,7 +248,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager, newTestMaintenanceStore())
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")

View File

@@ -1,27 +1,33 @@
package sqlrulestore
package sqlalertmanagerstore
import (
"context"
"log/slog"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type maintenance struct {
sqlstore sqlstore.SQLStore
logger *slog.Logger
}
func NewMaintenanceStore(store sqlstore.SQLStore) ruletypes.MaintenanceStore {
return &maintenance{sqlstore: store}
func NewMaintenanceStore(store sqlstore.SQLStore, providerSettings factory.ProviderSettings) alertmanagertypes.MaintenanceStore {
return &maintenance{
sqlstore: store,
logger: providerSettings.Logger,
}
}
func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string) ([]*ruletypes.PlannedMaintenance, error) {
gettableMaintenancesRules := make([]*ruletypes.PlannedMaintenanceWithRules, 0)
func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string) ([]*alertmanagertypes.PlannedMaintenance, error) {
gettableMaintenancesRules := make([]*alertmanagertypes.PlannedMaintenanceWithRules, 0)
err := r.sqlstore.
BunDB().
NewSelect().
@@ -33,16 +39,20 @@ func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string)
return nil, err
}
gettablePlannedMaintenance := make([]*ruletypes.PlannedMaintenance, 0)
gettablePlannedMaintenance := make([]*alertmanagertypes.PlannedMaintenance, 0)
for _, gettableMaintenancesRule := range gettableMaintenancesRules {
gettablePlannedMaintenance = append(gettablePlannedMaintenance, gettableMaintenancesRule.ToPlannedMaintenance())
m := gettableMaintenancesRule.ToPlannedMaintenance()
gettablePlannedMaintenance = append(gettablePlannedMaintenance, m)
if m.HasScheduleRecurrenceBoundsMismatch() {
r.logger.WarnContext(ctx, "planned_downtime_recurrence_schedule_mismatch", slog.String("maintenance_id", m.ID.StringValue()))
}
}
return gettablePlannedMaintenance, nil
}
func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.UUID) (*ruletypes.PlannedMaintenance, error) {
storableMaintenanceRule := new(ruletypes.PlannedMaintenanceWithRules)
func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.UUID) (*alertmanagertypes.PlannedMaintenance, error) {
storableMaintenanceRule := new(alertmanagertypes.PlannedMaintenanceWithRules)
err := r.sqlstore.
BunDB().
NewSelect().
@@ -57,13 +67,13 @@ func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.U
return storableMaintenanceRule.ToPlannedMaintenance(), nil
}
func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance *ruletypes.PostablePlannedMaintenance) (*ruletypes.PlannedMaintenance, error) {
func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance *alertmanagertypes.PostablePlannedMaintenance) (*alertmanagertypes.PlannedMaintenance, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
storablePlannedMaintenance := ruletypes.StorablePlannedMaintenance{
storablePlannedMaintenance := alertmanagertypes.StorablePlannedMaintenance{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -75,20 +85,21 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
CreatedBy: claims.Email,
UpdatedBy: claims.Email,
},
Name: maintenance.Name,
Description: maintenance.Description,
Schedule: maintenance.Schedule,
OrgID: claims.OrgID,
Name: maintenance.Name,
Description: maintenance.Description,
Schedule: maintenance.Schedule,
OrgID: claims.OrgID,
LabelExpression: maintenance.LabelExpression,
}
maintenanceRules := make([]*ruletypes.StorablePlannedMaintenanceRule, 0)
maintenanceRules := make([]*alertmanagertypes.StorablePlannedMaintenanceRule, 0)
for _, ruleIDStr := range maintenance.AlertIds {
ruleID, err := valuer.NewUUID(ruleIDStr)
if err != nil {
return nil, err
}
maintenanceRules = append(maintenanceRules, &ruletypes.StorablePlannedMaintenanceRule{
maintenanceRules = append(maintenanceRules, &alertmanagertypes.StorablePlannedMaintenanceRule{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -113,7 +124,6 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
NewInsert().
Model(&maintenanceRules).
Exec(ctx)
if err != nil {
return err
}
@@ -125,16 +135,17 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
return nil, err
}
return &ruletypes.PlannedMaintenance{
ID: storablePlannedMaintenance.ID,
Name: storablePlannedMaintenance.Name,
Description: storablePlannedMaintenance.Description,
Schedule: storablePlannedMaintenance.Schedule,
RuleIDs: maintenance.AlertIds,
CreatedAt: storablePlannedMaintenance.CreatedAt,
CreatedBy: storablePlannedMaintenance.CreatedBy,
UpdatedAt: storablePlannedMaintenance.UpdatedAt,
UpdatedBy: storablePlannedMaintenance.UpdatedBy,
return &alertmanagertypes.PlannedMaintenance{
ID: storablePlannedMaintenance.ID,
Name: storablePlannedMaintenance.Name,
Description: storablePlannedMaintenance.Description,
Schedule: storablePlannedMaintenance.Schedule,
RuleIDs: maintenance.AlertIds,
LabelExpression: maintenance.LabelExpression,
CreatedAt: storablePlannedMaintenance.CreatedAt,
CreatedBy: storablePlannedMaintenance.CreatedBy,
UpdatedAt: storablePlannedMaintenance.UpdatedAt,
UpdatedBy: storablePlannedMaintenance.UpdatedBy,
}, nil
}
@@ -142,7 +153,7 @@ func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UU
_, err := r.sqlstore.
BunDB().
NewDelete().
Model(new(ruletypes.StorablePlannedMaintenance)).
Model(new(alertmanagertypes.StorablePlannedMaintenance)).
Where("id = ?", id.StringValue()).
Exec(ctx)
if err != nil {
@@ -152,7 +163,7 @@ func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UU
return nil
}
func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance *ruletypes.PostablePlannedMaintenance, id valuer.UUID) error {
func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance *alertmanagertypes.PostablePlannedMaintenance, id valuer.UUID) error {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return err
@@ -163,7 +174,7 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
return err
}
storablePlannedMaintenance := ruletypes.StorablePlannedMaintenance{
storablePlannedMaintenance := alertmanagertypes.StorablePlannedMaintenance{
Identifiable: types.Identifiable{
ID: id,
},
@@ -175,20 +186,21 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
CreatedBy: existing.CreatedBy,
UpdatedBy: claims.Email,
},
Name: maintenance.Name,
Description: maintenance.Description,
Schedule: maintenance.Schedule,
OrgID: claims.OrgID,
Name: maintenance.Name,
Description: maintenance.Description,
Schedule: maintenance.Schedule,
OrgID: claims.OrgID,
LabelExpression: maintenance.LabelExpression,
}
storablePlannedMaintenanceRules := make([]*ruletypes.StorablePlannedMaintenanceRule, 0)
storablePlannedMaintenanceRules := make([]*alertmanagertypes.StorablePlannedMaintenanceRule, 0)
for _, ruleIDStr := range maintenance.AlertIds {
ruleID, err := valuer.NewUUID(ruleIDStr)
if err != nil {
return err
}
storablePlannedMaintenanceRules = append(storablePlannedMaintenanceRules, &ruletypes.StorablePlannedMaintenanceRule{
storablePlannedMaintenanceRules = append(storablePlannedMaintenanceRules, &alertmanagertypes.StorablePlannedMaintenanceRule{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -211,10 +223,9 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
_, err = r.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(ruletypes.StorablePlannedMaintenanceRule)).
Model(new(alertmanagertypes.StorablePlannedMaintenanceRule)).
Where("planned_maintenance_id = ?", storablePlannedMaintenance.ID.StringValue()).
Exec(ctx)
if err != nil {
return err
}
@@ -231,7 +242,6 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
}
return nil
})
if err != nil {
return err

View File

@@ -6,6 +6,7 @@ package alertmanagertest
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -1845,3 +1846,628 @@ func (_c *MockAlertmanager_UpdateRoutePolicyByID_Call) RunAndReturn(run func(ctx
_c.Call.Return(run)
return _c
}
// NewMockHandler creates a new instance of MockHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockHandler(t interface {
mock.TestingT
Cleanup(func())
}) *MockHandler {
mock := &MockHandler{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// MockHandler is an autogenerated mock type for the Handler type
type MockHandler struct {
mock.Mock
}
type MockHandler_Expecter struct {
mock *mock.Mock
}
func (_m *MockHandler) EXPECT() *MockHandler_Expecter {
return &MockHandler_Expecter{mock: &_m.Mock}
}
// CreateChannel provides a mock function for the type MockHandler
func (_mock *MockHandler) CreateChannel(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_CreateChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateChannel'
type MockHandler_CreateChannel_Call struct {
*mock.Call
}
// CreateChannel is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) CreateChannel(responseWriter interface{}, request interface{}) *MockHandler_CreateChannel_Call {
return &MockHandler_CreateChannel_Call{Call: _e.mock.On("CreateChannel", responseWriter, request)}
}
func (_c *MockHandler_CreateChannel_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_CreateChannel_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_CreateChannel_Call) Return() *MockHandler_CreateChannel_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_CreateChannel_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_CreateChannel_Call {
_c.Run(run)
return _c
}
// CreateRoutePolicy provides a mock function for the type MockHandler
func (_mock *MockHandler) CreateRoutePolicy(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_CreateRoutePolicy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRoutePolicy'
type MockHandler_CreateRoutePolicy_Call struct {
*mock.Call
}
// CreateRoutePolicy is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) CreateRoutePolicy(responseWriter interface{}, request interface{}) *MockHandler_CreateRoutePolicy_Call {
return &MockHandler_CreateRoutePolicy_Call{Call: _e.mock.On("CreateRoutePolicy", responseWriter, request)}
}
func (_c *MockHandler_CreateRoutePolicy_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_CreateRoutePolicy_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_CreateRoutePolicy_Call) Return() *MockHandler_CreateRoutePolicy_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_CreateRoutePolicy_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_CreateRoutePolicy_Call {
_c.Run(run)
return _c
}
// DeleteChannelByID provides a mock function for the type MockHandler
func (_mock *MockHandler) DeleteChannelByID(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_DeleteChannelByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteChannelByID'
type MockHandler_DeleteChannelByID_Call struct {
*mock.Call
}
// DeleteChannelByID is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) DeleteChannelByID(responseWriter interface{}, request interface{}) *MockHandler_DeleteChannelByID_Call {
return &MockHandler_DeleteChannelByID_Call{Call: _e.mock.On("DeleteChannelByID", responseWriter, request)}
}
func (_c *MockHandler_DeleteChannelByID_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_DeleteChannelByID_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_DeleteChannelByID_Call) Return() *MockHandler_DeleteChannelByID_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_DeleteChannelByID_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_DeleteChannelByID_Call {
_c.Run(run)
return _c
}
// DeleteRoutePolicyByID provides a mock function for the type MockHandler
func (_mock *MockHandler) DeleteRoutePolicyByID(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_DeleteRoutePolicyByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteRoutePolicyByID'
type MockHandler_DeleteRoutePolicyByID_Call struct {
*mock.Call
}
// DeleteRoutePolicyByID is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) DeleteRoutePolicyByID(responseWriter interface{}, request interface{}) *MockHandler_DeleteRoutePolicyByID_Call {
return &MockHandler_DeleteRoutePolicyByID_Call{Call: _e.mock.On("DeleteRoutePolicyByID", responseWriter, request)}
}
func (_c *MockHandler_DeleteRoutePolicyByID_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_DeleteRoutePolicyByID_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_DeleteRoutePolicyByID_Call) Return() *MockHandler_DeleteRoutePolicyByID_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_DeleteRoutePolicyByID_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_DeleteRoutePolicyByID_Call {
_c.Run(run)
return _c
}
// GetAlerts provides a mock function for the type MockHandler
func (_mock *MockHandler) GetAlerts(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_GetAlerts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAlerts'
type MockHandler_GetAlerts_Call struct {
*mock.Call
}
// GetAlerts is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) GetAlerts(responseWriter interface{}, request interface{}) *MockHandler_GetAlerts_Call {
return &MockHandler_GetAlerts_Call{Call: _e.mock.On("GetAlerts", responseWriter, request)}
}
func (_c *MockHandler_GetAlerts_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_GetAlerts_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_GetAlerts_Call) Return() *MockHandler_GetAlerts_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_GetAlerts_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_GetAlerts_Call {
_c.Run(run)
return _c
}
// GetAllRoutePolicies provides a mock function for the type MockHandler
func (_mock *MockHandler) GetAllRoutePolicies(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_GetAllRoutePolicies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllRoutePolicies'
type MockHandler_GetAllRoutePolicies_Call struct {
*mock.Call
}
// GetAllRoutePolicies is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) GetAllRoutePolicies(responseWriter interface{}, request interface{}) *MockHandler_GetAllRoutePolicies_Call {
return &MockHandler_GetAllRoutePolicies_Call{Call: _e.mock.On("GetAllRoutePolicies", responseWriter, request)}
}
func (_c *MockHandler_GetAllRoutePolicies_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_GetAllRoutePolicies_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_GetAllRoutePolicies_Call) Return() *MockHandler_GetAllRoutePolicies_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_GetAllRoutePolicies_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_GetAllRoutePolicies_Call {
_c.Run(run)
return _c
}
// GetChannelByID provides a mock function for the type MockHandler
func (_mock *MockHandler) GetChannelByID(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_GetChannelByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetChannelByID'
type MockHandler_GetChannelByID_Call struct {
*mock.Call
}
// GetChannelByID is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) GetChannelByID(responseWriter interface{}, request interface{}) *MockHandler_GetChannelByID_Call {
return &MockHandler_GetChannelByID_Call{Call: _e.mock.On("GetChannelByID", responseWriter, request)}
}
func (_c *MockHandler_GetChannelByID_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_GetChannelByID_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_GetChannelByID_Call) Return() *MockHandler_GetChannelByID_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_GetChannelByID_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_GetChannelByID_Call {
_c.Run(run)
return _c
}
// GetRoutePolicyByID provides a mock function for the type MockHandler
func (_mock *MockHandler) GetRoutePolicyByID(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_GetRoutePolicyByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRoutePolicyByID'
type MockHandler_GetRoutePolicyByID_Call struct {
*mock.Call
}
// GetRoutePolicyByID is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) GetRoutePolicyByID(responseWriter interface{}, request interface{}) *MockHandler_GetRoutePolicyByID_Call {
return &MockHandler_GetRoutePolicyByID_Call{Call: _e.mock.On("GetRoutePolicyByID", responseWriter, request)}
}
func (_c *MockHandler_GetRoutePolicyByID_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_GetRoutePolicyByID_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_GetRoutePolicyByID_Call) Return() *MockHandler_GetRoutePolicyByID_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_GetRoutePolicyByID_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_GetRoutePolicyByID_Call {
_c.Run(run)
return _c
}
// ListAllChannels provides a mock function for the type MockHandler
func (_mock *MockHandler) ListAllChannels(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_ListAllChannels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAllChannels'
type MockHandler_ListAllChannels_Call struct {
*mock.Call
}
// ListAllChannels is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) ListAllChannels(responseWriter interface{}, request interface{}) *MockHandler_ListAllChannels_Call {
return &MockHandler_ListAllChannels_Call{Call: _e.mock.On("ListAllChannels", responseWriter, request)}
}
func (_c *MockHandler_ListAllChannels_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_ListAllChannels_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_ListAllChannels_Call) Return() *MockHandler_ListAllChannels_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_ListAllChannels_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_ListAllChannels_Call {
_c.Run(run)
return _c
}
// ListChannels provides a mock function for the type MockHandler
func (_mock *MockHandler) ListChannels(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_ListChannels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListChannels'
type MockHandler_ListChannels_Call struct {
*mock.Call
}
// ListChannels is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) ListChannels(responseWriter interface{}, request interface{}) *MockHandler_ListChannels_Call {
return &MockHandler_ListChannels_Call{Call: _e.mock.On("ListChannels", responseWriter, request)}
}
func (_c *MockHandler_ListChannels_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_ListChannels_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_ListChannels_Call) Return() *MockHandler_ListChannels_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_ListChannels_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_ListChannels_Call {
_c.Run(run)
return _c
}
// TestReceiver provides a mock function for the type MockHandler
func (_mock *MockHandler) TestReceiver(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_TestReceiver_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TestReceiver'
type MockHandler_TestReceiver_Call struct {
*mock.Call
}
// TestReceiver is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) TestReceiver(responseWriter interface{}, request interface{}) *MockHandler_TestReceiver_Call {
return &MockHandler_TestReceiver_Call{Call: _e.mock.On("TestReceiver", responseWriter, request)}
}
func (_c *MockHandler_TestReceiver_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_TestReceiver_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_TestReceiver_Call) Return() *MockHandler_TestReceiver_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_TestReceiver_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_TestReceiver_Call {
_c.Run(run)
return _c
}
// UpdateChannelByID provides a mock function for the type MockHandler
func (_mock *MockHandler) UpdateChannelByID(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_UpdateChannelByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateChannelByID'
type MockHandler_UpdateChannelByID_Call struct {
*mock.Call
}
// UpdateChannelByID is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) UpdateChannelByID(responseWriter interface{}, request interface{}) *MockHandler_UpdateChannelByID_Call {
return &MockHandler_UpdateChannelByID_Call{Call: _e.mock.On("UpdateChannelByID", responseWriter, request)}
}
func (_c *MockHandler_UpdateChannelByID_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_UpdateChannelByID_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_UpdateChannelByID_Call) Return() *MockHandler_UpdateChannelByID_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_UpdateChannelByID_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_UpdateChannelByID_Call {
_c.Run(run)
return _c
}
// UpdateRoutePolicy provides a mock function for the type MockHandler
func (_mock *MockHandler) UpdateRoutePolicy(responseWriter http.ResponseWriter, request *http.Request) {
_mock.Called(responseWriter, request)
return
}
// MockHandler_UpdateRoutePolicy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRoutePolicy'
type MockHandler_UpdateRoutePolicy_Call struct {
*mock.Call
}
// UpdateRoutePolicy is a helper method to define mock.On call
// - responseWriter http.ResponseWriter
// - request *http.Request
func (_e *MockHandler_Expecter) UpdateRoutePolicy(responseWriter interface{}, request interface{}) *MockHandler_UpdateRoutePolicy_Call {
return &MockHandler_UpdateRoutePolicy_Call{Call: _e.mock.On("UpdateRoutePolicy", responseWriter, request)}
}
func (_c *MockHandler_UpdateRoutePolicy_Call) Run(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_UpdateRoutePolicy_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 http.ResponseWriter
if args[0] != nil {
arg0 = args[0].(http.ResponseWriter)
}
var arg1 *http.Request
if args[1] != nil {
arg1 = args[1].(*http.Request)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockHandler_UpdateRoutePolicy_Call) Return() *MockHandler_UpdateRoutePolicy_Call {
_c.Call.Return()
return _c
}
func (_c *MockHandler_UpdateRoutePolicy_Call) RunAndReturn(run func(responseWriter http.ResponseWriter, request *http.Request)) *MockHandler_UpdateRoutePolicy_Call {
_c.Run(run)
return _c
}

View File

@@ -39,16 +39,18 @@ type Service struct {
serversMtx sync.RWMutex
notificationManager nfmanager.NotificationManager
maintenanceStore alertmanagertypes.MaintenanceStore
}
func New(
ctx context.Context,
settings factory.ScopedProviderSettings,
config alertmanagerserver.Config,
stateStore alertmanagertypes.StateStore,
configStore alertmanagertypes.ConfigStore,
orgGetter organization.Getter,
nfManager nfmanager.NotificationManager,
maintenanceStore alertmanagertypes.MaintenanceStore,
) *Service {
service := &Service{
config: config,
@@ -59,6 +61,7 @@ func New(
servers: make(map[string]*alertmanagerserver.Server),
serversMtx: sync.RWMutex{},
notificationManager: nfManager,
maintenanceStore: maintenanceStore,
}
return service
@@ -177,7 +180,10 @@ func (service *Service) newServer(ctx context.Context, orgID string) (*alertmana
return nil, err
}
server, err := alertmanagerserver.New(ctx, service.settings.Logger(), service.settings.PrometheusRegisterer(), service.config, orgID, service.stateStore, service.notificationManager)
server, err := alertmanagerserver.New(
ctx, service.settings.Logger(), service.settings.PrometheusRegisterer(), service.config, orgID,
service.stateStore, service.notificationManager, service.maintenanceStore,
)
if err != nil {
return nil, err
}

View File

@@ -4,11 +4,8 @@ import (
"context"
"time"
"github.com/prometheus/common/model"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
amConfig "github.com/prometheus/alertmanager/config"
"github.com/prometheus/common/model"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
@@ -20,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -30,35 +28,49 @@ type provider struct {
configStore alertmanagertypes.ConfigStore
stateStore alertmanagertypes.StateStore
notificationManager nfmanager.NotificationManager
maintenanceStore alertmanagertypes.MaintenanceStore
stopC chan struct{}
}
func NewFactory(sqlstore sqlstore.SQLStore, orgGetter organization.Getter, notificationManager nfmanager.NotificationManager) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
func NewFactory(
sqlstore sqlstore.SQLStore,
orgGetter organization.Getter,
notificationManager nfmanager.NotificationManager,
maintenanceStore alertmanagertypes.MaintenanceStore,
) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) {
return New(ctx, settings, config, sqlstore, orgGetter, notificationManager)
return New(settings, config, sqlstore, orgGetter, notificationManager, maintenanceStore)
})
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore, orgGetter organization.Getter, notificationManager nfmanager.NotificationManager) (*provider, error) {
func New(
providerSettings factory.ProviderSettings,
config alertmanager.Config,
sqlstore sqlstore.SQLStore,
orgGetter organization.Getter,
notificationManager nfmanager.NotificationManager,
maintenanceStore alertmanagertypes.MaintenanceStore,
) (*provider, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager")
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
stateStore := sqlalertmanagerstore.NewStateStore(sqlstore)
p := &provider{
service: alertmanager.New(
ctx,
settings,
config.Signoz.Config,
stateStore,
configStore,
orgGetter,
notificationManager,
maintenanceStore,
),
settings: settings,
config: config,
configStore: configStore,
stateStore: stateStore,
notificationManager: notificationManager,
maintenanceStore: maintenanceStore,
stopC: make(chan struct{}),
}
@@ -113,7 +125,7 @@ func (provider *provider) TestAlert(ctx context.Context, orgID string, ruleID st
for k, v := range alert.Labels {
set[model.LabelName(k)] = model.LabelValue(v)
}
match, err := provider.notificationManager.Match(ctx, orgID, alert.Labels[labels.AlertRuleIdLabel], set)
match, err := provider.notificationManager.Match(ctx, orgID, alert.Labels[ruletypes.LabelRuleID], set)
if err != nil {
return err
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/gorilla/mux"
)
@@ -120,8 +121,8 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
Tags: []string{"downtimeschedules"},
Summary: "List downtime schedules",
Description: "This endpoint lists all planned maintenance / downtime schedules",
RequestQuery: new(ruletypes.ListPlannedMaintenanceParams),
Response: make([]*ruletypes.PlannedMaintenance, 0),
RequestQuery: new(alertmanagertypes.ListPlannedMaintenanceParams),
Response: make([]*alertmanagertypes.PlannedMaintenance, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
@@ -134,7 +135,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
Tags: []string{"downtimeschedules"},
Summary: "Get downtime schedule by ID",
Description: "This endpoint returns a downtime schedule by ID",
Response: new(ruletypes.PlannedMaintenance),
Response: new(alertmanagertypes.PlannedMaintenance),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
@@ -148,9 +149,9 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
Tags: []string{"downtimeschedules"},
Summary: "Create downtime schedule",
Description: "This endpoint creates a new planned maintenance / downtime schedule",
Request: new(ruletypes.PostablePlannedMaintenance),
Request: new(alertmanagertypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
Response: new(ruletypes.PlannedMaintenance),
Response: new(alertmanagertypes.PlannedMaintenance),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
@@ -164,7 +165,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
Tags: []string{"downtimeschedules"},
Summary: "Update downtime schedule",
Description: "This endpoint updates a downtime schedule by ID",
Request: new(ruletypes.PostablePlannedMaintenance),
Request: new(alertmanagertypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},

View File

@@ -64,7 +64,8 @@ func New(ctx context.Context, settings factory.ProviderSettings, config cache.Co
o.ObserveInt64(telemetry.setsRejected, int64(metrics.SetsRejected()), metric.WithAttributes(attributes...))
o.ObserveInt64(telemetry.getsDropped, int64(metrics.GetsDropped()), metric.WithAttributes(attributes...))
o.ObserveInt64(telemetry.getsKept, int64(metrics.GetsKept()), metric.WithAttributes(attributes...))
o.ObserveInt64(telemetry.totalCost, int64(cc.MaxCost()), metric.WithAttributes(attributes...))
o.ObserveInt64(telemetry.costUsed, int64(metrics.CostAdded())-int64(metrics.CostEvicted()), metric.WithAttributes(attributes...))
o.ObserveInt64(telemetry.totalCost, cc.MaxCost(), metric.WithAttributes(attributes...))
return nil
},
telemetry.cacheRatio,
@@ -79,6 +80,7 @@ func New(ctx context.Context, settings factory.ProviderSettings, config cache.Co
telemetry.setsRejected,
telemetry.getsDropped,
telemetry.getsKept,
telemetry.costUsed,
telemetry.totalCost,
)
if err != nil {
@@ -112,11 +114,13 @@ func (provider *provider) Set(ctx context.Context, orgID valuer.UUID, cacheKey s
}
if cloneable, ok := data.(cachetypes.Cloneable); ok {
cost := max(cloneable.Cost(), 1)
// Clamp to a minimum of 1: ristretto treats cost 0 specially and we
// never want zero-size entries to bypass admission accounting.
span.SetAttributes(attribute.Bool("memory.cloneable", true))
span.SetAttributes(attribute.Int64("memory.cost", 1))
span.SetAttributes(attribute.Int64("memory.cost", cost))
toCache := cloneable.Clone()
// In case of contention we are choosing to evict the cloneable entries first hence cost is set to 1
if ok := provider.cc.SetWithTTL(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, 1, ttl); !ok {
if ok := provider.cc.SetWithTTL(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, cost, ttl); !ok {
return errors.New(errors.TypeInternal, errors.CodeInternal, "error writing to cache")
}
@@ -125,15 +129,15 @@ func (provider *provider) Set(ctx context.Context, orgID valuer.UUID, cacheKey s
}
toCache, err := provider.marshalBinary(ctx, data)
cost := int64(len(toCache))
if err != nil {
return err
}
cost := max(int64(len(toCache)), 1)
span.SetAttributes(attribute.Bool("memory.cloneable", false))
span.SetAttributes(attribute.Int64("memory.cost", cost))
if ok := provider.cc.SetWithTTL(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, 1, ttl); !ok {
if ok := provider.cc.SetWithTTL(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, cost, ttl); !ok {
return errors.New(errors.TypeInternal, errors.CodeInternal, "error writing to cache")
}

View File

@@ -31,6 +31,10 @@ func (cloneable *CloneableA) Clone() cachetypes.Cacheable {
}
}
func (cloneable *CloneableA) Cost() int64 {
return int64(len(cloneable.Key)) + 16
}
func (cloneable *CloneableA) MarshalBinary() ([]byte, error) {
return json.Marshal(cloneable)
}
@@ -165,6 +169,45 @@ func TestSetGetWithDifferentTypes(t *testing.T) {
assert.Error(t, err)
}
// LargeCloneable reports a large byte cost so we can test ristretto eviction
// without allocating the full payload in memory.
type LargeCloneable struct {
Key string
CostHint int64
}
func (c *LargeCloneable) Clone() cachetypes.Cacheable {
return &LargeCloneable{Key: c.Key, CostHint: c.CostHint}
}
func (c *LargeCloneable) Cost() int64 { return c.CostHint }
func (c *LargeCloneable) MarshalBinary() ([]byte, error) { return json.Marshal(c) }
func (c *LargeCloneable) UnmarshalBinary(data []byte) error { return json.Unmarshal(data, c) }
func TestCloneableExceedingMaxCostIsRejected(t *testing.T) {
const maxCost int64 = 1 << 20 // 1 MiB
const oversize int64 = 2 << 20 // 2 MiB, larger than the entire cache
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
NumCounters: 10 * 1000,
MaxCost: maxCost,
}})
require.NoError(t, err)
orgID := valuer.GenerateUUID()
const key = "oversize-key"
assert.NoError(t, c.Set(context.Background(), orgID, key,
&LargeCloneable{Key: key, CostHint: oversize}, time.Minute))
// Ristretto rejects any entry with cost > MaxCost (policy.go:100). Probe
// ristretto directly to confirm no admission, instead of relying on metrics.
cc := c.(*provider).cc
_, ok := cc.Get(strings.Join([]string{orgID.StringValue(), key}, "::"))
assert.False(t, ok, "entry with Cost() > MaxCost must be rejected")
}
func TestCloneableConcurrentSetGet(t *testing.T) {
cache, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
NumCounters: 10 * 1000,

View File

@@ -7,17 +7,18 @@ import (
type telemetry struct {
cacheRatio metric.Float64ObservableGauge
cacheHits metric.Int64ObservableGauge
cacheMisses metric.Int64ObservableGauge
costAdded metric.Int64ObservableGauge
costEvicted metric.Int64ObservableGauge
keysAdded metric.Int64ObservableGauge
keysEvicted metric.Int64ObservableGauge
keysUpdated metric.Int64ObservableGauge
setsDropped metric.Int64ObservableGauge
setsRejected metric.Int64ObservableGauge
getsDropped metric.Int64ObservableGauge
getsKept metric.Int64ObservableGauge
cacheHits metric.Int64ObservableCounter
cacheMisses metric.Int64ObservableCounter
costAdded metric.Int64ObservableCounter
costEvicted metric.Int64ObservableCounter
keysAdded metric.Int64ObservableCounter
keysEvicted metric.Int64ObservableCounter
keysUpdated metric.Int64ObservableCounter
setsDropped metric.Int64ObservableCounter
setsRejected metric.Int64ObservableCounter
getsDropped metric.Int64ObservableCounter
getsKept metric.Int64ObservableCounter
costUsed metric.Int64ObservableGauge
totalCost metric.Int64ObservableGauge
}
@@ -28,62 +29,67 @@ func newMetrics(meter metric.Meter) (*telemetry, error) {
errs = errors.Join(errs, err)
}
cacheHits, err := meter.Int64ObservableGauge("signoz.cache.hits", metric.WithDescription("Hits is the number of Get calls where a value was found for the corresponding key."))
cacheHits, err := meter.Int64ObservableCounter("signoz.cache.hits", metric.WithDescription("Hits is the number of Get calls where a value was found for the corresponding key."))
if err != nil {
errs = errors.Join(errs, err)
}
cacheMisses, err := meter.Int64ObservableGauge("signoz.cache.misses", metric.WithDescription("Misses is the number of Get calls where a value was not found for the corresponding key"))
cacheMisses, err := meter.Int64ObservableCounter("signoz.cache.misses", metric.WithDescription("Misses is the number of Get calls where a value was not found for the corresponding key"))
if err != nil {
errs = errors.Join(errs, err)
}
costAdded, err := meter.Int64ObservableGauge("signoz.cache.cost.added", metric.WithDescription("CostAdded is the sum of costs that have been added (successful Set calls)"))
costAdded, err := meter.Int64ObservableCounter("signoz.cache.cost.added", metric.WithDescription("CostAdded is the sum of costs that have been added (successful Set calls)"))
if err != nil {
errs = errors.Join(errs, err)
}
costEvicted, err := meter.Int64ObservableGauge("signoz.cache.cost.evicted", metric.WithDescription("CostEvicted is the sum of all costs that have been evicted"))
costEvicted, err := meter.Int64ObservableCounter("signoz.cache.cost.evicted", metric.WithDescription("CostEvicted is the sum of all costs that have been evicted"))
if err != nil {
errs = errors.Join(errs, err)
}
keysAdded, err := meter.Int64ObservableGauge("signoz.cache.keys.added", metric.WithDescription("KeysAdded is the total number of Set calls where a new key-value item was added"))
keysAdded, err := meter.Int64ObservableCounter("signoz.cache.keys.added", metric.WithDescription("KeysAdded is the total number of Set calls where a new key-value item was added"))
if err != nil {
errs = errors.Join(errs, err)
}
keysEvicted, err := meter.Int64ObservableGauge("signoz.cache.keys.evicted", metric.WithDescription("KeysEvicted is the total number of keys evicted"))
keysEvicted, err := meter.Int64ObservableCounter("signoz.cache.keys.evicted", metric.WithDescription("KeysEvicted is the total number of keys evicted"))
if err != nil {
errs = errors.Join(errs, err)
}
keysUpdated, err := meter.Int64ObservableGauge("signoz.cache.keys.updated", metric.WithDescription("KeysUpdated is the total number of Set calls where the value was updated"))
keysUpdated, err := meter.Int64ObservableCounter("signoz.cache.keys.updated", metric.WithDescription("KeysUpdated is the total number of Set calls where the value was updated"))
if err != nil {
errs = errors.Join(errs, err)
}
setsDropped, err := meter.Int64ObservableGauge("signoz.cache.sets.dropped", metric.WithDescription("SetsDropped is the number of Set calls that don't make it into internal buffers (due to contention or some other reason)"))
setsDropped, err := meter.Int64ObservableCounter("signoz.cache.sets.dropped", metric.WithDescription("SetsDropped is the number of Set calls that don't make it into internal buffers (due to contention or some other reason)"))
if err != nil {
errs = errors.Join(errs, err)
}
setsRejected, err := meter.Int64ObservableGauge("signoz.cache.sets.rejected", metric.WithDescription("SetsRejected is the number of Set calls rejected by the policy (TinyLFU)"))
setsRejected, err := meter.Int64ObservableCounter("signoz.cache.sets.rejected", metric.WithDescription("SetsRejected is the number of Set calls rejected by the policy (TinyLFU)"))
if err != nil {
errs = errors.Join(errs, err)
}
getsDropped, err := meter.Int64ObservableGauge("signoz.cache.gets.dropped", metric.WithDescription("GetsDropped is the number of Get calls that don't make it into internal buffers (due to contention or some other reason)"))
getsDropped, err := meter.Int64ObservableCounter("signoz.cache.gets.dropped", metric.WithDescription("GetsDropped is the number of Get calls that don't make it into internal buffers (due to contention or some other reason)"))
if err != nil {
errs = errors.Join(errs, err)
}
getsKept, err := meter.Int64ObservableGauge("signoz.cache.gets.kept", metric.WithDescription("GetsKept is the number of Get calls that make it into internal buffers"))
getsKept, err := meter.Int64ObservableCounter("signoz.cache.gets.kept", metric.WithDescription("GetsKept is the number of Get calls that make it into internal buffers"))
if err != nil {
errs = errors.Join(errs, err)
}
totalCost, err := meter.Int64ObservableGauge("signoz.cache.total.cost", metric.WithDescription("TotalCost is the available cost configured for the cache"))
costUsed, err := meter.Int64ObservableGauge("signoz.cache.cost.used", metric.WithDescription("CostUsed is the current retained cost in the cache (CostAdded - CostEvicted)."))
if err != nil {
errs = errors.Join(errs, err)
}
totalCost, err := meter.Int64ObservableGauge("signoz.cache.total.cost", metric.WithDescription("TotalCost is the configured MaxCost ceiling for the cache."))
if err != nil {
errs = errors.Join(errs, err)
}
@@ -105,6 +111,7 @@ func newMetrics(meter metric.Meter) (*telemetry, error) {
setsRejected: setsRejected,
getsDropped: getsDropped,
getsKept: getsKept,
costUsed: costUsed,
totalCost: totalCost,
}, nil
}

View File

@@ -29,6 +29,10 @@ func (cacheable *CacheableA) Clone() cachetypes.Cacheable {
}
}
func (cacheable *CacheableA) Cost() int64 {
return int64(len(cacheable.Key)) + 16
}
func (cacheable *CacheableA) MarshalBinary() ([]byte, error) {
return json.Marshal(cacheable)
}

View File

@@ -2,6 +2,8 @@ package envprovider
import (
"context"
"os"
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/config"
@@ -9,7 +11,21 @@ import (
"github.com/stretchr/testify/require"
)
// clearSignozEnv unsets all existing SIGNOZ_* env vars for the duration of the test.
func clearSignozEnv(t *testing.T) {
t.Helper()
for _, kv := range os.Environ() {
if strings.HasPrefix(kv, prefix) {
key := strings.SplitN(kv, "=", 2)[0]
orig, _ := os.LookupEnv(key)
os.Unsetenv(key)
t.Cleanup(func() { os.Setenv(key, orig) })
}
}
}
func TestGetWithStrings(t *testing.T) {
clearSignozEnv(t)
t.Setenv("SIGNOZ_K1_K2", "string")
t.Setenv("SIGNOZ_K3__K4", "string")
t.Setenv("SIGNOZ_K5__K6_K7__K8", "string")
@@ -31,6 +47,7 @@ func TestGetWithStrings(t *testing.T) {
}
func TestGetWithNoPrefix(t *testing.T) {
clearSignozEnv(t)
t.Setenv("K1_K2", "string")
t.Setenv("K3_K4", "string")
expected := map[string]any{}
@@ -43,6 +60,7 @@ func TestGetWithNoPrefix(t *testing.T) {
}
func TestGetWithGoTypes(t *testing.T) {
clearSignozEnv(t)
t.Setenv("SIGNOZ_BOOL", "true")
t.Setenv("SIGNOZ_STRING", "string")
t.Setenv("SIGNOZ_INT", "1")

View File

@@ -9,7 +9,8 @@ var (
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
)
func MustNewRegistry() featuretypes.Registry {
@@ -70,6 +71,14 @@ func MustNewRegistry() featuretypes.Registry {
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
&featuretypes.Feature{
Name: FeatureUseFineGrainedAuthz,
Kind: featuretypes.KindBoolean,
Stage: featuretypes.StageExperimental,
Description: "Controls whether fine-grained authorization is enabled",
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
)
if err != nil {
panic(err)

View File

@@ -2,17 +2,12 @@ package implspanmapper
import (
"context"
"encoding/json"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/types/opamptypes"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var _ spanmapper.Module = (*module)(nil)
type module struct {
store spantypes.SpanMapperStore
}
@@ -39,22 +34,11 @@ func (module *module) UpdateGroup(ctx context.Context, orgID, id valuer.UUID, na
return err
}
group.Update(name, condition, enabled, updatedBy)
err = module.store.UpdateGroup(ctx, group)
if err != nil {
return err
}
agentConf.NotifyConfigUpdate(ctx)
return nil
return module.store.UpdateGroup(ctx, group)
}
func (module *module) DeleteGroup(ctx context.Context, orgID, id valuer.UUID) error {
err := module.store.DeleteGroup(ctx, orgID, id)
if err != nil {
return err
}
agentConf.NotifyConfigUpdate(ctx)
return nil
return module.store.DeleteGroup(ctx, orgID, id)
}
func (module *module) ListMappers(ctx context.Context, orgID, groupID valuer.UUID) ([]*spantypes.SpanMapper, error) {
@@ -70,12 +54,7 @@ func (module *module) CreateMapper(ctx context.Context, orgID, groupID valuer.UU
if _, err := module.store.GetGroup(ctx, orgID, groupID); err != nil {
return err
}
err := module.store.CreateMapper(ctx, mapper)
if err != nil {
return err
}
agentConf.NotifyConfigUpdate(ctx)
return nil
return module.store.CreateMapper(ctx, mapper)
}
func (module *module) UpdateMapper(ctx context.Context, orgID, groupID, id valuer.UUID, fieldContext spantypes.FieldContext, config *spantypes.SpanMapperConfig, enabled *bool, updatedBy string) error {
@@ -87,72 +66,9 @@ func (module *module) UpdateMapper(ctx context.Context, orgID, groupID, id value
return err
}
mapper.Update(fieldContext, config, enabled, updatedBy)
err = module.store.UpdateMapper(ctx, mapper)
if err != nil {
return err
}
agentConf.NotifyConfigUpdate(ctx)
return nil
return module.store.UpdateMapper(ctx, mapper)
}
func (module *module) DeleteMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error {
err := module.store.DeleteMapper(ctx, orgID, groupID, id)
if err != nil {
return err
}
agentConf.NotifyConfigUpdate(ctx)
return nil
}
func (module *module) AgentFeatureType() agentConf.AgentFeatureType {
return spantypes.SpanAttrMappingFeatureType
}
func (module *module) RecommendAgentConfig(orgID valuer.UUID, currentConfYaml []byte, configVersion *opamptypes.AgentConfigVersion) ([]byte, string, error) {
ctx := context.Background()
enabled, err := module.listEnabledGroupsWithMappers(ctx, orgID)
if err != nil {
return nil, "", err
}
updatedConf, err := spantypes.GenerateCollectorConfigWithSpanMapperProcessor(currentConfYaml, enabled)
if err != nil {
return nil, "", err
}
serialized, err := json.Marshal(enabled)
if err != nil {
return nil, "", err
}
return updatedConf, string(serialized), nil
}
// listEnabledGroupsWithMappers returns groups with their mappers.
func (module *module) listEnabledGroupsWithMappers(ctx context.Context, orgID valuer.UUID) ([]*spantypes.SpanMapperGroupWithMappers, error) {
enabled := true
groups, err := module.store.ListGroups(ctx, orgID, &spantypes.ListSpanMapperGroupsQuery{Enabled: &enabled})
if err != nil {
return nil, err
}
out := make([]*spantypes.SpanMapperGroupWithMappers, 0, len(groups))
for _, g := range groups {
mappers, err := module.store.ListMappers(ctx, orgID, g.ID)
if err != nil {
return nil, err
}
if len(mappers) == 0 {
continue
}
enabledMappers := make([]*spantypes.SpanMapper, 0, len(mappers))
for _, m := range mappers {
if m.Enabled {
enabledMappers = append(enabledMappers, m)
}
}
out = append(out, &spantypes.SpanMapperGroupWithMappers{Group: g, Mappers: enabledMappers})
}
return out, nil
return module.store.DeleteMapper(ctx, orgID, groupID, id)
}

View File

@@ -4,16 +4,12 @@ import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// Module defines the business logic for span attribute mapping groups and mappers.
type Module interface {
// Since this module interacts with OpAMP, it must implement the AgentFeature interface.
agentConf.AgentFeature
// Group operations
ListGroups(ctx context.Context, orgID valuer.UUID, q *spantypes.ListSpanMapperGroupsQuery) ([]*spantypes.SpanMapperGroup, error)
GetGroup(ctx context.Context, orgID, id valuer.UUID) (*spantypes.SpanMapperGroup, error)

View File

@@ -335,10 +335,8 @@ func (q *querier) applyFormulas(ctx context.Context, results map[string]*qbtypes
}
case qbtypes.RequestTypeScalar:
result := q.processScalarFormula(ctx, results, formula, req)
if result != nil {
result = q.applySeriesLimit(result, formula.Limit, formula.Order)
results[name] = result
}
// For scalar results, apply limit by processScalarFormula itself since it needs to be applied before converting back to scalar format
results[name] = result
}
}
@@ -526,6 +524,9 @@ func (q *querier) processScalarFormula(
return nil
}
// Apply ordering (and limit) before converting to scalar format.
formulaSeries = qbtypes.ApplySeriesLimit(formulaSeries, formula.Order, formula.Limit)
// Convert back to scalar format
scalarResult := &qbtypes.ScalarData{
QueryName: formula.Name,

View File

@@ -1,15 +1,155 @@
package querier
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// scalarInputResult builds a ScalarData result with one group column ("service")
// and one aggregation column ("__result"), holding the provided (service, value) rows.
func scalarInputResult(queryName string, rows []struct {
service string
value float64
}) *qbtypes.Result {
serviceKey := telemetrytypes.TelemetryFieldKey{
Name: "service",
FieldDataType: telemetrytypes.FieldDataTypeString,
}
resultKey := telemetrytypes.TelemetryFieldKey{
Name: "__result",
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
}
data := make([][]any, 0, len(rows))
for _, r := range rows {
data = append(data, []any{r.service, r.value})
}
return &qbtypes.Result{
Value: &qbtypes.ScalarData{
QueryName: queryName,
Columns: []*qbtypes.ColumnDescriptor{
{
TelemetryFieldKey: serviceKey,
QueryName: queryName,
Type: qbtypes.ColumnTypeGroup,
},
{
TelemetryFieldKey: resultKey,
QueryName: queryName,
AggregationIndex: 0,
Type: qbtypes.ColumnTypeAggregation,
},
},
Data: data,
},
}
}
func TestProcessScalarFormula_AppliesOrderAndLimit(t *testing.T) {
q := &querier{
logger: instrumentationtest.New().Logger(),
}
// Mimic what a dashboard emits: orderBy keyed by the formula name ("F1"),
// which applyFormulas rewrites to __result before sorting.
orderByFormula := func(name string, dir qbtypes.OrderDirection) []qbtypes.OrderBy {
return []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: name,
},
},
Direction: dir,
},
}
}
// A+B per service: a=101, b=11, c=2
makeInputs := func() map[string]*qbtypes.Result {
return map[string]*qbtypes.Result{
"A": scalarInputResult("A", []struct {
service string
value float64
}{
{"a", 100},
{"b", 10},
{"c", 1},
}),
"B": scalarInputResult("B", []struct {
service string
value float64
}{
{"a", 1},
{"b", 0},
{"c", 1},
}),
}
}
makeReq := func(formula qbtypes.QueryBuilderFormula) *qbtypes.QueryRangeRequest {
return &qbtypes.QueryRangeRequest{
RequestType: qbtypes.RequestTypeScalar,
CompositeQuery: qbtypes.CompositeQuery{
Queries: []qbtypes.QueryEnvelope{
{Type: qbtypes.QueryTypeBuilder, Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{Name: "A"}},
{Type: qbtypes.QueryTypeBuilder, Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{Name: "B"}},
{Type: qbtypes.QueryTypeFormula, Spec: formula},
},
},
}
}
t.Run("F1 desc with limit truncates and sorts", func(t *testing.T) {
formula := qbtypes.QueryBuilderFormula{
Name: "F1",
Expression: "A + B",
Order: orderByFormula("F1", qbtypes.OrderDirectionDesc),
Limit: 2,
}
out := q.applyFormulas(context.Background(), makeInputs(), makeReq(formula))
got, ok := out["F1"]
require.True(t, ok, "formula result missing")
scalar, ok := got.Value.(*qbtypes.ScalarData)
require.True(t, ok, "expected *ScalarData, got %T", got.Value)
// Limit=2 + F1 desc: the two largest __result rows in descending order.
require.Len(t, scalar.Data, 2, "limit=2 was ignored before the fix")
require.Equal(t, "a", scalar.Data[0][0])
require.InDelta(t, 101.0, scalar.Data[0][1].(float64), 1e-9)
require.Equal(t, "b", scalar.Data[1][0])
require.InDelta(t, 10.0, scalar.Data[1][1].(float64), 1e-9)
})
t.Run("F1 desc without limit sorts all rows", func(t *testing.T) {
formula := qbtypes.QueryBuilderFormula{
Name: "F1",
Expression: "A / B",
Order: orderByFormula("F1", qbtypes.OrderDirectionAsc),
}
out := q.applyFormulas(context.Background(), makeInputs(), makeReq(formula))
got, ok := out["F1"]
require.True(t, ok)
scalar, ok := got.Value.(*qbtypes.ScalarData)
require.True(t, ok)
require.Len(t, scalar.Data, 2)
require.Equal(t, "c", scalar.Data[0][0])
require.InDelta(t, 1.0, scalar.Data[0][1].(float64), 1e-9)
require.Equal(t, "a", scalar.Data[1][0])
require.InDelta(t, 100.0, scalar.Data[1][1].(float64), 1e-9)
})
}
// Multiple series with different number of labels, shouldn't panic and should align labels correctly.
func TestConvertTimeSeriesDataToScalar_RaggedLabels(t *testing.T) {
label := func(name string, value any) *qbtypes.Label {

View File

@@ -1784,6 +1784,15 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
fineGrainedAuthz := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureUseFineGrainedAuthz, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureUseFineGrainedAuthz.String()),
Active: fineGrainedAuthz,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -769,6 +769,13 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
// Clamp the top-level Step for PromQL
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypePromQL {
if minStep := common.MinAllowedStepInterval(queryRangeParams.Start, queryRangeParams.End); queryRangeParams.Step < minStep {
queryRangeParams.Step = minStep
}
}
// prepare the variables for the corresponding query type
formattedVars := make(map[string]interface{})
for name, value := range queryRangeParams.Variables {

View File

@@ -41,6 +41,11 @@ func (c *GetWaterfallSpansForTraceWithMetadataCache) Clone() cachetypes.Cacheabl
}
}
func (c *GetWaterfallSpansForTraceWithMetadataCache) Cost() int64 {
const perSpanBytes = 256
return int64(c.TotalSpans) * perSpanBytes
}
func (c *GetWaterfallSpansForTraceWithMetadataCache) MarshalBinary() (data []byte, err error) {
return json.Marshal(c)
}
@@ -66,6 +71,16 @@ func (c *GetFlamegraphSpansForTraceCache) Clone() cachetypes.Cacheable {
}
}
func (c *GetFlamegraphSpansForTraceCache) Cost() int64 {
const perSpanBytes = 128
var spans int64
for _, row := range c.SelectedSpans {
spans += int64(len(row))
}
spans += int64(len(c.TraceRoots))
return spans * perSpanBytes
}
func (c *GetFlamegraphSpansForTraceCache) MarshalBinary() (data []byte, err error) {
return json.Marshal(c)
}

View File

@@ -32,30 +32,28 @@ import (
)
type PrepareTaskOptions struct {
Rule *ruletypes.PostableRule
TaskName string
RuleStore ruletypes.RuleStore
MaintenanceStore ruletypes.MaintenanceStore
Querier querier.Querier
Logger *slog.Logger
Cache cache.Cache
ManagerOpts *ManagerOptions
NotifyFunc NotifyFunc
SQLStore sqlstore.SQLStore
OrgID valuer.UUID
Rule *ruletypes.PostableRule
TaskName string
RuleStore ruletypes.RuleStore
Querier querier.Querier
Logger *slog.Logger
Cache cache.Cache
ManagerOpts *ManagerOptions
NotifyFunc NotifyFunc
SQLStore sqlstore.SQLStore
OrgID valuer.UUID
}
type PrepareTestRuleOptions struct {
Rule *ruletypes.PostableRule
RuleStore ruletypes.RuleStore
MaintenanceStore ruletypes.MaintenanceStore
Querier querier.Querier
Logger *slog.Logger
Cache cache.Cache
ManagerOpts *ManagerOptions
NotifyFunc NotifyFunc
SQLStore sqlstore.SQLStore
OrgID valuer.UUID
Rule *ruletypes.PostableRule
RuleStore ruletypes.RuleStore
Querier querier.Querier
Logger *slog.Logger
Cache cache.Cache
ManagerOpts *ManagerOptions
NotifyFunc NotifyFunc
SQLStore sqlstore.SQLStore
OrgID valuer.UUID
}
const taskNameSuffix = "webAppEditor"
@@ -89,7 +87,7 @@ type ManagerOptions struct {
Alertmanager alertmanager.Alertmanager
OrgGetter organization.Getter
RuleStore ruletypes.RuleStore
MaintenanceStore ruletypes.MaintenanceStore
MaintenanceStore alertmanagertypes.MaintenanceStore
SQLStore sqlstore.SQLStore
QueryParser queryparser.QueryParser
}
@@ -103,7 +101,7 @@ type Manager struct {
block chan struct{}
// datastore to store alert definitions
ruleStore ruletypes.RuleStore
maintenanceStore ruletypes.MaintenanceStore
maintenanceStore alertmanagertypes.MaintenanceStore
logger *slog.Logger
cache cache.Cache
@@ -134,7 +132,6 @@ func defaultOptions(o *ManagerOptions) *ManagerOptions {
}
func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
rules := make([]Rule, 0)
var task Task
@@ -159,7 +156,6 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
WithMetadataStore(opts.ManagerOpts.MetadataStore),
WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
return task, err
}
@@ -167,7 +163,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
rules = append(rules, tr)
// create ch rule task for evaluation
task = newTask(TaskTypeCh, opts.TaskName, taskNameSuffix, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(TaskTypeCh, opts.TaskName, taskNameSuffix, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
@@ -183,7 +179,6 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
WithMetadataStore(opts.ManagerOpts.MetadataStore),
WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
return task, err
}
@@ -191,7 +186,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
rules = append(rules, pr)
// create promql rule task for evaluation
task = newTask(TaskTypeProm, opts.TaskName, taskNameSuffix, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
task = newTask(TaskTypeProm, opts.TaskName, taskNameSuffix, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
} else {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
@@ -237,10 +232,11 @@ func (m *Manager) RuleStore() ruletypes.RuleStore {
return m.ruleStore
}
func (m *Manager) MaintenanceStore() ruletypes.MaintenanceStore {
func (m *Manager) MaintenanceStore() alertmanagertypes.MaintenanceStore {
return m.maintenanceStore
}
// TODO(jatinderjit): remove (unused)?
func (m *Manager) Pause(b bool) {
m.mtx.Lock()
defer m.mtx.Unlock()
@@ -430,19 +426,17 @@ func (m *Manager) editTask(_ context.Context, orgID valuer.UUID, rule *ruletypes
m.logger.Debug("editing a rule task", "name", taskName)
newTask, err := m.prepareTaskFunc(PrepareTaskOptions{
Rule: rule,
TaskName: taskName,
RuleStore: m.ruleStore,
MaintenanceStore: m.maintenanceStore,
Querier: m.opts.Querier,
Logger: m.opts.Logger,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.notifyFunc,
SQLStore: m.sqlstore,
OrgID: orgID,
Rule: rule,
TaskName: taskName,
RuleStore: m.ruleStore,
Querier: m.opts.Querier,
Logger: m.opts.Logger,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.notifyFunc,
SQLStore: m.sqlstore,
OrgID: orgID,
})
if err != nil {
m.logger.Error("loading tasks failed", errors.Attr(err))
return errors.NewInvalidInputf(errors.CodeInvalidInput, "error preparing rule with given parameters, previous rule set restored")
@@ -643,19 +637,17 @@ func (m *Manager) addTask(_ context.Context, orgID valuer.UUID, rule *ruletypes.
m.logger.Debug("adding a new rule task", "name", taskName)
newTask, err := m.prepareTaskFunc(PrepareTaskOptions{
Rule: rule,
TaskName: taskName,
RuleStore: m.ruleStore,
MaintenanceStore: m.maintenanceStore,
Querier: m.opts.Querier,
Logger: m.opts.Logger,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.notifyFunc,
SQLStore: m.sqlstore,
OrgID: orgID,
Rule: rule,
TaskName: taskName,
RuleStore: m.ruleStore,
Querier: m.opts.Querier,
Logger: m.opts.Logger,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.notifyFunc,
SQLStore: m.sqlstore,
OrgID: orgID,
})
if err != nil {
m.logger.Error("creating rule task failed", "name", taskName, errors.Attr(err))
return errors.NewInvalidInputf(errors.CodeInvalidInput, "error loading rules, previous rule set restored")
@@ -703,7 +695,6 @@ func (m *Manager) RuleTasks() []Task {
// RuleTasksWithoutLock returns the list of manager's rule tasks without
// acquiring a lock on the manager.
func (m *Manager) RuleTasksWithoutLock() []Task {
rgs := make([]Task, 0, len(m.tasks))
for _, g := range m.tasks {
rgs = append(rgs, g)
@@ -897,7 +888,6 @@ func (m *Manager) GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.Getta
// the task state. For example - if a stored rule is disabled, then
// there is no task running against it.
func (m *Manager) syncRuleStateWithTask(ctx context.Context, orgID valuer.UUID, taskName string, rule *ruletypes.PostableRule) error {
if rule.Disabled {
// check if rule has any task running
if _, ok := m.tasks[taskName]; ok {
@@ -1029,16 +1019,15 @@ func (m *Manager) TestNotification(ctx context.Context, orgID valuer.UUID, ruleS
}
alertCount, err := m.prepareTestRuleFunc(PrepareTestRuleOptions{
Rule: &parsedRule,
RuleStore: m.ruleStore,
MaintenanceStore: m.maintenanceStore,
Querier: m.opts.Querier,
Logger: m.opts.Logger,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.testNotifyFunc,
SQLStore: m.sqlstore,
OrgID: orgID,
Rule: &parsedRule,
RuleStore: m.ruleStore,
Querier: m.opts.Querier,
Logger: m.opts.Logger,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.testNotifyFunc,
SQLStore: m.sqlstore,
OrgID: orgID,
})
return alertCount, err

View File

@@ -15,7 +15,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// PromRuleTask is a promql rule executor
@@ -39,14 +38,11 @@ type PromRuleTask struct {
pause bool
logger *slog.Logger
notify NotifyFunc
maintenanceStore ruletypes.MaintenanceStore
orgID valuer.UUID
}
// NewPromRuleTask holds rules that have promql condition
// and evaluates the rule at a given frequency
func NewPromRuleTask(name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID valuer.UUID) *PromRuleTask {
func NewPromRuleTask(name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc) *PromRuleTask {
opts.Logger.Info("initiating a new rule group", "name", name, "frequency", frequency)
if frequency == 0 {
@@ -63,10 +59,8 @@ func NewPromRuleTask(name, file string, frequency time.Duration, rules []Rule, o
seriesInPreviousEval: make([]map[string]plabels.Labels, len(rules)),
done: make(chan struct{}),
terminated: make(chan struct{}),
notify: notify,
maintenanceStore: maintenanceStore,
logger: opts.Logger,
orgID: orgID,
notify: notify,
logger: opts.Logger,
}
}
@@ -330,30 +324,12 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
}()
g.logger.InfoContext(ctx, "promql rule task", "name", g.name, "eval_started_at", ts)
maintenance, err := g.maintenanceStore.ListPlannedMaintenance(ctx, g.orgID.StringValue())
if err != nil {
g.logger.ErrorContext(ctx, "error in processing sql query", errors.Attr(err))
}
for i, rule := range g.rules {
if rule == nil {
continue
}
shouldSkip := false
for _, m := range maintenance {
g.logger.InfoContext(ctx, "checking if rule should be skipped", slog.String("rule.id", rule.ID()), slog.Any("maintenance", m))
if m.ShouldSkip(rule.ID(), ts) {
shouldSkip = true
break
}
}
if shouldSkip {
g.logger.InfoContext(ctx, "rule should be skipped", slog.String("rule.id", rule.ID()))
continue
}
select {
case <-g.done:
return

View File

@@ -2,20 +2,18 @@ package rules
import (
"context"
"log/slog"
"runtime/debug"
"sort"
"sync"
"time"
"log/slog"
opentracing "github.com/opentracing/opentracing-go"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// RuleTask holds a rule (with composite queries)
@@ -37,34 +35,28 @@ type RuleTask struct {
pause bool
notify NotifyFunc
maintenanceStore ruletypes.MaintenanceStore
orgID valuer.UUID
}
const DefaultFrequency = 1 * time.Minute
// NewRuleTask makes a new RuleTask with the given name, options, and rules.
func NewRuleTask(name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID valuer.UUID) *RuleTask {
func NewRuleTask(name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc) *RuleTask {
if frequency == 0 {
frequency = DefaultFrequency
}
opts.Logger.Info("initiating a new rule task", "name", name, "frequency", frequency)
return &RuleTask{
name: name,
file: file,
pause: false,
frequency: frequency,
rules: rules,
opts: opts,
logger: opts.Logger,
done: make(chan struct{}),
terminated: make(chan struct{}),
notify: notify,
maintenanceStore: maintenanceStore,
orgID: orgID,
name: name,
file: file,
pause: false,
frequency: frequency,
rules: rules,
opts: opts,
logger: opts.Logger,
done: make(chan struct{}),
terminated: make(chan struct{}),
notify: notify,
}
}
@@ -72,6 +64,7 @@ func NewRuleTask(name, file string, frequency time.Duration, rules []Rule, opts
func (g *RuleTask) Name() string { return g.name }
// Key returns the group key
// TODO(jatinderjit): remove (unused)?
func (g *RuleTask) Key() string {
return g.name + ";" + g.file
}
@@ -83,7 +76,7 @@ func (g *RuleTask) Type() TaskType { return TaskTypeCh }
func (g *RuleTask) Rules() []Rule { return g.rules }
// Interval returns the group's interval.
// TODO: remove (unused)?
// TODO(jatinderjit): remove (unused)?
func (g *RuleTask) Interval() time.Duration { return g.frequency }
func (g *RuleTask) Pause(b bool) {
@@ -261,7 +254,6 @@ func nameAndLabels(rule Rule) string {
// Rules are matched based on their name and labels. If there are duplicates, the
// first is matched with the first, second with the second etc.
func (g *RuleTask) CopyState(fromTask Task) error {
from, ok := fromTask.(*RuleTask)
if !ok {
return errors.NewInternalf(errors.CodeInternal, "invalid from task for copy")
@@ -306,7 +298,6 @@ func (g *RuleTask) CopyState(fromTask Task) error {
// Eval runs a single evaluation cycle in which all rules are evaluated sequentially.
func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
defer func() {
if r := recover(); r != nil {
g.logger.ErrorContext(
@@ -318,31 +309,11 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
g.logger.DebugContext(ctx, "rule task eval started", "name", g.name, "start_time", ts)
maintenance, err := g.maintenanceStore.ListPlannedMaintenance(ctx, g.orgID.StringValue())
if err != nil {
g.logger.ErrorContext(ctx, "error in processing sql query", errors.Attr(err))
}
for i, rule := range g.rules {
if rule == nil {
continue
}
shouldSkip := false
for _, m := range maintenance {
g.logger.InfoContext(ctx, "checking if rule should be skipped", slog.String("rule.id", rule.ID()), slog.Any("maintenance", m))
if m.ShouldSkip(rule.ID(), ts) {
shouldSkip = true
break
}
}
if shouldSkip {
g.logger.InfoContext(ctx, "rule should be skipped", slog.String("rule.id", rule.ID()))
continue
}
select {
case <-g.done:
return
@@ -382,7 +353,6 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
}
rule.SendAlerts(ctx, ts, g.opts.ResendDelay, g.frequency, g.notify)
}(i, rule)
}
}

View File

@@ -3,9 +3,6 @@ package rules
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type TaskType string
@@ -32,9 +29,9 @@ type Task interface {
// newTask returns an appropriate group for
// rule type
func newTask(taskType TaskType, name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID valuer.UUID) Task {
func newTask(taskType TaskType, name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc) Task {
if taskType == TaskTypeCh {
return NewRuleTask(name, file, frequency, rules, opts, notify, maintenanceStore, orgID)
return NewRuleTask(name, file, frequency, rules, opts, notify)
}
return NewPromRuleTask(name, file, frequency, rules, opts, notify, maintenanceStore, orgID)
return NewPromRuleTask(name, file, frequency, rules, opts, notify)
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -44,5 +45,5 @@ type Ruler interface {
// MaintenanceStore returns the store for planned maintenance / downtime schedules.
// TODO: expose downtime CRUD as methods on Ruler directly instead of leaking the
// store interface. The handler should not call store methods directly.
MaintenanceStore() ruletypes.MaintenanceStore
MaintenanceStore() alertmanagertypes.MaintenanceStore
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -194,7 +195,7 @@ func (handler *handler) ListDowntimeSchedules(rw http.ResponseWriter, req *http.
return
}
var params ruletypes.ListPlannedMaintenanceParams
var params alertmanagertypes.ListPlannedMaintenanceParams
if err := binding.Query.BindQuery(req.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
@@ -207,7 +208,7 @@ func (handler *handler) ListDowntimeSchedules(rw http.ResponseWriter, req *http.
}
if params.Active != nil {
activeSchedules := make([]*ruletypes.PlannedMaintenance, 0)
activeSchedules := make([]*alertmanagertypes.PlannedMaintenance, 0)
for _, schedule := range schedules {
now := time.Now().In(time.FixedZone(schedule.Schedule.Timezone, 0))
if schedule.IsActive(now) == *params.Active {
@@ -218,7 +219,7 @@ func (handler *handler) ListDowntimeSchedules(rw http.ResponseWriter, req *http.
}
if params.Recurring != nil {
recurringSchedules := make([]*ruletypes.PlannedMaintenance, 0)
recurringSchedules := make([]*alertmanagertypes.PlannedMaintenance, 0)
for _, schedule := range schedules {
if schedule.IsRecurring() == *params.Recurring {
recurringSchedules = append(recurringSchedules, schedule)
@@ -253,7 +254,7 @@ func (handler *handler) CreateDowntimeSchedule(rw http.ResponseWriter, req *http
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
schedule := new(ruletypes.PostablePlannedMaintenance)
schedule := new(alertmanagertypes.PostablePlannedMaintenance)
if err := binding.JSON.BindBody(req.Body, schedule); err != nil {
render.Error(rw, err)
return
@@ -283,7 +284,7 @@ func (handler *handler) UpdateDowntimeScheduleByID(rw http.ResponseWriter, req *
return
}
schedule := new(ruletypes.PostablePlannedMaintenance)
schedule := new(alertmanagertypes.PostablePlannedMaintenance)
if err := binding.JSON.BindBody(req.Body, schedule); err != nil {
render.Error(rw, err)
return

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -16,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -44,7 +46,7 @@ func NewFactory(
) factory.ProviderFactory[ruler.Ruler, ruler.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config ruler.Config) (ruler.Ruler, error) {
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
maintenanceStore := sqlalertmanagerstore.NewMaintenanceStore(sqlstore, providerSettings)
managerOpts := &rules.ManagerOptions{
TelemetryStore: telemetryStore,
@@ -129,6 +131,6 @@ func (provider *provider) TestNotification(ctx context.Context, orgID valuer.UUI
return provider.manager.TestNotification(ctx, orgID, ruleStr)
}
func (provider *provider) MaintenanceStore() ruletypes.MaintenanceStore {
func (provider *provider) MaintenanceStore() alertmanagertypes.MaintenanceStore {
return provider.manager.MaintenanceStore()
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfmanagertest"
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
@@ -40,7 +41,8 @@ func TestNewHandlers(t *testing.T) {
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstore), sharder)
notificationManager := nfmanagertest.NewMock()
require.NoError(t, err)
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{}, sqlstore, orgGetter, notificationManager)
maintenanceStore := sqlalertmanagerstore.NewMaintenanceStore(sqlstore, providerSettings)
alertmanager, err := signozalertmanager.New(providerSettings, alertmanager.Config{}, sqlstore, orgGetter, notificationManager, maintenanceStore)
require.NoError(t, err)
tokenizer := tokenizertest.NewMockTokenizer(t)
emailing := emailingtest.New()

View File

@@ -7,6 +7,7 @@ import (
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfmanagertest"
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
@@ -41,7 +42,8 @@ func TestNewModules(t *testing.T) {
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstore), sharder)
notificationManager := nfmanagertest.NewMock()
require.NoError(t, err)
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{}, sqlstore, orgGetter, notificationManager)
maintenanceStore := sqlalertmanagerstore.NewMaintenanceStore(sqlstore, providerSettings)
alertmanager, err := signozalertmanager.New(providerSettings, alertmanager.Config{}, sqlstore, orgGetter, notificationManager, maintenanceStore)
require.NoError(t, err)
tokenizer := tokenizertest.NewMockTokenizer(t)
emailing := emailingtest.New()

View File

@@ -203,6 +203,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewMigrateMetaresourcesTuplesFactory(sqlstore),
sqlmigration.NewAddTagsFactory(sqlstore, sqlschema),
sqlmigration.NewAddRoleCRUDTuplesFactory(sqlstore),
sqlmigration.NewAddLabelExpressionToPlannedMaintenanceFactory(sqlstore, sqlschema),
)
}
@@ -229,9 +230,14 @@ func NewNotificationManagerProviderFactories(routeStore alertmanagertypes.RouteS
)
}
func NewAlertmanagerProviderFactories(sqlstore sqlstore.SQLStore, orgGetter organization.Getter, nfManager nfmanager.NotificationManager) factory.NamedMap[factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config]] {
func NewAlertmanagerProviderFactories(
sqlstore sqlstore.SQLStore,
orgGetter organization.Getter,
nfManager nfmanager.NotificationManager,
maintenanceStore alertmanagertypes.MaintenanceStore,
) factory.NamedMap[factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config]] {
return factory.MustNewNamedMap(
signozalertmanager.NewFactory(sqlstore, orgGetter, nfManager),
signozalertmanager.NewFactory(sqlstore, orgGetter, nfManager, maintenanceStore),
)
}

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