Compare commits

...

14 Commits

Author SHA1 Message Date
nityanandagohain
aa140b3456 feat: trace based filters for logs, supporting aggregations as well 2026-05-21 18:47:25 +05:30
primus-bot[bot]
fb3e316ce9 chore(release): bump to v0.125.1 (#11381)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-05-20 17:46:41 +00:00
Manika Malhotra
b753b95a8a chore: replace antd badge with signozhq badge (#11377)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: replace antd badge with signozhq badge

* chore: add badge to banned components
2026-05-20 14:24:52 +00:00
Vinicius Lourenço
4757550189 fix(alerts): ensure edit alert name is updated correctly and not override after save (#11348) 2026-05-20 13:57:52 +00:00
Vinicius Lourenço
96ad37fea9 fix(tanstack-table): reset page to 1 when change page size (#11344)
* fix(tanstack-table): reset page to 1 when change page size

* fix(tanstack): ensure page change callback is called
2026-05-20 13:57:40 +00:00
Vinicius Lourenço
5419e8461c fix(alerts-new): show tabs and breadcrumbs on create alert (#11316)
* fix(alerts-new): show tabs and breadcrumbs on create alert

* fix(pr): address comments

* fix(composite-query): not automatically showing the create alerts when have this query param

* fix(breadcrumb): align ui with periscope
2026-05-20 13:29:23 +00:00
Aditya Singh
e634eb4452 fix: expand waterfall ancestors on flamegraph click (#11373)
* fix: scroll to span in frontend mode when nodes are collapsed

* fix: fix tests

* feat: change default span details position
2026-05-20 13:28:22 +00:00
Piyush Singariya
a50bc53f4c chore: Accept body as Map in FE (#11291)
* fix: backend changes for message key postprocessing

* fix: message postprocessing

* chore: update in e2e tests

* fix: table view

* fix: support body as json in FE

* chore: separate frontend from backend changes

* chore: remove dead code
2026-05-20 12:52:53 +00:00
Srikanth Chekuri
9f60bdf54a chore: create source field in dashboards (#11367)
* chore: create source field in dashboards

* chore: consolidate checks to module

* chore: run generate

* chore: address review comments

* chore: separate test file

* chore: address review comments
2026-05-20 12:37:25 +00:00
Nikhil Mantri
e41639dea0 chore: function refactor (#11371) 2026-05-20 12:10:42 +00:00
Piyush Singariya
847bc71f4e fix: postprocess json logs message key (#11189)
* fix: backend changes for message key postprocessing

* fix: message postprocessing

* chore: update in e2e tests

* fix: table view

* chore: separate frontend from backend changes

* fix: integration tests
2026-05-20 11:46:27 +00:00
Ashwin Bhatkal
8d7d3e5c64 fix(metrics-explorer): show actual timestamp in metric sidesheet 'Last Received' tooltip (#11370) 2026-05-20 10:51:27 +00:00
primus-bot[bot]
74c875ec79 chore(release): bump to v0.125.0 (#11369)
Some checks failed
Release Drafter / update_release_draft (push) Has been cancelled
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
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-05-20 07:18:40 +00:00
Jatinderjit Singh
a27b7d3d8e move planned maintenance to alertmanager pipeline (#11130)
* 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>

* 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.

* 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>

* 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>

* 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>

* refactor: pass MaintenanceMuter directly to pipelineBuilder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: remove dead orgID param from task constructors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* rename buildReceiverStage -> createReceiverStage

* 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>

* 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>

* 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>

* 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>

* remove redundant MemMarker wrapper

* 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>

* code cleanup

* 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>

* 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>

* Update schema changes

* Re-add marker

* fix NewMaintenanceStore in tests

* Go lint fixes

* 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>

* 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>

* cleanup test

* test: add e2e muting tests for maintenance window behaviour

* fix updates: omit empty endTime from serialization

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 04:20:16 +00:00
126 changed files with 4282 additions and 1341 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

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.124.0
image: signoz/signoz:v0.125.1
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.124.0
image: signoz/signoz:v0.125.1
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.124.0}
image: signoz/signoz:${VERSION:-v0.125.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.124.0}
image: signoz/signoz:${VERSION:-v0.125.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -96,6 +96,53 @@ 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'
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 +259,23 @@ components:
required:
- name
type: object
AlertmanagertypesPostablePlannedMaintenance:
properties:
alertIds:
items:
type: string
nullable: true
type: array
description:
type: string
name:
type: string
schedule:
$ref: '#/components/schemas/AlertmanagertypesSchedule'
required:
- name
- schedule
type: object
AlertmanagertypesPostableRoutePolicy:
properties:
channels:
@@ -237,6 +301,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:
@@ -2224,6 +2342,8 @@ components:
type: boolean
org_id:
type: string
source:
$ref: '#/components/schemas/DashboardtypesSource'
updatedAt:
format: date-time
type: string
@@ -2253,6 +2373,12 @@ components:
timeRangeEnabled:
type: boolean
type: object
DashboardtypesSource:
enum:
- user
- system
- integration
type: object
DashboardtypesStorableDashboardData:
additionalProperties: {}
type: object
@@ -5137,17 +5263,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 +5290,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 +5342,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 +5353,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 +5472,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 +8032,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
type: array
status:
type: string
@@ -8067,7 +8075,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
responses:
"201":
content:
@@ -8075,7 +8083,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
status:
type: string
required:
@@ -8178,7 +8186,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
status:
type: string
required:
@@ -8232,7 +8240,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
responses:
"204":
description: No Content

View File

@@ -49,6 +49,14 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
if err != nil {
return err
}
if err := dashboard.ErrIfNotPublishable(); err != nil {
return err
}
storablePublicDashboard, err := module.store.GetPublic(ctx, publicDashboard.DashboardID.StringValue())
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
@@ -129,6 +137,14 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
if err != nil {
return err
}
if err := dashboard.ErrIfNotPublishable(); err != nil {
return err
}
return module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard))
}
@@ -138,6 +154,10 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
return err
}
if err := dashboard.ErrIfNotDeletable(); err != nil {
return err
}
if dashboard.Locked {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
}
@@ -168,6 +188,14 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, dashboardID)
if err != nil {
return err
}
if err := dashboard.ErrIfNotPublishable(); err != nil {
return err
}
err = module.store.DeletePublic(ctx, dashboardID.StringValue())
if err != nil {
return err

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

@@ -15,6 +15,7 @@
const BANNED_COMPONENTS = {
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
};
export default {

View File

@@ -47,7 +47,6 @@ export const TracesFunnels = Loadable(
import(/* webpackChunkName: "Traces Funnels" */ 'pages/TracesModulePage'),
);
export const TracesFunnelDetails = Loadable(
// eslint-disable-next-line sonarjs/no-identical-functions
() =>
import(
/* webpackChunkName: "Traces Funnel Details" */ 'pages/TracesModulePage'
@@ -313,13 +312,6 @@ export const PublicDashboardPage = Loadable(
),
);
export const AlertTypeSelectionPage = Loadable(
() =>
import(
/* webpackChunkName: "Alert Type Selection Page" */ 'pages/AlertTypeSelection'
),
);
export const MeterExplorerPage = Loadable(
() =>
import(/* webpackChunkName: "Meter Explorer Page" */ 'pages/MeterExplorer'),

View File

@@ -5,7 +5,6 @@ import {
AIAssistantPage,
AlertHistory,
AlertOverview,
AlertTypeSelectionPage,
AllAlertChannels,
AllErrors,
ApiMonitoring,
@@ -213,13 +212,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'LIST_ALL_ALERT',
},
{
path: ROUTES.ALERT_TYPE_SELECTION,
exact: true,
component: AlertTypeSelectionPage,
isPrivate: true,
key: 'ALERT_TYPE_SELECTION',
},
{
path: ROUTES.ALERTS_NEW,
exact: true,
@@ -533,18 +525,6 @@ export const LIST_LICENSES: AppRoutes = {
key: 'LIST_LICENSES',
};
export const oldRoutes = [
'/pipelines',
'/logs-explorer',
'/logs-explorer/live',
'/logs-save-views',
'/traces-save-views',
'/settings/access-tokens',
'/settings/api-keys',
'/messaging-queues',
'/alerts/edit',
];
export const oldNewRoutesMapping: Record<string, string> = {
'/pipelines': '/logs/pipelines',
'/logs-explorer': '/logs/logs-explorer',
@@ -555,7 +535,9 @@ export const oldNewRoutesMapping: Record<string, string> = {
'/settings/api-keys': '/settings/service-accounts',
'/messaging-queues': '/messaging-queues/overview',
'/alerts/edit': '/alerts/overview',
'/alerts/type-selection': '/alerts/new',
};
export const oldRoutes = Object.keys(oldNewRoutesMapping);
export const ROUTES_NOT_TO_BE_OVERRIDEN: string[] = [
ROUTES.WORKSPACE_LOCKED,

View File

@@ -18,6 +18,7 @@ import type {
} from 'react-query';
import type {
AlertmanagertypesPostablePlannedMaintenanceDTO,
CreateDowntimeSchedule201,
DeleteDowntimeScheduleByIDPathParameters,
GetDowntimeScheduleByID200,
@@ -25,7 +26,6 @@ import type {
ListDowntimeSchedules200,
ListDowntimeSchedulesParams,
RenderErrorResponseDTO,
RuletypesPostablePlannedMaintenanceDTO,
UpdateDowntimeScheduleByIDPathParameters,
} from '../sigNoz.schemas';
@@ -135,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,
});
};
@@ -154,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'];
@@ -174,7 +174,7 @@ export const getCreateDowntimeScheduleMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> }
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -188,7 +188,7 @@ export type CreateDowntimeScheduleMutationResult = NonNullable<
Awaited<ReturnType<typeof createDowntimeSchedule>>
>;
export type CreateDowntimeScheduleMutationBody =
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
| undefined;
export type CreateDowntimeScheduleMutationError =
ErrorType<RenderErrorResponseDTO>;
@@ -203,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));
@@ -403,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,
});
};
@@ -424,7 +424,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
},
TContext
>;
@@ -433,7 +433,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
},
TContext
> => {
@@ -450,7 +450,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -465,7 +465,7 @@ export type UpdateDowntimeScheduleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>
>;
export type UpdateDowntimeScheduleByIDMutationBody =
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
| undefined;
export type UpdateDowntimeScheduleByIDMutationError =
ErrorType<RenderErrorResponseDTO>;
@@ -482,7 +482,7 @@ export const useUpdateDowntimeScheduleByID = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
},
TContext
>;
@@ -491,7 +491,7 @@ export const useUpdateDowntimeScheduleByID = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
},
TContext
> => {

View File

@@ -134,6 +134,109 @@ 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?: string | null;
/**
* @type array,null
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: string;
}
export interface AlertmanagertypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: string;
recurrence?: AlertmanagertypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: string;
/**
* @type string
*/
timezone: string;
}
export interface AlertmanagertypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: AlertmanagertypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
status: AlertmanagertypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface ConfigAuthorizationDTO {
/**
* @type string
@@ -1597,6 +1700,22 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
wechat_configs?: ConfigWechatConfigDTO[];
};
export interface AlertmanagertypesPostablePlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
}
export interface AlertmanagertypesPostableRoutePolicyDTO {
/**
* @type array,null
@@ -2880,6 +2999,11 @@ export interface CoretypesPatchableObjectsDTO {
deletions: CoretypesObjectGroupDTO[] | null;
}
export enum DashboardtypesSourceDTO {
user = 'user',
system = 'system',
integration = 'integration',
}
export interface DashboardtypesDashboardDTO {
/**
* @type string
@@ -2903,6 +3027,7 @@ export interface DashboardtypesDashboardDTO {
* @type string
*/
org_id?: string;
source?: DashboardtypesSourceDTO;
/**
* @type string
* @format date-time
@@ -6116,15 +6241,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
@@ -6156,116 +6272,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?: string | null;
/**
* @type array,null
*/
repeatOn?: RuletypesRepeatOnDTO[] | null;
repeatType: RuletypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: string;
}
export interface RuletypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: string;
recurrence?: RuletypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: string;
/**
* @type string
*/
timezone: string;
}
export interface RuletypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @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?: string;
/**
* @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 };
@@ -7793,7 +7799,7 @@ export type ListDowntimeSchedules200 = {
/**
* @type array
*/
data: RuletypesPlannedMaintenanceDTO[];
data: AlertmanagertypesPlannedMaintenanceDTO[];
/**
* @type string
*/
@@ -7801,7 +7807,7 @@ export type ListDowntimeSchedules200 = {
};
export type CreateDowntimeSchedule201 = {
data: RuletypesPlannedMaintenanceDTO;
data: AlertmanagertypesPlannedMaintenanceDTO;
/**
* @type string
*/
@@ -7815,7 +7821,7 @@ export type GetDowntimeScheduleByIDPathParameters = {
id: string;
};
export type GetDowntimeScheduleByID200 = {
data: RuletypesPlannedMaintenanceDTO;
data: AlertmanagertypesPlannedMaintenanceDTO;
/**
* @type string
*/

View File

@@ -0,0 +1,17 @@
.breadcrumb {
padding-left: 16px;
ol {
align-items: center;
}
:global(.ant-breadcrumb-separator) {
color: var(--muted-foreground);
}
}
.divider {
border-color: var(--l1-border);
margin: 16px 0;
margin-top: 10px;
}

View File

@@ -0,0 +1,32 @@
import { Breadcrumb, Divider } from 'antd';
import styles from './AlertBreadcrumb.module.scss';
import BreadcrumbItem, { BreadcrumbItemConfig } from './BreadcrumbItem';
export interface AlertBreadcrumbProps {
items: BreadcrumbItemConfig[];
className?: string;
showDivider?: boolean;
}
function AlertBreadcrumb({
items,
className,
showDivider = true,
}: AlertBreadcrumbProps): JSX.Element {
const breadcrumbItems = items.map((item) => ({
title: <BreadcrumbItem {...item} />,
}));
return (
<>
<Breadcrumb
className={`${styles.breadcrumb} ${className || ''}`}
items={breadcrumbItems}
/>
{showDivider && <Divider className={styles.divider} />}
</>
);
}
export default AlertBreadcrumb;

View File

@@ -0,0 +1,9 @@
.item {
--button-padding: 0;
--button-font-size: var(--periscope-font-size-base);
}
.itemLast {
color: var(--muted-foreground);
font-size: var(--periscope-font-size-base);
}

View File

@@ -0,0 +1,45 @@
import { Button } from '@signozhq/ui/button';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isModifierKeyPressed } from 'utils/app';
import styles from './BreadcrumbItem.module.scss';
export type BreadcrumbItemConfig =
| {
title: string | null;
route?: string;
}
| {
title: string | null;
isLast?: true;
};
function BreadcrumbItem({
title,
...props
}: BreadcrumbItemConfig): JSX.Element {
const { safeNavigate } = useSafeNavigate();
if ('isLast' in props) {
return <div className={styles.itemLast}>{title}</div>;
}
return (
<Button
variant="ghost"
color="secondary"
className={styles.item}
onClick={(e: React.MouseEvent): void => {
if (!('route' in props) || !props.route) {
return;
}
safeNavigate(props.route, { newTab: isModifierKeyPressed(e) });
}}
>
{title}
</Button>
);
}
export default BreadcrumbItem;

View File

@@ -0,0 +1,6 @@
export { default } from './AlertBreadcrumb';
export {
default as BreadcrumbItem,
type BreadcrumbItemConfig,
} from './BreadcrumbItem';
export type { AlertBreadcrumbProps } from './AlertBreadcrumb';

View File

@@ -50,6 +50,7 @@ import {
import { JsonView } from 'periscope/components/JsonView';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { ILogBody } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -217,20 +218,17 @@ function LogDetailInner({
const logBody = useMemo(() => {
if (!isBodyJsonQueryEnabled) {
return log?.body || '';
return (log?.body as string) ?? '';
}
try {
const json = JSON.parse(log?.body || '');
if (typeof json?.message === 'string' && json.message !== '') {
return json.message;
}
return log?.body || '';
} catch {
return log?.body || '';
// Feature enabled: body is always a map; message is always a string
const bodyObj = log?.body as ILogBody;
if (!bodyObj) {
return '';
}
if (bodyObj.message) {
return bodyObj.message;
}
return JSON.stringify(bodyObj);
}, [isBodyJsonQueryEnabled, log?.body]);
const htmlBody = useMemo(

View File

@@ -9,7 +9,10 @@ import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import {
getBodyDisplayString,
getSanitizedLogBody,
} from 'container/LogDetailedView/utils';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
// hooks
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -99,7 +102,7 @@ function RawLogView({
// Check if body is selected
const showBody = selectedFields.some((field) => field.name === 'body');
if (showBody) {
parts.push(`${attributesText} ${data.body}`);
parts.push(`${attributesText} ${getBodyDisplayString(data.body)}`);
} else {
parts.push(attributesText);
}

View File

@@ -2,7 +2,10 @@ import type { ReactElement } from 'react';
import { useMemo } from 'react';
import TanStackTable from 'components/TanStackTableView';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import {
getBodyDisplayString,
getSanitizedLogBody,
} from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
@@ -87,7 +90,7 @@ export function useLogsTableColumns({
? {
id: 'body',
header: 'Body',
accessorFn: (log): string => log.body,
accessorFn: (log): string => getBodyDisplayString(log.body),
canBeHidden: false,
width: { default: '100%', min: 300 },
cell: ({ value, isActive }): ReactElement => (

View File

@@ -626,6 +626,10 @@ function TanStackTableInner<TData>(
onChange={(value): void => {
setLimit(+value);
pagination.onLimitChange?.(+value);
if (page !== 1) {
setPage(1);
pagination.onPageChange?.(1);
}
}}
items={paginationPageSizeItems}
/>

View File

@@ -401,6 +401,62 @@ describe('TanStackTableView Integration', () => {
expect(onLimitChange).toHaveBeenCalledWith(20);
});
});
it('resets page to 1 when limit changes', async () => {
const user = userEvent.setup();
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const onPageChange = jest.fn();
renderTanStackTable({
props: {
pagination: { total: 100, defaultPage: 1, defaultLimit: 10, onPageChange },
enableQueryParams: true,
},
onUrlUpdate,
});
await waitFor(() => {
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
// Navigate to page 2
const nav = screen.getByRole('navigation');
const page2 = Array.from(nav.querySelectorAll('button')).find(
(btn) => btn.textContent?.trim() === '2',
);
if (!page2) {
throw new Error('Page 2 button not found in pagination');
}
await user.click(page2);
await waitFor(() => {
const lastPage = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('page'))
.filter(Boolean)
.pop();
expect(lastPage).toBe('2');
});
// Change page size
const comboboxTrigger = document.querySelector(
'button[aria-haspopup="dialog"]',
) as HTMLElement;
await user.click(comboboxTrigger);
const option20 = await screen.findByRole('option', { name: '20' });
await user.click(option20);
// Verify page reset to 1 (nuqs removes default values from URL)
await waitFor(() => {
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
const lastPage = lastCall[0].searchParams.get('page');
expect(lastPage === '1' || lastPage === null).toBe(true);
expect(lastCall[0].searchParams.get('limit')).toBe('20');
});
// Verify onPageChange callback was called with 1
expect(onPageChange).toHaveBeenCalledWith(1);
});
});
describe('sorting', () => {

View File

@@ -29,7 +29,6 @@ const ROUTES = {
ALERTS_NEW: '/alerts/new',
ALERT_HISTORY: '/alerts/history',
ALERT_OVERVIEW: '/alerts/overview',
ALERT_TYPE_SELECTION: '/alerts/type-selection',
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/settings/channels/new',
CHANNELS_EDIT: '/settings/channels/edit/:channelId',

View File

@@ -1,12 +1,23 @@
import ROUTES from 'constants/routes';
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
import CreateAlertPage from 'pages/CreateAlert';
import { act, fireEvent, render } from 'tests/test-utils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_TO_TITLE, ALERT_TYPE_URL_MAP } from './constants';
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigationType: jest.fn(() => 'PUSH'),
useLocation: jest.fn(() => ({
pathname: '/alerts/new',
search: '',
hash: '',
state: null,
})),
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
}));
jest
.spyOn(usePrefillAlertConditions, 'usePrefillAlertConditions')
.mockReturnValue({
@@ -54,20 +65,13 @@ describe('Alert rule documentation redirection', () => {
window.open = mockWindowOpen;
});
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERT_TYPE_SELECTION}`,
}),
}));
beforeEach(() => {
act(() => {
renderResult = render(
<AlertTypeSelectionPage />,
<CreateAlertPage />,
{},
{
initialRoute: ROUTES.ALERT_TYPE_SELECTION,
initialRoute: ROUTES.ALERTS_NEW,
},
);
});

View File

@@ -15,6 +15,18 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigationType: jest.fn(() => 'PUSH'),
useLocation: jest.fn(() => ({
pathname: '/alerts/new',
search: 'ruleType=anomaly_rule',
hash: '',
state: null,
})),
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
}));
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({

View File

@@ -0,0 +1,75 @@
.create-alert-tabs {
&__extra {
display: flex;
align-items: center;
gap: 16px;
}
}
.create-alert-wrapper {
margin-top: 10px;
.divider {
border-color: var(--l1-border);
margin: 16px 0;
}
.breadcrumb-divider {
margin-top: 10px;
}
}
.create-alert__breadcrumb {
padding-left: 16px;
ol {
align-items: center;
}
.breadcrumb-item {
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
padding: 0;
}
.ant-breadcrumb-separator,
.breadcrumb-item--last {
color: var(--muted-foreground);
font-family: 'Geist Mono';
}
}
.alerts-container {
.top-level-tab.periscope-tab {
padding: 2px 0;
}
.ant-tabs {
&-nav {
padding: 0 8px;
margin-bottom: 0 !important;
&::before {
border-bottom: 1px solid var(--l1-border) !important;
}
}
&-tab {
&[data-node-key='TriggeredAlerts'] {
margin-left: 16px;
}
&:not(:first-of-type) {
margin-left: 24px !important;
}
[aria-selected='false'] {
.periscope-tab {
color: var(--l2-foreground);
}
}
}
}
}

View File

@@ -0,0 +1,111 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
import * as appHooks from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import SelectAlertType from '..';
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
describe('SelectAlertType', () => {
const mockOnSelect = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
useAppContextSpy.mockReturnValue(getAppContextMockState());
});
it('should render all alert type options when anomaly detection is enabled', () => {
useAppContextSpy.mockReturnValue({
...getAppContextMockState({}),
featureFlags: [
{
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
});
render(<SelectAlertType onSelect={mockOnSelect} />);
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
expect(screen.getByText('anomaly_based_alert')).toBeInTheDocument();
});
it('should render all alert type options except anomaly based alert when anomaly detection is disabled', () => {
render(<SelectAlertType onSelect={mockOnSelect} />);
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
expect(screen.queryByText('anomaly_based_alert')).not.toBeInTheDocument();
});
it('should call onSelect with metrics based alert type', () => {
render(<SelectAlertType onSelect={mockOnSelect} />);
fireEvent.click(screen.getByText('metric_based_alert'));
expect(mockOnSelect).toHaveBeenCalledWith(
AlertTypes.METRICS_BASED_ALERT,
false,
);
});
it('should call onSelect with anomaly based alert type', () => {
useAppContextSpy.mockReturnValue({
...getAppContextMockState({}),
featureFlags: [
{
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
});
render(<SelectAlertType onSelect={mockOnSelect} />);
fireEvent.click(screen.getByText('anomaly_based_alert'));
expect(mockOnSelect).toHaveBeenCalledWith(
AlertTypes.ANOMALY_BASED_ALERT,
false,
);
});
it('should call onSelect with log based alert type', () => {
render(<SelectAlertType onSelect={mockOnSelect} />);
fireEvent.click(screen.getByText('log_based_alert'));
expect(mockOnSelect).toHaveBeenCalledWith(AlertTypes.LOGS_BASED_ALERT, false);
});
it('should call onSelect with traces based alert type', () => {
render(<SelectAlertType onSelect={mockOnSelect} />);
fireEvent.click(screen.getByText('traces_based_alert'));
expect(mockOnSelect).toHaveBeenCalledWith(
AlertTypes.TRACES_BASED_ALERT,
false,
);
});
it('should call onSelect with exceptions based alert type', () => {
render(<SelectAlertType onSelect={mockOnSelect} />);
fireEvent.click(screen.getByText('exceptions_based_alert'));
expect(mockOnSelect).toHaveBeenCalledWith(
AlertTypes.EXCEPTIONS_BASED_ALERT,
false,
);
});
});

View File

@@ -1,13 +1,37 @@
import { render, screen } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
import * as useCompositeQueryParamHooks from 'hooks/queryBuilder/useGetCompositeQueryParam';
import * as navigateHooks from 'hooks/useSafeNavigate';
import * as useUrlQueryHooks from 'hooks/useUrlQuery';
import * as appHooks from 'providers/App/App';
import { render } from 'tests/test-utils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { DataSource } from 'types/common/queryBuilder';
import CreateAlertRule from '../index';
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigationType: jest.fn(() => 'PUSH'),
useLocation: jest.fn(() => ({
pathname: '/alerts/new',
search: '',
hash: '',
state: null,
})),
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
}));
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
__esModule: true,
default: function MockDateTimeSelector(): JSX.Element {
return <div data-testid="datetime-selector">Mock DateTime Selector</div>;
},
}));
jest.mock('container/FormAlertRules', () => ({
__esModule: true,
default: function MockFormAlertRules({
@@ -48,10 +72,14 @@ const useCompositeQueryParamSpy = jest.spyOn(
'useGetCompositeQueryParam',
);
const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
const useSafeNavigateSpy = jest.spyOn(navigateHooks, 'useSafeNavigate');
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
const mockSetUrlQuery = jest.fn();
const mockToString = jest.fn();
const mockGetUrlQuery = jest.fn();
const mockSafeNavigate = jest.fn();
const mockDeleteUrlQuery = jest.fn();
const FORM_ALERT_RULES_TEXT = 'Form Alert Rules';
const CREATE_ALERT_V2_TEXT = 'Create Alert V2';
@@ -63,8 +91,13 @@ describe('CreateAlertRule', () => {
set: mockSetUrlQuery,
toString: mockToString,
get: mockGetUrlQuery,
delete: mockDeleteUrlQuery,
} as Partial<URLSearchParams> as URLSearchParams);
useCompositeQueryParamSpy.mockReturnValue(initialQueriesMap.metrics);
useSafeNavigateSpy.mockReturnValue({
safeNavigate: mockSafeNavigate,
});
useAppContextSpy.mockReturnValue(getAppContextMockState());
});
it('should render classic flow when showClassicCreateAlertsPage is true', () => {
@@ -72,18 +105,53 @@ describe('CreateAlertRule', () => {
if (key === QueryParams.showClassicCreateAlertsPage) {
return 'true';
}
if (key === QueryParams.alertType) {
return AlertTypes.METRICS_BASED_ALERT;
}
return null;
});
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
});
it('should render new flow by default', () => {
mockGetUrlQuery.mockReturnValue(null);
it('should render new flow when alertType is provided', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.alertType) {
return AlertTypes.METRICS_BASED_ALERT;
}
return null;
});
render(<CreateAlertRule />);
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
});
it('should render type selection when no alertType in URL and no compositeQuery', () => {
mockGetUrlQuery.mockReturnValue(null);
useCompositeQueryParamSpy.mockReturnValue(null);
render(<CreateAlertRule />);
expect(screen.queryByText(FORM_ALERT_RULES_TEXT)).not.toBeInTheDocument();
expect(screen.queryByText(CREATE_ALERT_V2_TEXT)).not.toBeInTheDocument();
});
it('should skip type selection and render alert form when compositeQuery is present', () => {
mockGetUrlQuery.mockReturnValue(null);
useCompositeQueryParamSpy.mockReturnValue({
...initialQueriesMap.metrics,
builder: {
...initialQueriesMap.metrics.builder,
queryData: [
{
...initialQueriesMap.metrics.builder.queryData[0],
dataSource: DataSource.METRICS,
},
],
},
});
render(<CreateAlertRule />);
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
});
it('should render classic flow when ruleType is anomaly_rule even if showClassicCreateAlertsPage is not true', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.showClassicCreateAlertsPage) {
@@ -111,8 +179,13 @@ describe('CreateAlertRule', () => {
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
});
it('should use alertType from compositeQuery dataSource when alertType is not in URL', () => {
mockGetUrlQuery.mockReturnValue(null);
it('should use alertType from URL over compositeQuery dataSource', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.alertType) {
return AlertTypes.LOGS_BASED_ALERT;
}
return null;
});
useCompositeQueryParamSpy.mockReturnValue({
...initialQueriesMap.metrics,
builder: {
@@ -127,14 +200,123 @@ describe('CreateAlertRule', () => {
});
render(<CreateAlertRule />);
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.TRACES_BASED_ALERT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
});
it('should default to METRICS_BASED_ALERT when no alertType and no compositeQuery', () => {
mockGetUrlQuery.mockReturnValue(null);
useCompositeQueryParamSpy.mockReturnValue(null);
render(<CreateAlertRule />);
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
describe('handleSelectType navigation', () => {
beforeEach(() => {
mockGetUrlQuery.mockReturnValue(null);
useCompositeQueryParamSpy.mockReturnValue(null);
});
it('should navigate with threshold alert params for metrics alert', () => {
render(<CreateAlertRule />);
fireEvent.click(screen.getByText('metric_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
'threshold_rule',
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate with threshold alert params for logs alert', () => {
render(<CreateAlertRule />);
fireEvent.click(screen.getByText('log_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
'threshold_rule',
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.LOGS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate with threshold alert params for traces alert', () => {
render(<CreateAlertRule />);
fireEvent.click(screen.getByText('traces_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
'threshold_rule',
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.TRACES_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate with threshold alert params for exceptions alert', () => {
render(<CreateAlertRule />);
fireEvent.click(screen.getByText('exceptions_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
'threshold_rule',
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.EXCEPTIONS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate with anomaly detection params for anomaly alert', () => {
useAppContextSpy.mockReturnValue({
...getAppContextMockState({}),
featureFlags: [
{
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
});
render(<CreateAlertRule />);
fireEvent.click(screen.getByText('anomaly_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
'anomaly_rule',
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate even when showClassicCreateAlertsPage flag is present', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.showClassicCreateAlertsPage) {
return 'true';
}
return null;
});
render(<CreateAlertRule />);
fireEvent.click(screen.getByText('metric_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
'threshold_rule',
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
});
});

View File

@@ -208,3 +208,11 @@ export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
[AlertTypes.EXCEPTIONS_BASED_ALERT]: exceptionAlertDefaults,
};
export const ALERT_TYPE_BREADCRUMB_TITLE: Record<AlertTypes, string> = {
[AlertTypes.ANOMALY_BASED_ALERT]: 'Anomaly-Based Alert',
[AlertTypes.METRICS_BASED_ALERT]: 'Metric-Based Alert',
[AlertTypes.LOGS_BASED_ALERT]: 'Log-Based Alert',
[AlertTypes.TRACES_BASED_ALERT]: 'Traces-Based Alert',
[AlertTypes.EXCEPTIONS_BASED_ALERT]: 'Exceptions-Based Alert',
};

View File

@@ -1,21 +1,34 @@
import { useMemo } from 'react';
import { Form } from 'antd';
import { useCallback, useEffect, useMemo } from 'react';
import { Form, Tabs, TabsProps } from 'antd';
import logEvent from 'api/common/logEvent';
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
import AlertBreadcrumb from 'components/AlertBreadcrumb';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import CreateAlertV2 from 'container/CreateAlertV2';
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { AlertListTabs } from 'pages/AlertList/types';
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
import { ALERTS_VALUES_MAP } from './defaults';
import { ALERTS_VALUES_MAP, ALERT_TYPE_BREADCRUMB_TITLE } from './defaults';
import SelectAlertType from './SelectAlertType';
import './CreateAlertRule.styles.scss';
function CreateRules(): JSX.Element {
const [formInstance] = Form.useForm();
const compositeQuery = useGetCompositeQueryParam();
const queryParams = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
const ruleTypeFromURL = queryParams.get(QueryParams.ruleType);
const alertTypeFromURL = queryParams.get(QueryParams.alertType);
@@ -23,6 +36,15 @@ function CreateRules(): JSX.Element {
const showClassicCreateAlertsPageFlag =
queryParams.get(QueryParams.showClassicCreateAlertsPage) === 'true';
const isTypeSelectionMode =
!alertTypeFromURL && !ruleTypeFromURL && !compositeQuery;
useEffect(() => {
if (isTypeSelectionMode) {
logEvent('Alert: New alert data source selection page visited', {});
}
}, [isTypeSelectionMode]);
const alertType = useMemo(() => {
if (ruleTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
return AlertTypes.ANOMALY_BASED_ALERT;
@@ -45,22 +67,142 @@ function CreateRules(): JSX.Element {
[alertType, version],
);
// Load old alerts flow always for anomaly based alerts and when showClassicCreateAlertsPage is true
if (
showClassicCreateAlertsPageFlag ||
alertType === AlertTypes.ANOMALY_BASED_ALERT
) {
return (
<FormAlertRules
alertType={alertType}
formInstance={formInstance}
initialValue={initialAlertValue}
ruleId=""
/>
);
}
const handleTabChange = useCallback(
(tab: string): void => {
queryParams.set('tab', tab);
queryParams.delete('subTab');
queryParams.delete('search');
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${queryParams.toString()}`);
},
[safeNavigate, queryParams],
);
return <CreateAlertV2 alertType={alertType} />;
const handleSelectType = useCallback(
(type: AlertTypes, newTab?: boolean): void => {
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
queryParams.set(
QueryParams.ruleType,
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
);
queryParams.set(QueryParams.alertType, AlertTypes.METRICS_BASED_ALERT);
} else {
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
queryParams.set(QueryParams.alertType, type);
}
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { newTab });
},
[queryParams, safeNavigate],
);
const alertContent = useMemo(() => {
if (isTypeSelectionMode) {
return <SelectAlertType onSelect={handleSelectType} />;
}
if (
showClassicCreateAlertsPageFlag ||
alertType === AlertTypes.ANOMALY_BASED_ALERT
) {
return (
<FormAlertRules
alertType={alertType}
formInstance={formInstance}
initialValue={initialAlertValue}
ruleId=""
/>
);
}
return <CreateAlertV2 alertType={alertType} />;
}, [
isTypeSelectionMode,
handleSelectType,
showClassicCreateAlertsPageFlag,
alertType,
formInstance,
initialAlertValue,
]);
const items: TabsProps['items'] = [
{
label: (
<div className="periscope-tab top-level-tab">
<GalleryVerticalEnd size={14} />
Triggered Alerts
</div>
),
key: AlertListTabs.TRIGGERED_ALERTS,
children: null,
},
{
label: (
<div className="periscope-tab top-level-tab">
<Pyramid size={14} />
Alert Rules
</div>
),
key: AlertListTabs.ALERT_RULES,
children: (
<div className="create-alert-wrapper">
<AlertBreadcrumb
className="create-alert__breadcrumb"
items={
isTypeSelectionMode
? [
{
title: 'Alert Rules',
route: `${ROUTES.LIST_ALL_ALERT}?tab=${AlertListTabs.ALERT_RULES}`,
},
{ title: 'Select Alert Type', isLast: true },
]
: [
{
title: 'Alert Rules',
route: `${ROUTES.LIST_ALL_ALERT}?tab=${AlertListTabs.ALERT_RULES}`,
},
{ title: 'Select Alert Type', route: ROUTES.ALERTS_NEW },
{
title: ALERT_TYPE_BREADCRUMB_TITLE[alertType],
isLast: true,
},
]
}
/>
{alertContent}
</div>
),
},
{
label: (
<div className="periscope-tab top-level-tab">
<ConfigureIcon width={14} height={14} />
Configuration
</div>
),
key: AlertListTabs.CONFIGURATION,
children: null,
},
];
return (
<Tabs
destroyInactiveTabPane
items={items}
activeKey={AlertListTabs.ALERT_RULES}
onChange={handleTabChange}
className="alerts-container create-alert-tabs"
tabBarExtraContent={
<div className="create-alert-tabs__extra">
<DateTimeSelector showAutoRefresh />
<HeaderRightSection
enableAnnouncements={false}
enableShare
enableFeedback
/>
</div>
}
/>
);
}
export default CreateRules;

View File

@@ -9,6 +9,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { RotateCcw } from '@signozhq/icons';
import { useAlertRuleOptional } from 'providers/Alert';
import { Labels } from 'types/api/alerts/def';
import { useCreateAlertState } from '../context';
@@ -18,6 +19,7 @@ import './styles.scss';
function CreateAlertHeader(): JSX.Element {
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
const alertRuleContext = useAlertRuleOptional();
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
@@ -74,9 +76,13 @@ function CreateAlertHeader(): JSX.Element {
<Input
type="text"
value={alertState.name}
onChange={(e): void =>
setAlertState({ type: 'SET_ALERT_NAME', payload: e.target.value })
}
onChange={(e): void => {
const newName = e.target.value;
setAlertState({ type: 'SET_ALERT_NAME', payload: newName });
if (isEditMode && alertRuleContext?.setAlertRuleName) {
alertRuleContext.setAlertRuleName(newName);
}
}}
className="alert-header__input title"
placeholder="Enter alert rule name"
data-testid="alert-name-input"

View File

@@ -20,6 +20,11 @@ import {
} from './utils';
import './styles.scss';
import {
invalidateGetRuleByID,
invalidateListRules,
} from 'api/generated/services/rules';
import { useQueryClient } from 'react-query';
function Footer(): JSX.Element {
const {
@@ -115,6 +120,7 @@ function Footer(): JSX.Element {
testAlertRule,
]);
const queryClient = useQueryClient();
const handleSaveAlert = useCallback((): void => {
const payload = buildCreateThresholdAlertRulePayload({
alertType,
@@ -133,6 +139,9 @@ function Footer(): JSX.Element {
},
{
onSuccess: () => {
void invalidateGetRuleByID(queryClient, { id: ruleId });
void invalidateListRules(queryClient);
toast.success('Alert rule updated successfully');
safeNavigate('/alerts');
},

View File

@@ -7,6 +7,7 @@ import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationS
import * as createAlertState from '../../context';
import Footer from '../Footer';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
// Mock the hooks used by Footer component
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
@@ -64,6 +65,12 @@ const mockAlertContextState = createMockAlertContextState({
},
});
const WrappedFooter = (): JSX.Element => (
<MockQueryClientProvider>
<Footer />
</MockQueryClientProvider>
);
jest
.spyOn(createAlertState, 'useCreateAlertState')
.mockReturnValue(mockAlertContextState);
@@ -97,20 +104,20 @@ describe('Footer', () => {
});
it('should render the component with 3 buttons', () => {
render(<Footer />);
render(<WrappedFooter />);
expect(screen.getByText(SAVE_ALERT_RULE_TEXT)).toBeInTheDocument();
expect(screen.getByText(TEST_NOTIFICATION_TEXT)).toBeInTheDocument();
expect(screen.getByText(DISCARD_TEXT)).toBeInTheDocument();
});
it('discard action works correctly', () => {
render(<Footer />);
render(<WrappedFooter />);
fireEvent.click(screen.getByText(DISCARD_TEXT));
expect(mockDiscardAlertRule).toHaveBeenCalled();
});
it('save alert rule action works correctly', () => {
render(<Footer />);
render(<WrappedFooter />);
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
expect(mockCreateAlertRule).toHaveBeenCalled();
});
@@ -120,13 +127,13 @@ describe('Footer', () => {
...mockAlertContextState,
isEditMode: true,
});
render(<Footer />);
render(<WrappedFooter />);
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
expect(mockUpdateAlertRule).toHaveBeenCalled();
});
it('test notification action works correctly', () => {
render(<Footer />);
render(<WrappedFooter />);
fireEvent.click(screen.getByText(TEST_NOTIFICATION_TEXT));
expect(mockTestAlertRule).toHaveBeenCalled();
});
@@ -136,7 +143,7 @@ describe('Footer', () => {
...mockAlertContextState,
isCreatingAlertRule: true,
});
render(<Footer />);
render(<WrappedFooter />);
expect(
screen.getByRole('button', { name: /save alert rule/i }),
@@ -152,7 +159,7 @@ describe('Footer', () => {
...mockAlertContextState,
isUpdatingAlertRule: true,
});
render(<Footer />);
render(<WrappedFooter />);
// Target the button elements directly instead of the text spans inside them
expect(
@@ -169,7 +176,7 @@ describe('Footer', () => {
...mockAlertContextState,
isTestingAlertRule: true,
});
render(<Footer />);
render(<WrappedFooter />);
// Target the button elements directly instead of the text spans inside them
expect(
@@ -189,7 +196,7 @@ describe('Footer', () => {
name: '',
},
});
render(<Footer />);
render(<WrappedFooter />);
expect(
screen.getByRole('button', { name: /save alert rule/i }),
@@ -217,7 +224,7 @@ describe('Footer', () => {
},
});
render(<Footer />);
render(<WrappedFooter />);
expect(
screen.getByRole('button', { name: /save alert rule/i }),
@@ -245,7 +252,7 @@ describe('Footer', () => {
},
});
render(<Footer />);
render(<WrappedFooter />);
expect(
screen.getByRole('button', { name: /save alert rule/i }),
@@ -261,7 +268,7 @@ describe('Footer', () => {
...mockAlertContextState,
isTestingAlertRule: true,
});
render(<Footer />);
render(<WrappedFooter />);
// When testing alert rule, the play icon is replaced with a loader icon
expect(
@@ -276,7 +283,7 @@ describe('Footer', () => {
...mockAlertContextState,
isUpdatingAlertRule: true,
});
render(<Footer />);
render(<WrappedFooter />);
// When updating alert rule, the check icon is replaced with a loader icon
expect(
@@ -291,7 +298,7 @@ describe('Footer', () => {
...mockAlertContextState,
isCreatingAlertRule: true,
});
render(<Footer />);
render(<WrappedFooter />);
// When creating alert rule, the check icon is replaced with a loader icon
expect(

View File

@@ -38,6 +38,7 @@ import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/map
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty, isEqual } from 'lodash-es';
import Tabs2 from 'periscope/components/Tabs2';
import { useAlertRuleOptional } from 'providers/Alert';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { AppState } from 'store/reducers';
@@ -92,7 +93,6 @@ const ALERT_SETUP_GUIDE_URLS: Record<AlertTypes, string> = {
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
};
// eslint-disable-next-line sonarjs/cognitive-complexity
function FormAlertRules({
alertType,
formInstance,
@@ -160,6 +160,32 @@ function FormAlertRules({
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
const alertRuleContext = useAlertRuleOptional();
const providerAlertName = alertRuleContext?.alertRuleName;
useEffect(() => {
if (providerAlertName) {
setAlertDef((prev) => {
if (prev.alert === providerAlertName) {
return prev;
}
return { ...prev, alert: providerAlertName };
});
formInstance.setFieldsValue({ alert: providerAlertName });
}
}, [providerAlertName, formInstance]);
// Wrap setAlertDef to sync alert name to provider when user types
const handleSetAlertDef = useCallback(
(newDef: AlertDef) => {
setAlertDef(newDef);
// Sync alert name change to provider for header display
if (newDef.alert !== alertDef.alert && alertRuleContext?.setAlertRuleName) {
alertRuleContext.setAlertRuleName(newDef.alert);
}
},
[alertDef.alert, alertRuleContext],
);
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
@@ -680,7 +706,7 @@ function FormAlertRules({
const renderBasicInfo = (): JSX.Element => (
<BasicInfo
alertDef={alertDef}
setAlertDef={setAlertDef}
setAlertDef={handleSetAlertDef}
isNewRule={isNewRule}
/>
);

View File

@@ -111,7 +111,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
number: allAlertRules?.length,
layout: 'new',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, {
safeNavigate(ROUTES.ALERTS_NEW, {
newTab: isModifierKeyPressed(e),
});
},

View File

@@ -22,3 +22,9 @@
gap: 8px;
}
}
// FieldRenderer is used inside log/trace/metric detail drawers (z-index 1000).
// The design-system tooltip defaults to z-index 50 and would render behind them.
.field-renderer-tooltip-content {
--tooltip-z-index: 1000;
}

View File

@@ -1,4 +1,5 @@
import { Divider, Tooltip } from 'antd';
import { Divider } from 'antd';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { TagContainer, TagLabel, TagValue } from './FieldRenderer.styles';
@@ -7,6 +8,10 @@ import { getFieldAttributes } from './utils';
import './FieldRenderer.styles.scss';
const TOOLTIP_CONTENT_PROPS = {
className: 'field-renderer-tooltip-content',
};
function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
const { dataType, newField, logType } = getFieldAttributes(field);
@@ -14,11 +19,16 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
<span className="field-renderer-container">
{dataType && newField && logType ? (
<>
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
<TooltipSimple
title={newField}
side="left"
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
arrow
>
<Typography.Text truncate={1} className="label">
{newField}{' '}
</Typography.Text>
</Tooltip>
</TooltipSimple>
<div className="tags">
<TagContainer>

View File

@@ -14,7 +14,7 @@ import { ILog } from 'types/api/logs/log';
import { ActionItemProps } from './ActionItem';
import TableView from './TableView';
import { removeEscapeCharacters } from './utils';
import { getBodyDisplayString, removeEscapeCharacters } from './utils';
import './Overview.styles.scss';
@@ -112,7 +112,7 @@ function Overview({
children: (
<div className="logs-body-content">
<MEditor
value={removeEscapeCharacters(logData.body)}
value={removeEscapeCharacters(getBodyDisplayString(logData.body))}
language="json"
options={options}
onChange={(): void => {}}

View File

@@ -106,10 +106,20 @@ function TableView({
isListViewPanel,
]);
const flattenLogData: Record<string, string> | null = useMemo(
() => (logData ? flattenObject(logData) : null),
[logData],
);
// When USE_JSON_BODY is enabled, body arrives as a pre-parsed object. Serialize it
// back to a string so flattenObject keeps `body` as a single table row instead of
// recursively expanding it into dotted sub-keys (body.message, body.foo.bar, …),
// which would break the tree view in BodyContent that relies on record.field === 'body'.
const flattenLogData: Record<string, string> | null = useMemo(() => {
if (!logData) {
return null;
}
const normalizedLog =
typeof logData.body === 'object' && logData.body !== null
? { ...logData, body: JSON.stringify(logData.body) }
: logData;
return flattenObject(normalizedLog);
}, [logData]);
const handleClick = (
operator: string,

View File

@@ -10,7 +10,7 @@ const MAX_BODY_BYTES = 100 * 1024; // 100 KB
// Hook for async JSON processing
const useAsyncJSONProcessing = (
value: string,
value: string | Record<string, unknown>,
shouldProcess: boolean,
handleChangeSelectedView?: ChangeViewFunctionType,
): {
@@ -40,11 +40,17 @@ const useAsyncJSONProcessing = (
return (): void => {};
}
// Avoid processing if the json is too large
const byteSize = new Blob([value]).size;
if (byteSize > MAX_BODY_BYTES) {
return (): void => {};
}
// When value is already a parsed object skip the size check and JSON parsing
const parseBody = (): Record<string, unknown> | null => {
if (typeof value === 'object' && value !== null) {
return value as Record<string, unknown>;
}
const byteSize = new Blob([value as string]).size;
if (byteSize > MAX_BODY_BYTES) {
return null;
}
return recursiveParseJSON(value as string);
};
processingRef.current = true;
setJsonState({ isLoading: true, treeData: null, error: null });
@@ -53,8 +59,8 @@ const useAsyncJSONProcessing = (
const processAsync = (): void => {
setTimeout(() => {
try {
const parsedBody = recursiveParseJSON(value);
if (!isEmpty(parsedBody)) {
const parsedBody = parseBody();
if (parsedBody && !isEmpty(parsedBody)) {
const treeData = jsonToDataNodes(parsedBody, {
isBodyJsonQueryEnabled,
handleChangeSelectedView,
@@ -82,8 +88,8 @@ const useAsyncJSONProcessing = (
// eslint-disable-next-line sonarjs/no-identical-functions
(): void => {
try {
const parsedBody = recursiveParseJSON(value);
if (!isEmpty(parsedBody)) {
const parsedBody = parseBody();
if (parsedBody && !isEmpty(parsedBody)) {
const treeData = jsonToDataNodes(parsedBody, {
isBodyJsonQueryEnabled,
handleChangeSelectedView,

View File

@@ -4,7 +4,11 @@ import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { MetricsType } from 'container/MetricsApplication/constant';
import dompurify from 'dompurify';
import { uniqueId } from 'lodash-es';
import { ILog, ILogAggregateAttributesResources } from 'types/api/logs/log';
import {
ILog,
ILogAggregateAttributesResources,
ILogBody,
} from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { FORBID_DOM_PURIFY_ATTR, FORBID_DOM_PURIFY_TAGS } from 'utils/app';
@@ -433,3 +437,8 @@ export const getSanitizedLogBody = (
return '{}';
}
};
// Returns a plain string for display contexts (Monaco editor, table cells, raw log row).
export function getBodyDisplayString(body: string | ILogBody): string {
return typeof body === 'string' ? body : JSON.stringify(body as ILogBody);
}

View File

@@ -1,8 +1,11 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Spin, Tooltip } from 'antd';
import { Button, Spin } from 'antd';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { useGetMetricHighlights } from 'api/generated/services/metrics';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { Info } from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import { HighlightsProps } from './types';
@@ -11,6 +14,10 @@ import {
formatTimestampToReadableDate,
} from './utils';
const TOOLTIP_CONTENT_PROPS = {
className: 'metric-highlights-tooltip-content',
};
function Highlights({ metricName }: HighlightsProps): JSX.Element {
const {
data: metricHighlightsData,
@@ -39,6 +46,13 @@ function Highlights({ metricName }: HighlightsProps): JSX.Element {
const lastReceivedText = formatTimestampToReadableDate(
metricHighlights?.lastReceived,
);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const lastReceivedTooltipText = metricHighlights?.lastReceived
? `Last received on ${formatTimezoneAdjustedTimestamp(
metricHighlights.lastReceived,
DATE_TIME_FORMATS.DASH_DATETIME_UTC,
)}`
: 'No data received yet';
if (isErrorMetricHighlights) {
return (
@@ -90,27 +104,42 @@ function Highlights({ metricName }: HighlightsProps): JSX.Element {
className="metric-details-grid-value"
data-testid="metric-highlights-data-points"
>
<Tooltip title={metricHighlights?.dataPoints?.toLocaleString()}>
{formatNumberIntoHumanReadableFormat(metricHighlights?.dataPoints ?? 0)}
</Tooltip>
<TooltipSimple
title={metricHighlights?.dataPoints?.toLocaleString()}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
arrow
>
<span>
{formatNumberIntoHumanReadableFormat(
metricHighlights?.dataPoints ?? 0,
)}
</span>
</TooltipSimple>
</Typography.Text>
<Typography.Text
className="metric-details-grid-value"
data-testid="metric-highlights-time-series-total"
>
<Tooltip
title="Active time series are those that have received data points in the last 1
hour."
placement="top"
<TooltipSimple
title="Active time series are those that have received data points in the last 1 hour."
side="top"
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
arrow
>
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
</Tooltip>
</TooltipSimple>
</Typography.Text>
<Typography.Text
className="metric-details-grid-value"
data-testid="metric-highlights-last-received"
>
<Tooltip title={lastReceivedText}>{lastReceivedText}</Tooltip>
<TooltipSimple
title={lastReceivedTooltipText}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
arrow
>
<span>{lastReceivedText}</span>
</TooltipSimple>
</Typography.Text>
</>
)}

View File

@@ -510,6 +510,12 @@
color: var(--bg-robin-400) !important;
}
// The MetricDetails Drawer sits at z-index 1000; the design-system tooltip
// defaults to z-index 50 and would otherwise render behind the drawer.
.metric-highlights-tooltip-content {
--tooltip-z-index: 1000;
}
@keyframes fade-in-out {
0% {
opacity: 0;

View File

@@ -1,10 +1,22 @@
import { render, screen } from '@testing-library/react';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import TimezoneProvider from 'providers/Timezone';
import Highlights from '../Highlights';
import { formatTimestampToReadableDate } from '../utils';
import { getMockMetricHighlightsData, MOCK_METRIC_NAME } from './testUtlls';
function renderHighlights(metricName: string): ReturnType<typeof render> {
return render(
<TimezoneProvider>
<TooltipProvider>
<Highlights metricName={metricName} />
</TooltipProvider>
</TimezoneProvider>,
);
}
const useGetMetricHighlightsMock = jest.spyOn(
metricsExplorerHooks,
'useGetMetricHighlights',
@@ -16,7 +28,7 @@ describe('Highlights', () => {
});
it('should render all highlights data correctly', () => {
render(<Highlights metricName={MOCK_METRIC_NAME} />);
renderHighlights(MOCK_METRIC_NAME);
const dataPoints = screen.getByTestId('metric-highlights-data-points');
const timeSeriesTotal = screen.getByTestId(
@@ -41,7 +53,7 @@ describe('Highlights', () => {
),
);
render(<Highlights metricName={MOCK_METRIC_NAME} />);
renderHighlights(MOCK_METRIC_NAME);
expect(
screen.getByTestId('metric-highlights-error-state'),
@@ -58,7 +70,7 @@ describe('Highlights', () => {
),
);
render(<Highlights metricName={MOCK_METRIC_NAME} />);
renderHighlights(MOCK_METRIC_NAME);
expect(screen.getByText('SAMPLES')).toBeInTheDocument();
expect(screen.getByText('TIME SERIES')).toBeInTheDocument();

View File

@@ -1,17 +1,5 @@
.new-explorer-cta {
display: flex;
.new-explorer-cta-with-badge {
display: inline-flex;
align-items: center;
color: var(--muted-foreground);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px;
}
gap: 6px;
}

View File

@@ -1,9 +1,5 @@
import ROUTES from 'constants/routes';
export const RIBBON_STYLES = {
top: '-0.75rem',
};
export const buttonText: Record<string, string> = {
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
[ROUTES.TRACE]: 'New Explorer',

View File

@@ -1,12 +1,13 @@
import React, { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Badge, Button } from 'antd';
import { Button } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Undo } from '@signozhq/icons';
import { isModifierKeyPressed } from 'utils/app';
import { buttonText, RIBBON_STYLES } from './config';
import { buttonText } from './config';
import './NewExplorerCTA.styles.scss';
@@ -70,9 +71,12 @@ function NewExplorerCTA(): JSX.Element | null {
}
return (
<Badge.Ribbon style={RIBBON_STYLES} text="New">
<span className="new-explorer-cta-with-badge">
{button}
</Badge.Ribbon>
<Badge color="robin" variant="default">
New
</Badge>
</span>
);
}

View File

@@ -2,6 +2,7 @@ import { Expand } from '@signozhq/icons';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getBodyDisplayString } from 'container/LogDetailedView/utils';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useTimezone } from 'providers/Timezone';
import { ILog } from 'types/api/logs/log';
@@ -26,7 +27,9 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
DATE_TIME_FORMATS.UTC_MONTH_SHORT,
)}
</div>
<div className="logs-preview-list-item-body">{log.body}</div>
<div className="logs-preview-list-item-body">
{getBodyDisplayString(log.body)}
</div>
<div
className="logs-preview-list-item-expand"
onClick={makeLogDetailsHandler(log)}

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,7 +48,9 @@ export function PlannedDowntime(): JSX.Element {
const urlQuery = useUrlQuery();
const [initialValues, setInitialValues] =
useState<Partial<RuletypesPlannedMaintenanceDTO>>(defaultInitialValues);
useState<Partial<AlertmanagertypesPlannedMaintenanceDTO>>(
defaultInitialValues,
);
const downtimeSchedules = useListDowntimeSchedules();
const alertOptions = React.useMemo(

View File

@@ -20,9 +20,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';
@@ -74,16 +74,16 @@ interface PlannedDowntimeFormData {
name: string;
startTime: dayjs.Dayjs | null;
endTime: dayjs.Dayjs | null;
recurrence?: RuletypesRecurrenceDTO;
recurrence?: AlertmanagertypesRecurrenceDTO;
alertRules: DefaultOptionType[];
recurrenceSelect?: RuletypesRecurrenceDTO;
recurrenceSelect?: AlertmanagertypesRecurrenceDTO;
timezone?: string;
}
const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
interface PlannedDowntimeFormProps {
initialValues: Partial<RuletypesPlannedMaintenanceDTO>;
initialValues: Partial<AlertmanagertypesPlannedMaintenanceDTO>;
alertOptions: DefaultOptionType[];
isError: boolean;
isLoading: boolean;
@@ -139,7 +139,7 @@ export function PlannedDowntimeForm(
const saveHandler = useCallback(
async (values: PlannedDowntimeFormData) => {
const data: RuletypesPostablePlannedMaintenanceDTO = {
const data: AlertmanagertypesPostablePlannedMaintenanceDTO = {
alertIds: values.alertRules
.map((alert) => alert.value)
.filter((alert) => alert !== undefined) as string[],
@@ -276,7 +276,7 @@ export function PlannedDowntimeForm(
? recurrenceOptions.doesNotRepeat.value
: schedule?.recurrence?.repeatType,
duration: getDurationInfo(schedule?.recurrence?.duration)?.value ?? '',
} as RuletypesRecurrenceDTO,
} as AlertmanagertypesRecurrenceDTO,
timezone: schedule?.timezone as string,
};
}, [initialValues, alertOptions]);

View File

@@ -7,8 +7,8 @@ import type { DefaultOptionType } from 'antd/es/select';
import type {
ListDowntimeSchedules200,
RenderErrorResponseDTO,
RuletypesPlannedMaintenanceDTO,
RuletypesScheduleDTO,
AlertmanagertypesPlannedMaintenanceDTO,
AlertmanagertypesScheduleDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import cx from 'classnames';
@@ -133,7 +133,7 @@ export function CollapseListContent({
created_at?: string;
created_by_name?: string;
created_by_email?: string;
schedule?: RuletypesScheduleDTO;
schedule?: AlertmanagertypesScheduleDTO;
updated_at?: string;
updated_by_name?: string;
alertOptions?: DefaultOptionType[];
@@ -214,7 +214,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;
@@ -281,9 +281,10 @@ export function CustomCollapseList(
);
}
export type DowntimeSchedulesTableData = RuletypesPlannedMaintenanceDTO & {
alertOptions: DefaultOptionType[];
};
export type DowntimeSchedulesTableData =
AlertmanagertypesPlannedMaintenanceDTO & {
alertOptions: DefaultOptionType[];
};
export function PlannedDowntimeList({
downtimeSchedules,
@@ -300,7 +301,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';
@@ -66,7 +66,7 @@ export const getAlertOptionsFromIds = (
);
export const recurrenceInfo = (
recurrence?: RuletypesRecurrenceDTO | null,
recurrence?: AlertmanagertypesRecurrenceDTO | null,
timezone?: string,
): string => {
if (!recurrence) {
@@ -87,19 +87,20 @@ export const recurrenceInfo = (
return `Repeats - ${repeatType} ${weeklyRepeatString} from ${formattedStartTime} ${formattedEndTime} ${durationString}`;
};
export const defaultInitialValues: Partial<RuletypesPlannedMaintenanceDTO> = {
name: '',
description: '',
schedule: {
timezone: '',
endTime: undefined,
recurrence: undefined,
startTime: undefined,
},
alertIds: [],
createdAt: undefined,
createdBy: undefined,
};
export const defaultInitialValues: Partial<AlertmanagertypesPlannedMaintenanceDTO> =
{
name: '',
description: '',
schedule: {
timezone: '',
endTime: undefined,
recurrence: undefined,
startTime: undefined,
},
alertIds: [],
createdAt: undefined,
createdBy: undefined,
};
type DeleteDowntimeScheduleProps = {
deleteDowntimeScheduleAsync: UseMutateAsyncFunction<
@@ -215,5 +216,5 @@ export const recurrenceOptionWithSubmenu: Option[] = [
];
export const isScheduleRecurring = (
schedule?: RuletypesPlannedMaintenanceDTO['schedule'] | null,
schedule?: AlertmanagertypesPlannedMaintenanceDTO['schedule'] | null,
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);

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

@@ -33,14 +33,12 @@ function TopNav(): JSX.Element | null {
[location.pathname],
);
const isNewAlertsLandingPage = useMemo(
() =>
matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }) &&
!location.search,
[location.pathname, location.search],
const isAlertCreationPage = useMemo(
() => matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }),
[location.pathname],
);
if (isSignUpPage || isDisabled || isRouteToSkip || isNewAlertsLandingPage) {
if (isSignUpPage || isDisabled || isRouteToSkip || isAlertCreationPage) {
return null;
}

View File

@@ -41,7 +41,6 @@
}
.alert-details {
margin-top: 10px;
.divider {
border-color: var(--l1-border);
margin: 16px 0;

View File

@@ -1,8 +1,9 @@
import React, { useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Breadcrumb, Button, Divider } from 'antd';
import { Divider } from 'antd';
import logEvent from 'api/common/logEvent';
import classNames from 'classnames';
import AlertBreadcrumb from 'components/AlertBreadcrumb';
import { Filters } from 'components/AlertDetailsFilters/Filters';
import RouteTab from 'components/RouteTab';
import Spinner from 'components/Spinner';
@@ -10,13 +11,12 @@ import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { CreateAlertProvider } from 'container/CreateAlertV2/context';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { useAlertRule } from 'providers/Alert';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
import { fromRuleDTOToPostableRuleV2 } from 'types/api/alerts/convert';
import { isModifierKeyPressed } from 'utils/app';
import AlertHeader from './AlertHeader/AlertHeader';
import AlertNotFound from './AlertNotFound';
@@ -24,42 +24,11 @@ import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
import './AlertDetails.styles.scss';
function BreadCrumbItem({
title,
isLast,
route,
}: {
title: string | null;
isLast?: boolean;
route?: string;
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
if (isLast) {
return <div className="breadcrumb-item breadcrumb-item--last">{title}</div>;
}
const handleNavigate = (e: React.MouseEvent): void => {
if (!route) {
return;
}
safeNavigate(ROUTES.LIST_ALL_ALERT, { newTab: isModifierKeyPressed(e) });
};
return (
<Button type="text" className="breadcrumb-item" onClick={handleNavigate}>
{title}
</Button>
);
}
BreadCrumbItem.defaultProps = {
isLast: false,
route: '',
};
function AlertDetails(): JSX.Element {
const { pathname } = useLocation();
const { routes } = useRouteTabUtils();
const params = useUrlQuery();
const { alertRuleName } = useAlertRule();
const { isLoading, isError, ruleId, isValidRuleId, alertDetailsResponse } =
useGetAlertRuleDetails();
@@ -69,7 +38,7 @@ function AlertDetails(): JSX.Element {
}, [params]);
const getDocumentTitle = useMemo(() => {
const alertTitle = alertDetailsResponse?.data?.alert;
const alertTitle = alertRuleName ?? alertDetailsResponse?.data?.alert;
if (alertTitle) {
return alertTitle;
}
@@ -80,7 +49,7 @@ function AlertDetails(): JSX.Element {
return document.title;
}
return 'Alert Not Found';
}, [alertDetailsResponse?.data?.alert, isTestAlert, isLoading]);
}, [alertRuleName, alertDetailsResponse?.data?.alert, isTestAlert, isLoading]);
useEffect(() => {
document.title = getDocumentTitle;
@@ -126,20 +95,13 @@ function AlertDetails(): JSX.Element {
<div
className={classNames('alert-details', { 'alert-details-v2': isV2Alert })}
>
<Breadcrumb
<AlertBreadcrumb
className="alert-details__breadcrumb"
items={[
{
title: (
<BreadCrumbItem title="Alert Rules" route={ROUTES.LIST_ALL_ALERT} />
),
},
{
title: <BreadCrumbItem title={ruleId} isLast />,
},
{ title: 'Alert Rules', route: ROUTES.LIST_ALL_ALERT },
{ title: ruleId, isLast: true },
]}
/>
<Divider className="divider breadcrumb-divider" />
{alertRuleDetails && <AlertHeader alertDetails={alertRuleDetails} />}
<Divider className="divider" />

View File

@@ -33,13 +33,12 @@ const menuItemStyleV2: CSSProperties = {
function AlertActionButtons({
ruleId,
alertDetails,
setUpdatedName,
}: {
ruleId: string;
alertDetails: AlertHeaderProps['alertDetails'];
setUpdatedName: (name: string) => void;
}): JSX.Element {
const { alertRuleState, setAlertRuleState } = useAlertRule();
const { alertRuleState, setAlertRuleState, alertRuleName, setAlertRuleName } =
useAlertRule();
const [intermediateName, setIntermediateName] = useState<string>(
alertDetails.alert,
);
@@ -53,7 +52,7 @@ function AlertActionButtons({
const { handleAlertDelete } = useAlertRuleDelete({ ruleId });
const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({
alertDetails: alertDetails as unknown as AlertDef,
setUpdatedName,
setAlertRuleName,
intermediateName,
});
@@ -113,6 +112,12 @@ function AlertActionButtons({
}
}, [setAlertRuleState, alertRuleState, alertDetails.state]);
useEffect(() => {
if (alertRuleName !== undefined) {
setIntermediateName(alertRuleName);
}
}, [alertRuleName]);
// on unmount remove the alert state
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => (): void => setAlertRuleState(undefined), []);

View File

@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo } from 'react';
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
import CreateAlertV2Header from 'container/CreateAlertV2/CreateAlertHeader';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
@@ -20,8 +20,17 @@ export type AlertHeaderProps = {
};
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
const { state, alert: alertName, labels } = alertDetails;
const { alertRuleState } = useAlertRule();
const [updatedName, setUpdatedName] = useState(alertName);
const { alertRuleState, alertRuleName, setAlertRuleName } = useAlertRule();
useEffect(() => {
if (alertRuleName === undefined && alertName) {
setAlertRuleName(alertName);
}
}, [alertRuleName, alertName, setAlertRuleName]);
useEffect(() => (): void => setAlertRuleName(undefined), [setAlertRuleName]);
const displayName = alertRuleName ?? alertName;
const labelsWithoutSeverity = useMemo(() => {
if (labels) {
@@ -40,7 +49,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
<div className="alert-title-wrapper">
<AlertState state={alertRuleState ?? state ?? ''} />
<div className="alert-title">
<LineClampedText text={updatedName || alertName} />
<LineClampedText text={displayName || ''} />
</div>
</div>
</div>
@@ -64,7 +73,6 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
<AlertActionButtons
alertDetails={alertDetails}
ruleId={alertDetails?.id || ''}
setUpdatedName={setUpdatedName}
/>
</div>
</div>

View File

@@ -12,7 +12,9 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
createRule,
deleteRuleByID,
getGetRuleByIDQueryKey,
invalidateGetRuleByID,
invalidateListRules,
updateRuleByID,
useGetRuleByID,
useListRules,
@@ -490,11 +492,11 @@ export const useAlertRuleDuplicate = ({
};
export const useAlertRuleUpdate = ({
alertDetails,
setUpdatedName,
setAlertRuleName,
intermediateName,
}: {
alertDetails: AlertDef;
setUpdatedName: (name: string) => void;
setAlertRuleName: (name: string | undefined) => void;
intermediateName: string;
}): {
handleAlertUpdate: () => void;
@@ -502,17 +504,29 @@ export const useAlertRuleUpdate = ({
} => {
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const queryClient = useQueryClient();
const { mutate: updateAlertRule, isLoading } = useMutation(
[REACT_QUERY_KEY.UPDATE_ALERT_RULE, alertDetails.id],
(args: { data: AlertDef; id: string }) =>
updateRuleByID({ id: args.id }, toPostableRuleDTOFromAlertDef(args.data)),
{
onMutate: () => setUpdatedName(intermediateName),
onSuccess: () =>
notifications.success({ message: 'Alert renamed successfully' }),
onMutate: () => setAlertRuleName(intermediateName),
onSuccess: () => {
const ruleId = alertDetails.id || '';
const ruleQueryKey = getGetRuleByIDQueryKey({ id: ruleId });
const existingRule = queryClient.getQueryData<GetRuleByID200>(ruleQueryKey);
if (existingRule) {
queryClient.setQueryData<GetRuleByID200>(ruleQueryKey, {
...existingRule,
data: { ...existingRule.data, alert: intermediateName },
});
}
void invalidateListRules(queryClient);
notifications.success({ message: 'Alert renamed successfully' });
},
onError: (error) => {
setUpdatedName(alertDetails.alert);
setAlertRuleName(alertDetails.alert);
showErrorModal(
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
);
@@ -551,7 +565,6 @@ export const useAlertRuleDelete = ({
history.push(ROUTES.LIST_ALL_ALERT);
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: (error) =>
showErrorModal(
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,

View File

@@ -1,15 +1,44 @@
.alerts-container {
.ant-tabs-nav {
padding: 0 8px;
.top-level-tab.periscope-tab {
padding: 2px 0;
}
.ant-tabs {
&-nav {
padding: 0 8px;
margin-bottom: 0 !important;
&::before {
border-bottom: 1px solid var(--l1-border) !important;
}
}
&-tab {
&[data-node-key='TriggeredAlerts'] {
margin-left: 16px;
}
&:not(:first-of-type) {
margin-left: 24px !important;
}
[aria-selected='false'] {
.periscope-tab {
color: var(--l2-foreground);
}
}
}
}
.configuration-tabs {
margin-top: -16px;
.ant-tabs-nav {
.ant-tabs-nav-wrap {
padding: 0 8px;
}
}
}
.alert-rules-container {
margin-top: 10px;
}
}

View File

@@ -1,56 +0,0 @@
import { useCallback, useEffect } from 'react';
import { Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import SelectAlertType from 'container/CreateAlertRule/SelectAlertType';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { AlertTypes } from 'types/api/alerts/alertTypes';
function AlertTypeSelectionPage(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const queryParams = useUrlQuery();
useEffect(() => {
logEvent('Alert: New alert data source selection page visited', {});
}, []);
const handleSelectType = useCallback(
(type: AlertTypes, newTab?: boolean): void => {
// For anamoly based alert, we need to set the ruleType to anomaly_rule
// and alertType to metrics_based_alert
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
queryParams.set(
QueryParams.ruleType,
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
);
queryParams.set(QueryParams.alertType, AlertTypes.METRICS_BASED_ALERT);
// For other alerts, we need to set the ruleType to threshold_rule
// and alertType to the selected type
} else {
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
queryParams.set(QueryParams.alertType, type);
}
const showClassicCreateAlertsPageFlag = queryParams.get(
QueryParams.showClassicCreateAlertsPage,
);
if (showClassicCreateAlertsPageFlag === 'true') {
queryParams.set(QueryParams.showClassicCreateAlertsPage, 'true');
}
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { newTab });
},
[queryParams, safeNavigate],
);
return (
<Row wrap={false}>
<SelectAlertType onSelect={handleSelectType} />
</Row>
);
}
export default AlertTypeSelectionPage;

View File

@@ -1,189 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
import * as navigateHooks from 'hooks/useSafeNavigate';
import * as useUrlQueryHooks from 'hooks/useUrlQuery';
import * as appHooks from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import AlertTypeSelection from '../AlertTypeSelection';
const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
const useSafeNavigateSpy = jest.spyOn(navigateHooks, 'useSafeNavigate');
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
const mockSetUrlQuery = jest.fn();
const mockSafeNavigate = jest.fn();
const mockToString = jest.fn();
const mockGetUrlQuery = jest.fn();
describe('AlertTypeSelection', () => {
beforeEach(() => {
jest.clearAllMocks();
useAppContextSpy.mockReturnValue(getAppContextMockState());
useUrlQuerySpy.mockReturnValue({
set: mockSetUrlQuery,
toString: mockToString,
get: mockGetUrlQuery,
} as Partial<URLSearchParams> as URLSearchParams);
useSafeNavigateSpy.mockReturnValue({
safeNavigate: mockSafeNavigate,
});
});
it('should render all alert type options when anomaly detection is enabled', () => {
useAppContextSpy.mockReturnValue({
...getAppContextMockState({}),
featureFlags: [
{
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
});
render(<AlertTypeSelection />);
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
expect(screen.getByText('anomaly_based_alert')).toBeInTheDocument();
});
it('should render all alert type options except anomaly based alert when anomaly detection is disabled', () => {
render(<AlertTypeSelection />);
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
expect(screen.queryByText('anomaly_based_alert')).not.toBeInTheDocument();
});
it('should navigate to metrics based alert with correct params', () => {
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('metric_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to anomaly based alert with correct params', () => {
useAppContextSpy.mockReturnValue({
...getAppContextMockState({}),
featureFlags: [
{
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
});
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('anomaly_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to log based alert with correct params', () => {
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('log_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.LOGS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to traces based alert with correct params', () => {
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('traces_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.TRACES_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to exceptions based alert with correct params', () => {
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('exceptions_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.EXCEPTIONS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to classic create alerts page with correct params if showClassicCreateAlertsPage is true', () => {
useUrlQuerySpy.mockReturnValue({
set: mockSetUrlQuery,
toString: mockToString,
get: mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.showClassicCreateAlertsPage) {
return 'true';
}
return null;
}),
} as Partial<URLSearchParams> as URLSearchParams);
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('metric_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(3);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.showClassicCreateAlertsPage,
'true',
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
});

View File

@@ -1,3 +0,0 @@
import AlertTypeSelectionPage from './AlertTypeSelection';
export default AlertTypeSelectionPage;

View File

@@ -526,7 +526,7 @@ function SpanDetailsPanel({
const PANEL_WIDTH = 500;
const PANEL_MARGIN_RIGHT = 20;
const PANEL_MARGIN_TOP = 25;
const PANEL_MARGIN_TOP = 50;
const PANEL_MARGIN_BOTTOM = 25;
const content = (

View File

@@ -580,10 +580,9 @@ function Success(props: ISuccessProps): JSX.Element {
}
return next;
});
return;
}
// Backend mode: trigger API call (current behavior)
// keeping this for both mode to support scroll to view to function well.
// interestedspan would not make api call in frontend mode so it is safe to use for both mode.
// Backend mode: trigger refetch via interestedSpanId
setInterestedSpanId({
spanId,
isUncollapsed: !collapse,
@@ -782,19 +781,26 @@ function Success(props: ISuccessProps): JSX.Element {
[],
);
// Backend mode: scroll + select to the interestedSpanId target. `spans` in
// deps so we retry once a refetch lands (chevron / pagination / deep-link).
useEffect(() => {
if (interestedSpanId.spanId !== '') {
const idx = spans.findIndex(
(span) => span.span_id === interestedSpanId.spanId,
);
if (idx !== -1) {
scrollSpanIntoView(spans[idx], spans);
setSelectedSpan(spans[idx]);
}
} else {
setSelectedSpan((prev) => prev ?? spans[0]);
if (isFullDataLoaded || interestedSpanId.spanId === '') {
return;
}
}, [interestedSpanId, setSelectedSpan, spans, scrollSpanIntoView]);
const idx = spans.findIndex(
(span) => span.span_id === interestedSpanId.spanId,
);
if (idx !== -1) {
scrollSpanIntoView(spans[idx], spans);
setSelectedSpan(spans[idx]);
}
}, [
interestedSpanId,
setSelectedSpan,
spans,
scrollSpanIntoView,
isFullDataLoaded,
]);
// Covers URL-driven navigation to an already-loaded span (flamegraph /
// filter / browser back) that the interestedSpanId-keyed effect doesn't see.

View File

@@ -199,10 +199,12 @@ const mockSpans = [
createMockSpan('span-3', 1),
];
// Shared TestComponent for all tests
// Shared TestComponent for all tests. Default selectedSpan to the root mirrors
// what TraceDetailsV3's deep-link one-shot effect does when there's no spanId
// in the URL — Success no longer owns that default itself.
function TestComponent(): JSX.Element {
const [selectedSpan, setSelectedSpan] = React.useState<SpanV3 | undefined>(
undefined,
mockSpans[0],
);
return (

View File

@@ -75,6 +75,7 @@ function TraceDetailsV3(): JSX.Element {
});
const allSpansRef = useRef<SpanV3[]>([]);
const deepLinkResolvedRef = useRef(false);
// 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.
@@ -175,12 +176,36 @@ function TraceDetailsV3(): JSX.Element {
}
}, [traceData, isFullDataLoaded]);
// Frontend mode: auto-expand ancestors of the selected span so it becomes visible
// Tracks whether we've already done the initial URL→selectedSpan handoff
//Lets `interestedSpanId` stay purely as the refetch trigger in frontend mode.
useEffect(() => {
if (!isFullDataLoaded || !interestedSpanId.spanId || allSpans.length === 0) {
if (deepLinkResolvedRef.current) {
return;
}
const ancestors = getAncestorSpanIds(allSpans, interestedSpanId.spanId);
if (allSpans.length === 0) {
return;
}
if (selectedSpanId) {
const span = allSpans.find((s) => s.span_id === selectedSpanId);
if (!span) {
// Span not in the current window — wait for more data (backend
// pagination) before marking resolved.
return;
}
setSelectedSpan(span);
} else {
setSelectedSpan((prev) => prev ?? allSpans[0]);
}
deepLinkResolvedRef.current = true;
}, [selectedSpanId, allSpans]);
// Frontend mode: auto-expand ancestors of the URL-targeted span so it's
// visible. Keyed on URL `spanId`(selectedSpanId).
useEffect(() => {
if (!isFullDataLoaded || !selectedSpanId || allSpans.length === 0) {
return;
}
const ancestors = getAncestorSpanIds(allSpans, selectedSpanId);
if (ancestors.size === 0) {
return;
}
@@ -203,7 +228,7 @@ function TraceDetailsV3(): JSX.Element {
}
return next;
});
}, [isFullDataLoaded, interestedSpanId.spanId, allSpans]);
}, [isFullDataLoaded, selectedSpanId, allSpans]);
const [activeKeys, setActiveKeys] = useState<string[]>(['flame', 'waterfall']);
@@ -217,7 +242,7 @@ function TraceDetailsV3(): JSX.Element {
() =>
(getLocalStorageKey(
LOCALSTORAGE.TRACE_DETAILS_SPAN_DETAILS_POSITION,
) as SpanDetailVariant) || SpanDetailVariant.DOCKED,
) as SpanDetailVariant) || SpanDetailVariant.DIALOG,
);
const handleVariantChange = useCallback(

View File

@@ -9,6 +9,8 @@ import React, {
interface AlertRuleContextType {
alertRuleState: string | undefined;
setAlertRuleState: React.Dispatch<React.SetStateAction<string | undefined>>;
alertRuleName: string | undefined;
setAlertRuleName: React.Dispatch<React.SetStateAction<string | undefined>>;
}
const AlertRuleContext = createContext<AlertRuleContextType | undefined>(
@@ -23,13 +25,18 @@ function AlertRuleProvider({
const [alertRuleState, setAlertRuleState] = useState<string | undefined>(
undefined,
);
const [alertRuleName, setAlertRuleName] = useState<string | undefined>(
undefined,
);
const value = React.useMemo(
() => ({
alertRuleState,
setAlertRuleState,
alertRuleName,
setAlertRuleName,
}),
[alertRuleState],
[alertRuleState, alertRuleName],
);
return (
@@ -47,4 +54,7 @@ export const useAlertRule = (): AlertRuleContextType => {
return context;
};
export const useAlertRuleOptional = (): AlertRuleContextType | undefined =>
useContext(AlertRuleContext);
export default AlertRuleProvider;

View File

@@ -1,3 +1,8 @@
export interface ILogBody {
message?: string | null;
[key: string]: unknown;
}
export interface ILog {
date: string;
timestamp: number | string;
@@ -8,7 +13,7 @@ export interface ILog {
traceFlags: number;
severityText: string;
severityNumber: number;
body: string;
body: string | ILogBody;
resources_string: Record<string, never>;
scope_string: Record<string, never>;
attributesString: Record<string, never>;

View File

@@ -132,7 +132,6 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
METER: ['ADMIN', 'EDITOR', 'VIEWER'],
METER_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
PUBLIC_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'],
ALERT_TYPE_SELECTION: ['ADMIN', 'EDITOR'],
AI_ASSISTANT: ['ADMIN', 'EDITOR', 'VIEWER'],
AI_ASSISTANT_ICON_PREVIEW: ['ADMIN', 'EDITOR', 'VIEWER'],
MCP_SERVER: ['ADMIN', 'EDITOR', 'VIEWER'],

2
go.mod
View File

@@ -11,6 +11,7 @@ require (
github.com/SigNoz/signoz-otel-collector v0.144.3
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3
github.com/bytedance/sonic v1.14.1
github.com/cespare/xxhash/v2 v2.3.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/dgraph-io/ristretto/v2 v2.3.0
@@ -112,7 +113,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // 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) {
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) {
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,4 +1,4 @@
package sqlrulestore
package sqlalertmanagerstore
import (
"context"
@@ -9,8 +9,8 @@ import (
"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"
)
@@ -19,15 +19,15 @@ type maintenance struct {
logger *slog.Logger
}
func NewMaintenanceStore(store sqlstore.SQLStore, providerSettings factory.ProviderSettings) ruletypes.MaintenanceStore {
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().
@@ -39,7 +39,7 @@ 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 {
m := gettableMaintenancesRule.ToPlannedMaintenance()
gettablePlannedMaintenance = append(gettablePlannedMaintenance, m)
@@ -51,8 +51,8 @@ func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string)
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().
@@ -67,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(),
},
@@ -91,14 +91,14 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
OrgID: claims.OrgID,
}
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(),
},
@@ -135,7 +135,7 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
return nil, err
}
return &ruletypes.PlannedMaintenance{
return &alertmanagertypes.PlannedMaintenance{
ID: storablePlannedMaintenance.ID,
Name: storablePlannedMaintenance.Name,
Description: storablePlannedMaintenance.Description,
@@ -152,7 +152,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 {
@@ -162,7 +162,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
@@ -173,7 +173,7 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
return err
}
storablePlannedMaintenance := ruletypes.StorablePlannedMaintenance{
storablePlannedMaintenance := alertmanagertypes.StorablePlannedMaintenance{
Identifiable: types.Identifiable{
ID: id,
},
@@ -191,14 +191,14 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
OrgID: claims.OrgID,
}
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(),
},
@@ -221,7 +221,7 @@ 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)

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

@@ -38,7 +38,7 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
}
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard)
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, dashboardtypes.SourceUser, postableDashboard)
if err != nil {
return nil, err
}
@@ -72,7 +72,16 @@ func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboard
return nil, err
}
return dashboardtypes.NewDashboardsFromStorableDashboards(storableDashboards), nil
// system dashboards are hidden from the listing endpoint but still gettable by id.
filtered := make([]*dashboardtypes.StorableDashboard, 0, len(storableDashboards))
for _, storable := range storableDashboards {
if storable.Source == dashboardtypes.SourceSystem {
continue
}
filtered = append(filtered, storable)
}
return dashboardtypes.NewDashboardsFromStorableDashboards(filtered), nil
}
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatableDashboard dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) {
@@ -81,6 +90,10 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
return nil, err
}
if err := dashboard.ErrIfNotMutable(); err != nil {
return nil, err
}
err = dashboard.Update(ctx, updatableDashboard, updatedBy, diff)
if err != nil {
return nil, err
@@ -105,6 +118,10 @@ func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valu
return err
}
if err := dashboard.ErrIfNotLockable(); err != nil {
return err
}
err = dashboard.LockUnlock(lock, isAdmin, updatedBy)
if err != nil {
return err
@@ -128,6 +145,10 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
return err
}
if err := dashboard.ErrIfNotDeletable(); err != nil {
return err
}
if dashboard.Locked {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
}

View File

@@ -231,7 +231,7 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
return nil, err
}
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req, pageGroups)
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
@@ -314,19 +314,12 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
return nil, err
}
nodeConditionCounts, err := m.getPerGroupNodeConditionCounts(ctx, req, pageGroups)
nodeConditionCounts, err := m.getPerGroupNodeConditionCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
// Start/End/Filter/GroupBy from PostablePods.
podPhaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
Start: req.Start,
End: req.End,
Filter: req.Filter,
GroupBy: req.GroupBy,
}, pageGroups)
podPhaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
@@ -409,14 +402,7 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
return nil, err
}
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
// Start/End/Filter/GroupBy from PostablePods.
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
Start: req.Start,
End: req.End,
Filter: req.Filter,
GroupBy: req.GroupBy,
}, pageGroups)
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
@@ -498,27 +484,14 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
return nil, err
}
// Reuse the nodes condition-counts CTE function via a temp struct — it reads only
// Start/End/Filter/GroupBy from PostableNodes. With default groupBy
// [k8s.cluster.name], counts are bucketed per cluster; with a custom groupBy,
// they aggregate across clusters in that group.
nodeConditionCountsMap, err := m.getPerGroupNodeConditionCounts(ctx, &inframonitoringtypes.PostableNodes{
Start: req.Start,
End: req.End,
Filter: req.Filter,
GroupBy: req.GroupBy,
}, pageGroups)
// With default groupBy [k8s.cluster.name], counts are bucketed per cluster;
// with a custom groupBy, they aggregate across clusters in that group.
nodeConditionCountsMap, err := m.getPerGroupNodeConditionCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
// Same pattern for pod phase counts via PostablePods shim.
podPhaseCountsMap, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
Start: req.Start,
End: req.End,
Filter: req.Filter,
GroupBy: req.GroupBy,
}, pageGroups)
podPhaseCountsMap, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
@@ -689,14 +662,7 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
return nil, err
}
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
// Start/End/Filter/GroupBy from PostablePods.
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
Start: req.Start,
End: req.End,
Filter: req.Filter,
GroupBy: req.GroupBy,
}, pageGroups)
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
@@ -784,16 +750,9 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
return nil, err
}
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
// Start/End/Filter/GroupBy from PostablePods. Pods owned by a StatefulSet carry
// k8s.statefulset.name as a resource attribute, so default-groupBy gives
// per-statefulset phase counts automatically.
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
Start: req.Start,
End: req.End,
Filter: req.Filter,
GroupBy: req.GroupBy,
}, pageGroups)
// Pods owned by a StatefulSet carry k8s.statefulset.name as a resource attribute,
// so default-groupBy gives per-statefulset phase counts automatically.
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
@@ -881,16 +840,9 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
return nil, err
}
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
// Start/End/Filter/GroupBy from PostablePods. Pods owned by a Job carry
// k8s.job.name as a resource attribute, so default-groupBy gives
// per-job phase counts automatically.
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
Start: req.Start,
End: req.End,
Filter: req.Filter,
GroupBy: req.GroupBy,
}, pageGroups)
// Pods owned by a Job carry k8s.job.name as a resource attribute, so default-groupBy
// gives per-job phase counts automatically.
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
@@ -978,16 +930,9 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
return nil, err
}
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
// Start/End/Filter/GroupBy from PostablePods. Pods owned by a DaemonSet carry
// k8s.daemonset.name as a resource attribute, so default-groupBy gives
// per-daemonset phase counts automatically.
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
Start: req.Start,
End: req.End,
Filter: req.Filter,
GroupBy: req.GroupBy,
}, pageGroups)
// Pods owned by a DaemonSet carry k8s.daemonset.name as a resource attribute,
// so default-groupBy gives per-daemonset phase counts automatically.
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}

View File

@@ -170,27 +170,29 @@ func (m *module) getNodesTableMetadata(ctx context.Context, req *inframonitoring
// Groups absent from the result map have implicit zero counts (caller default).
func (m *module) getPerGroupNodeConditionCounts(
ctx context.Context,
req *inframonitoringtypes.PostableNodes,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
) (map[string]nodeConditionCounts, error) {
if len(pageGroups) == 0 || len(req.GroupBy) == 0 {
if len(pageGroups) == 0 || len(groupBy) == 0 {
return map[string]nodeConditionCounts{}, nil
}
// Merged filter expression (user filter + page-groups IN clauses).
reqFilterExpr := ""
if req.Filter != nil {
reqFilterExpr = req.Filter.Expression
// Merge user filter with page-groups IN clauses.
userFilterExpr := ""
if filter != nil {
userFilterExpr = filter.Expression
}
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
filterExpr := mergeFilterExpressions(reqFilterExpr, pageGroupsFilterExpr)
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
// Resolve tables. Same convention as pods.
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
uint64(req.Start), uint64(req.End), nil,
uint64(start), uint64(end), nil,
)
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
uint64(req.Start), uint64(req.End),
uint64(start), uint64(end),
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
)
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
@@ -201,7 +203,7 @@ func (m *module) getPerGroupNodeConditionCounts(
"fingerprint",
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(nodeNameAttrKey)),
}
for _, key := range req.GroupBy {
for _, key := range groupBy {
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", timeSeriesFPs.Var(key.Name), quoteIdentifier(key.Name)),
)
@@ -213,8 +215,8 @@ func (m *module) getPerGroupNodeConditionCounts(
timeSeriesFPs.GE("unix_milli", adjustedStart),
timeSeriesFPs.L("unix_milli", adjustedEnd),
)
if filterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
if mergedFilterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}
@@ -223,7 +225,7 @@ func (m *module) getPerGroupNodeConditionCounts(
}
}
timeSeriesFPsGroupBy := []string{"fingerprint", "node_name"}
for _, key := range req.GroupBy {
for _, key := range groupBy {
timeSeriesFPsGroupBy = append(timeSeriesFPsGroupBy, quoteIdentifier(key.Name))
}
timeSeriesFPs.GroupBy(timeSeriesFPsGroupBy...)
@@ -233,7 +235,7 @@ func (m *module) getPerGroupNodeConditionCounts(
latestConditionPerNode := sqlbuilder.NewSelectBuilder()
latestConditionPerNodeSelectCols := []string{"tsfp.node_name AS node_name"}
latestConditionPerNodeGroupBy := []string{"node_name"}
for _, key := range req.GroupBy {
for _, key := range groupBy {
col := quoteIdentifier(key.Name)
latestConditionPerNodeSelectCols = append(latestConditionPerNodeSelectCols, fmt.Sprintf("tsfp.%s AS %s", col, col))
latestConditionPerNodeGroupBy = append(latestConditionPerNodeGroupBy, col)
@@ -248,17 +250,17 @@ func (m *module) getPerGroupNodeConditionCounts(
))
latestConditionPerNode.Where(
latestConditionPerNode.E("samples.metric_name", nodeConditionMetricName),
latestConditionPerNode.GE("samples.unix_milli", req.Start),
latestConditionPerNode.L("samples.unix_milli", req.End),
latestConditionPerNode.GE("samples.unix_milli", start),
latestConditionPerNode.L("samples.unix_milli", end),
"tsfp.node_name != ''",
)
latestConditionPerNode.GroupBy(latestConditionPerNodeGroupBy...)
latestConditionPerNodeSQL, latestConditionPerNodeArgs := latestConditionPerNode.BuildWithFlavor(sqlbuilder.ClickHouse)
// ----- countNodesPerCondition (outer SELECT) -----
countNodesPerConditionSelectCols := make([]string, 0, len(req.GroupBy)+2)
countNodesPerConditionGroupBy := make([]string, 0, len(req.GroupBy))
for _, key := range req.GroupBy {
countNodesPerConditionSelectCols := make([]string, 0, len(groupBy)+2)
countNodesPerConditionGroupBy := make([]string, 0, len(groupBy))
for _, key := range groupBy {
col := quoteIdentifier(key.Name)
countNodesPerConditionSelectCols = append(countNodesPerConditionSelectCols, col)
countNodesPerConditionGroupBy = append(countNodesPerConditionGroupBy, col)
@@ -289,8 +291,8 @@ func (m *module) getPerGroupNodeConditionCounts(
result := make(map[string]nodeConditionCounts)
for rows.Next() {
groupVals := make([]string, len(req.GroupBy))
scanPtrs := make([]any, 0, len(req.GroupBy)+2)
groupVals := make([]string, len(groupBy))
scanPtrs := make([]any, 0, len(groupBy)+2)
for i := range groupVals {
scanPtrs = append(scanPtrs, &groupVals[i])
}

View File

@@ -189,27 +189,29 @@ func (m *module) getPodsTableMetadata(ctx context.Context, req *inframonitoringt
// Groups absent from the result map have implicit zero counts (caller default).
func (m *module) getPerGroupPodPhaseCounts(
ctx context.Context,
req *inframonitoringtypes.PostablePods,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
) (map[string]podPhaseCounts, error) {
if len(pageGroups) == 0 || len(req.GroupBy) == 0 {
if len(pageGroups) == 0 || len(groupBy) == 0 {
return map[string]podPhaseCounts{}, nil
}
// Merged filter expression (user filter + page-groups IN clauses).
reqFilterExpr := ""
if req.Filter != nil {
reqFilterExpr = req.Filter.Expression
// Merge user filter with page-groups IN clauses.
userFilterExpr := ""
if filter != nil {
userFilterExpr = filter.Expression
}
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
filterExpr := mergeFilterExpressions(reqFilterExpr, pageGroupsFilterExpr)
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
// Resolve tables. Same convention as hosts (distributed names from helpers).
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
uint64(req.Start), uint64(req.End), nil,
uint64(start), uint64(end), nil,
)
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
uint64(req.Start), uint64(req.End),
uint64(start), uint64(end),
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
)
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
@@ -220,7 +222,7 @@ func (m *module) getPerGroupPodPhaseCounts(
"fingerprint",
fmt.Sprintf("JSONExtractString(labels, %s) AS pod_uid", timeSeriesFPs.Var(podUIDAttrKey)),
}
for _, key := range req.GroupBy {
for _, key := range groupBy {
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", timeSeriesFPs.Var(key.Name), quoteIdentifier(key.Name)),
)
@@ -232,8 +234,8 @@ func (m *module) getPerGroupPodPhaseCounts(
timeSeriesFPs.GE("unix_milli", adjustedStart),
timeSeriesFPs.L("unix_milli", adjustedEnd),
)
if filterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
if mergedFilterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}
@@ -242,7 +244,7 @@ func (m *module) getPerGroupPodPhaseCounts(
}
}
timeSeriesFPsGroupBy := []string{"fingerprint", "pod_uid"}
for _, key := range req.GroupBy {
for _, key := range groupBy {
timeSeriesFPsGroupBy = append(timeSeriesFPsGroupBy, quoteIdentifier(key.Name))
}
timeSeriesFPs.GroupBy(timeSeriesFPsGroupBy...)
@@ -251,7 +253,7 @@ func (m *module) getPerGroupPodPhaseCounts(
latestPhasePerPod := sqlbuilder.NewSelectBuilder()
latestPhasePerPodSelectCols := []string{"tsfp.pod_uid AS pod_uid"}
latestPhasePerPodGroupBy := []string{"pod_uid"}
for _, key := range req.GroupBy {
for _, key := range groupBy {
col := quoteIdentifier(key.Name)
latestPhasePerPodSelectCols = append(latestPhasePerPodSelectCols, fmt.Sprintf("tsfp.%s AS %s", col, col))
latestPhasePerPodGroupBy = append(latestPhasePerPodGroupBy, col)
@@ -266,17 +268,17 @@ func (m *module) getPerGroupPodPhaseCounts(
))
latestPhasePerPod.Where(
latestPhasePerPod.E("samples.metric_name", podPhaseMetricName),
latestPhasePerPod.GE("samples.unix_milli", req.Start),
latestPhasePerPod.L("samples.unix_milli", req.End),
latestPhasePerPod.GE("samples.unix_milli", start),
latestPhasePerPod.L("samples.unix_milli", end),
"tsfp.pod_uid != ''",
)
latestPhasePerPod.GroupBy(latestPhasePerPodGroupBy...)
latestPhasePerPodSQL, latestPhasePerPodArgs := latestPhasePerPod.BuildWithFlavor(sqlbuilder.ClickHouse)
// ----- countPodsPerPhase (outer SELECT) -----
countPodsPerPhaseSelectCols := make([]string, 0, len(req.GroupBy)+5)
countPodsPerPhaseGroupBy := make([]string, 0, len(req.GroupBy))
for _, key := range req.GroupBy {
countPodsPerPhaseSelectCols := make([]string, 0, len(groupBy)+5)
countPodsPerPhaseGroupBy := make([]string, 0, len(groupBy))
for _, key := range groupBy {
col := quoteIdentifier(key.Name)
countPodsPerPhaseSelectCols = append(countPodsPerPhaseSelectCols, col)
countPodsPerPhaseGroupBy = append(countPodsPerPhaseGroupBy, col)
@@ -310,8 +312,8 @@ func (m *module) getPerGroupPodPhaseCounts(
result := make(map[string]podPhaseCounts)
for rows.Next() {
groupVals := make([]string, len(req.GroupBy))
scanPtrs := make([]any, 0, len(req.GroupBy)+5)
groupVals := make([]string, len(groupBy))
scanPtrs := make([]any, 0, len(groupBy)+5)
for i := range groupVals {
scanPtrs = append(scanPtrs, &groupVals[i])
}

View File

@@ -199,7 +199,16 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
return q.executeWindowList(ctx)
}
stmt, err := q.stmtBuilder.Build(ctx, q.fromMS, q.toMS, q.kind, q.spec, q.variables)
fromMS, toMS := q.fromMS, q.toMS
if q.spec.Signal == telemetrytypes.SignalTraces || q.spec.Signal == telemetrytypes.SignalLogs {
var overlap bool
fromMS, toMS, overlap = q.narrowWindowByTraceID(ctx, fromMS, toMS)
if !overlap {
return emptyResultFor(q.kind, q.spec.Name), nil
}
}
stmt, err := q.stmtBuilder.Build(ctx, fromMS, toMS, q.kind, q.spec, q.variables)
if err != nil {
return nil, err
}
@@ -215,6 +224,81 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
return result, nil
}
// narrowWindowByTraceID inspects the filter for trace_id predicates and clamps
// [fromMS,toMS] to the time range stored in signoz_traces.distributed_trace_summary.
// Returns the (possibly narrowed) window and overlap=false when the trace lies
// completely outside the query window — callers should short-circuit in that case.
//
// When the trace_id is not present in trace_summary the behaviour differs by
// signal:
// - traces: trace_summary is derived from the spans table, so a missing row
// means no spans exist for that trace_id; we short-circuit to empty.
// - logs: logs can carry a trace_id even when traces are not ingested at all
// (e.g. traces disabled). We must not short-circuit; instead leave the
// window untouched and let the query run.
func (q *builderQuery[T]) narrowWindowByTraceID(ctx context.Context, fromMS, toMS uint64) (uint64, uint64, bool) {
if q.spec.Filter == nil || q.spec.Filter.Expression == "" {
return fromMS, toMS, true
}
traceIDs, found := telemetrytraces.ExtractTraceIDsFromFilter(q.spec.Filter.Expression)
if !found || len(traceIDs) == 0 {
return fromMS, toMS, true
}
finder := telemetrytraces.NewTraceTimeRangeFinder(q.telemetryStore)
traceStart, traceEnd, ok := finder.GetTraceTimeRangeMulti(ctx, traceIDs)
if !ok {
if q.spec.Signal == telemetrytypes.SignalTraces {
q.logger.DebugContext(ctx, "trace_id not found in trace_summary; short-circuiting traces query to empty",
slog.Any("trace_ids", traceIDs))
return fromMS, toMS, false
}
q.logger.DebugContext(ctx, "trace_id not found in trace_summary; leaving time range untouched for logs",
slog.Any("trace_ids", traceIDs))
return fromMS, toMS, true
}
traceStartMS := uint64(traceStart) / 1_000_000
traceEndMS := uint64(traceEnd) / 1_000_000
if traceStartMS == 0 || traceEndMS == 0 {
return fromMS, toMS, true
}
if traceStartMS > toMS || traceEndMS < fromMS {
return fromMS, toMS, false
}
if traceStartMS > fromMS {
fromMS = traceStartMS
}
if traceEndMS < toMS {
toMS = traceEndMS
}
q.logger.DebugContext(ctx, "optimized time range using trace_id lookup",
slog.String("signal", q.spec.Signal.StringValue()),
slog.Any("trace_ids", traceIDs),
slog.Uint64("start", fromMS),
slog.Uint64("end", toMS))
return fromMS, toMS, true
}
// emptyResultFor returns an empty result payload appropriate for the given kind.
func emptyResultFor(kind qbtypes.RequestType, queryName string) *qbtypes.Result {
var value any
switch kind {
case qbtypes.RequestTypeTimeSeries:
value = &qbtypes.TimeSeriesData{QueryName: queryName}
case qbtypes.RequestTypeScalar:
value = &qbtypes.ScalarData{QueryName: queryName}
default:
value = &qbtypes.RawData{QueryName: queryName}
}
return &qbtypes.Result{
Type: kind,
Value: value,
}
}
// executeWithContext executes the query with query window and step context for partial value detection.
func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string, args []any) (*qbtypes.Result, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
@@ -310,42 +394,22 @@ func (q *builderQuery[T]) executeWindowList(ctx context.Context) (*qbtypes.Resul
totalBytes := uint64(0)
start := time.Now()
// Check if filter contains trace_id(s) and optimize time range if needed
if q.spec.Signal == telemetrytypes.SignalTraces &&
q.spec.Filter != nil && q.spec.Filter.Expression != "" {
traceIDs, found := telemetrytraces.ExtractTraceIDsFromFilter(q.spec.Filter.Expression)
if found && len(traceIDs) > 0 {
finder := telemetrytraces.NewTraceTimeRangeFinder(q.telemetryStore)
traceStart, traceEnd, ok := finder.GetTraceTimeRangeMulti(ctx, traceIDs)
traceStartMS := uint64(traceStart) / 1_000_000
traceEndMS := uint64(traceEnd) / 1_000_000
if !ok {
q.logger.DebugContext(ctx, "failed to get trace time range", slog.Any("trace_ids", traceIDs))
} else if traceStartMS > 0 && traceEndMS > 0 {
// no overlap — nothing to return
if uint64(traceStartMS) > toMS || uint64(traceEndMS) < fromMS {
return &qbtypes.Result{
Type: qbtypes.RequestTypeRaw,
Value: &qbtypes.RawData{
QueryName: q.spec.Name,
},
Stats: qbtypes.ExecStats{
DurationMS: uint64(time.Since(start).Milliseconds()),
},
}, nil
}
// clamp window to trace time range before bucketing
if uint64(traceStartMS) > fromMS {
fromMS = uint64(traceStartMS)
}
if uint64(traceEndMS) < toMS {
toMS = uint64(traceEndMS)
}
q.logger.DebugContext(ctx, "optimized time range for traces", slog.Any("trace_ids", traceIDs), slog.Uint64("start", fromMS), slog.Uint64("end", toMS))
}
// Check if filter contains trace_id(s) and optimize time range if needed.
// Applies to both traces (the listing this branch was built for) and logs
// (which carry trace_id and benefit from the same clamp before bucketing).
if q.spec.Signal == telemetrytypes.SignalTraces || q.spec.Signal == telemetrytypes.SignalLogs {
var overlap bool
fromMS, toMS, overlap = q.narrowWindowByTraceID(ctx, fromMS, toMS)
if !overlap {
return &qbtypes.Result{
Type: qbtypes.RequestTypeRaw,
Value: &qbtypes.RawData{
QueryName: q.spec.Name,
},
Stats: qbtypes.ExecStats{
DurationMS: uint64(time.Since(start).Milliseconds()),
},
}, nil
}
}

View File

@@ -12,8 +12,10 @@ import (
"time"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/bytedance/sonic"
)
var (
@@ -22,6 +24,8 @@ var (
// written clickhouse query. The column alias indcate which value is
// to be considered as final result (or target).
legacyReservedColumnTargetAliases = []string{"__result", "__value", "result", "res", "value"}
CodeFailUnmarshalJSONColumn = errors.MustNewCode("fail_unmarshal_json_column")
)
// consume reads every row and shapes it into the payload expected for the
@@ -393,11 +397,16 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
// de-reference the typed pointer to any
val := reflect.ValueOf(cellPtr).Elem().Interface()
// Post-process JSON columns: normalize into String value
// Post-process JSON columns: unmarshal bytes into map[string]any
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
switch x := val.(type) {
case []byte:
val = string(x)
var m map[string]any
err := sonic.Unmarshal(x, &m)
if err != nil {
return nil, errors.WrapInternalf(err, CodeFailUnmarshalJSONColumn, "failed to unmarshal JSON column %s", name)
}
val = m
default:
// already a structured type (map[string]any, []any, etc.)
}

View File

@@ -12,9 +12,12 @@ import (
"github.com/SigNoz/govaluate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// queryInfo holds common query properties.
@@ -50,7 +53,7 @@ func getQueryName(spec any) string {
return getqueryInfo(spec).Name
}
func (q *querier) postProcessResults(ctx context.Context, results map[string]any, req *qbtypes.QueryRangeRequest) (map[string]any, error) {
func (q *querier) postProcessResults(ctx context.Context, orgID valuer.UUID, results map[string]any, req *qbtypes.QueryRangeRequest) (map[string]any, error) {
// Convert results to typed format for processing
typedResults := make(map[string]*qbtypes.Result)
for name, result := range results {
@@ -69,6 +72,7 @@ func (q *querier) postProcessResults(ctx context.Context, results map[string]any
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
if result, ok := typedResults[spec.Name]; ok {
result = postProcessBuilderQuery(q, result, spec, req)
result = q.postProcessLogBody(ctx, orgID, result, req)
typedResults[spec.Name] = result
}
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
@@ -1046,3 +1050,33 @@ func (q *querier) calculateFormulaStep(expression string, req *qbtypes.QueryRang
return result
}
// postProcessLogBody removes the "message" key from the body map when it is empty.
// Only runs for raw list queries with the use_json_body feature enabled.
func (q *querier) postProcessLogBody(ctx context.Context, orgID valuer.UUID, result *qbtypes.Result, req *qbtypes.QueryRangeRequest) *qbtypes.Result {
if req.RequestType != qbtypes.RequestTypeRaw {
return result
}
if !q.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID)) {
return result
}
rawData, ok := result.Value.(*qbtypes.RawData)
if !ok {
return result
}
for _, row := range rawData.Rows {
bodyMap, ok := row.Data["body"].(map[string]any)
if !ok {
continue
}
if msg, exists := bodyMap["message"]; exists {
switch v := msg.(type) {
case string:
if v == "" {
delete(bodyMap, "message")
}
}
}
}
return result
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/querybuilder"
@@ -35,6 +36,7 @@ var (
type querier struct {
logger *slog.Logger
fl flagger.Flagger
telemetryStore telemetrystore.TelemetryStore
metadataStore telemetrytypes.MetadataStore
promEngine prometheus.Prometheus
@@ -62,10 +64,12 @@ func New(
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
traceOperatorStmtBuilder qbtypes.TraceOperatorStatementBuilder,
bucketCache BucketCache,
flagger flagger.Flagger,
) *querier {
querierSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querier")
return &querier{
logger: querierSettings.Logger(),
fl: flagger,
telemetryStore: telemetryStore,
metadataStore: metadataStore,
promEngine: promEngine,
@@ -684,7 +688,7 @@ func (q *querier) run(
}
gomaps.Copy(results, preseededResults)
processedResults, err := q.postProcessResults(ctx, results, req)
processedResults, err := q.postProcessResults(ctx, orgID, results, req)
if err != nil {
return nil, err
}

View File

@@ -7,6 +7,7 @@ import (
cmock "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
@@ -44,14 +45,15 @@ func TestQueryRange_MetricTypeMissing(t *testing.T) {
providerSettings,
nil, // telemetryStore
metadataStore,
nil, // prometheus
nil, // traceStmtBuilder
nil, // logStmtBuilder
nil, // auditStmtBuilder
nil, // metricStmtBuilder
nil, // meterStmtBuilder
nil, // traceOperatorStmtBuilder
nil, // bucketCache
nil, // prometheus
nil, // traceStmtBuilder
nil, // logStmtBuilder
nil, // auditStmtBuilder
nil, // metricStmtBuilder
nil, // meterStmtBuilder
nil, // traceOperatorStmtBuilder
nil, // bucketCache
flaggertest.New(t), // flagger
)
req := &qbtypes.QueryRangeRequest{
@@ -116,6 +118,7 @@ func TestQueryRange_MetricTypeFromStore(t *testing.T) {
nil, // meterStmtBuilder
nil, // traceOperatorStmtBuilder
nil, // bucketCache
flaggertest.New(t), // flagger
)
req := &qbtypes.QueryRangeRequest{

View File

@@ -186,5 +186,6 @@ func newProvider(
meterStmtBuilder,
traceOperatorStmtBuilder,
bucketCache,
flagger,
), nil
}

View File

@@ -348,7 +348,8 @@ func (m *Manager) GetInstalledIntegrationDashboardById(
CreatedBy: author,
UpdatedBy: author,
},
OrgID: orgId,
OrgID: orgId,
Source: dashboardtypes.SourceIntegration,
}, nil
}
}
@@ -387,7 +388,8 @@ func (m *Manager) GetDashboardsForInstalledIntegrations(
CreatedBy: author,
UpdatedBy: author,
},
OrgID: orgId,
OrgID: orgId,
Source: dashboardtypes.SourceIntegration,
})
}
}

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

@@ -53,6 +53,7 @@ func prepareQuerierForMetrics(t *testing.T, telemetryStore telemetrystore.Teleme
nil, // meterStmtBuilder
nil, // traceOperatorStmtBuilder
nil, // bucketCache
flagger,
), metadataStore
}
@@ -102,6 +103,7 @@ func prepareQuerierForLogs(t *testing.T, telemetryStore telemetrystore.Telemetry
nil, // meterStmtBuilder
nil, // traceOperatorStmtBuilder
nil, // bucketCache
fl,
)
}
@@ -146,5 +148,6 @@ func prepareQuerierForTraces(t *testing.T, telemetryStore telemetrystore.Telemet
nil, // meterStmtBuilder
nil, // traceOperatorStmtBuilder
nil, // bucketCache
fl,
)
}

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
}

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