mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-20 17:00:29 +01:00
Compare commits
6 Commits
main
...
feat/ai-as
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcdfba5ab5 | ||
|
|
567ab424ef | ||
|
|
82cabd2e87 | ||
|
|
d05a20c973 | ||
|
|
6f204a8e13 | ||
|
|
bdb274183d |
@@ -8,14 +8,6 @@ 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
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.125.0
|
||||
image: signoz/signoz:v0.124.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.125.0
|
||||
image: signoz/signoz:v0.124.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -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.125.0}
|
||||
image: signoz/signoz:${VERSION:-v0.124.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -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.125.0}
|
||||
image: signoz/signoz:${VERSION:-v0.124.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -96,53 +96,6 @@ 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:
|
||||
@@ -259,23 +212,6 @@ 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:
|
||||
@@ -301,60 +237,6 @@ 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:
|
||||
@@ -2342,8 +2224,6 @@ components:
|
||||
type: boolean
|
||||
org_id:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/components/schemas/DashboardtypesSource'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -2373,12 +2253,6 @@ components:
|
||||
timeRangeEnabled:
|
||||
type: boolean
|
||||
type: object
|
||||
DashboardtypesSource:
|
||||
enum:
|
||||
- user
|
||||
- system
|
||||
- integration
|
||||
type: object
|
||||
DashboardtypesStorableDashboardData:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
@@ -5263,6 +5137,17 @@ components:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
RuletypesMaintenanceKind:
|
||||
enum:
|
||||
- fixed
|
||||
- recurring
|
||||
type: string
|
||||
RuletypesMaintenanceStatus:
|
||||
enum:
|
||||
- active
|
||||
- upcoming
|
||||
- expired
|
||||
type: string
|
||||
RuletypesMatchType:
|
||||
enum:
|
||||
- at_least_once
|
||||
@@ -5290,6 +5175,59 @@ 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:
|
||||
@@ -5342,6 +5280,29 @@ 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:
|
||||
@@ -5353,6 +5314,22 @@ 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:
|
||||
@@ -5472,6 +5449,21 @@ 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
|
||||
@@ -8032,7 +8024,7 @@ paths:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
@@ -8075,7 +8067,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
@@ -8083,7 +8075,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -8186,7 +8178,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -8240,7 +8232,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
|
||||
@@ -49,14 +49,6 @@ 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
|
||||
@@ -137,14 +129,6 @@ 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))
|
||||
}
|
||||
|
||||
@@ -154,10 +138,6 @@ 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")
|
||||
}
|
||||
@@ -188,14 +168,6 @@ 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
|
||||
|
||||
@@ -13,6 +13,7 @@ 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) {
|
||||
@@ -48,7 +49,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)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
|
||||
|
||||
@@ -72,7 +73,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)
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
@@ -95,7 +96,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)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
@@ -209,9 +210,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) baserules.Task {
|
||||
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 {
|
||||
if taskType == baserules.TaskTypeCh {
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify)
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
}
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify)
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
const BANNED_COMPONENTS = {
|
||||
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
|
||||
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -47,6 +47,7 @@ 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'
|
||||
@@ -312,6 +313,13 @@ 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'),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AIAssistantPage,
|
||||
AlertHistory,
|
||||
AlertOverview,
|
||||
AlertTypeSelectionPage,
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
ApiMonitoring,
|
||||
@@ -212,6 +213,13 @@ 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,
|
||||
@@ -525,6 +533,18 @@ 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',
|
||||
@@ -535,9 +555,7 @@ 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,
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
AlertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
CreateDowntimeSchedule201,
|
||||
DeleteDowntimeScheduleByIDPathParameters,
|
||||
GetDowntimeScheduleByID200,
|
||||
@@ -26,6 +25,7 @@ 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 = (
|
||||
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
|
||||
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateDowntimeSchedule201>({
|
||||
url: `/api/v1/downtime_schedules`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
data: ruletypesPostablePlannedMaintenanceDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -154,13 +154,13 @@ export const getCreateDowntimeScheduleMutationOptions = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createDowntimeSchedule'];
|
||||
@@ -174,7 +174,7 @@ export const getCreateDowntimeScheduleMutationOptions = <
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> }
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
@@ -188,7 +188,7 @@ export type CreateDowntimeScheduleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>
|
||||
>;
|
||||
export type CreateDowntimeScheduleMutationBody =
|
||||
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
|
||||
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
|
||||
| undefined;
|
||||
export type CreateDowntimeScheduleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
@@ -203,13 +203,13 @@ export const useCreateDowntimeSchedule = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateDowntimeScheduleMutationOptions(options));
|
||||
@@ -403,14 +403,14 @@ export const invalidateGetDowntimeScheduleByID = async (
|
||||
*/
|
||||
export const updateDowntimeScheduleByID = (
|
||||
{ id }: UpdateDowntimeScheduleByIDPathParameters,
|
||||
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
|
||||
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/downtime_schedules/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
data: ruletypesPostablePlannedMaintenanceDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -424,7 +424,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -433,7 +433,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -450,7 +450,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
@@ -465,7 +465,7 @@ export type UpdateDowntimeScheduleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>
|
||||
>;
|
||||
export type UpdateDowntimeScheduleByIDMutationBody =
|
||||
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
|
||||
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
|
||||
| undefined;
|
||||
export type UpdateDowntimeScheduleByIDMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
@@ -482,7 +482,7 @@ export const useUpdateDowntimeScheduleByID = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -491,7 +491,7 @@ export const useUpdateDowntimeScheduleByID = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
@@ -134,109 +134,6 @@ 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
|
||||
@@ -1700,22 +1597,6 @@ 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
|
||||
@@ -2999,11 +2880,6 @@ export interface CoretypesPatchableObjectsDTO {
|
||||
deletions: CoretypesObjectGroupDTO[] | null;
|
||||
}
|
||||
|
||||
export enum DashboardtypesSourceDTO {
|
||||
user = 'user',
|
||||
system = 'system',
|
||||
integration = 'integration',
|
||||
}
|
||||
export interface DashboardtypesDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -3027,7 +2903,6 @@ export interface DashboardtypesDashboardDTO {
|
||||
* @type string
|
||||
*/
|
||||
org_id?: string;
|
||||
source?: DashboardtypesSourceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -6241,6 +6116,15 @@ 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
|
||||
@@ -6272,6 +6156,116 @@ 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 };
|
||||
@@ -7799,7 +7793,7 @@ export type ListDowntimeSchedules200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: AlertmanagertypesPlannedMaintenanceDTO[];
|
||||
data: RuletypesPlannedMaintenanceDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7807,7 +7801,7 @@ export type ListDowntimeSchedules200 = {
|
||||
};
|
||||
|
||||
export type CreateDowntimeSchedule201 = {
|
||||
data: AlertmanagertypesPlannedMaintenanceDTO;
|
||||
data: RuletypesPlannedMaintenanceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7821,7 +7815,7 @@ export type GetDowntimeScheduleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetDowntimeScheduleByID200 = {
|
||||
data: AlertmanagertypesPlannedMaintenanceDTO;
|
||||
data: RuletypesPlannedMaintenanceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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;
|
||||
@@ -1,9 +0,0 @@
|
||||
.item {
|
||||
--button-padding: 0;
|
||||
--button-font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
|
||||
.itemLast {
|
||||
color: var(--muted-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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;
|
||||
@@ -1,6 +0,0 @@
|
||||
export { default } from './AlertBreadcrumb';
|
||||
export {
|
||||
default as BreadcrumbItem,
|
||||
type BreadcrumbItemConfig,
|
||||
} from './BreadcrumbItem';
|
||||
export type { AlertBreadcrumbProps } from './AlertBreadcrumb';
|
||||
@@ -50,7 +50,6 @@ 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';
|
||||
@@ -218,17 +217,20 @@ function LogDetailInner({
|
||||
|
||||
const logBody = useMemo(() => {
|
||||
if (!isBodyJsonQueryEnabled) {
|
||||
return (log?.body as string) ?? '';
|
||||
return log?.body || '';
|
||||
}
|
||||
// Feature enabled: body is always a map; message is always a string
|
||||
const bodyObj = log?.body as ILogBody;
|
||||
if (!bodyObj) {
|
||||
return '';
|
||||
|
||||
try {
|
||||
const json = JSON.parse(log?.body || '');
|
||||
|
||||
if (typeof json?.message === 'string' && json.message !== '') {
|
||||
return json.message;
|
||||
}
|
||||
|
||||
return log?.body || '';
|
||||
} catch {
|
||||
return log?.body || '';
|
||||
}
|
||||
if (bodyObj.message) {
|
||||
return bodyObj.message;
|
||||
}
|
||||
return JSON.stringify(bodyObj);
|
||||
}, [isBodyJsonQueryEnabled, log?.body]);
|
||||
|
||||
const htmlBody = useMemo(
|
||||
|
||||
@@ -9,10 +9,7 @@ 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 {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
// hooks
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -102,7 +99,7 @@ function RawLogView({
|
||||
// Check if body is selected
|
||||
const showBody = selectedFields.some((field) => field.name === 'body');
|
||||
if (showBody) {
|
||||
parts.push(`${attributesText} ${getBodyDisplayString(data.body)}`);
|
||||
parts.push(`${attributesText} ${data.body}`);
|
||||
} else {
|
||||
parts.push(attributesText);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -90,7 +87,7 @@ export function useLogsTableColumns({
|
||||
? {
|
||||
id: 'body',
|
||||
header: 'Body',
|
||||
accessorFn: (log): string => getBodyDisplayString(log.body),
|
||||
accessorFn: (log): string => log.body,
|
||||
canBeHidden: false,
|
||||
width: { default: '100%', min: 300 },
|
||||
cell: ({ value, isActive }): ReactElement => (
|
||||
|
||||
@@ -626,10 +626,6 @@ function TanStackTableInner<TData>(
|
||||
onChange={(value): void => {
|
||||
setLimit(+value);
|
||||
pagination.onLimitChange?.(+value);
|
||||
if (page !== 1) {
|
||||
setPage(1);
|
||||
pagination.onPageChange?.(1);
|
||||
}
|
||||
}}
|
||||
items={paginationPageSizeItems}
|
||||
/>
|
||||
|
||||
@@ -401,62 +401,6 @@ 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', () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ 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',
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Collapsed activity summary — one row that hides the underlying
|
||||
// thinking + tool-call steps. Reuses the same quiet treatment as
|
||||
// ThinkingStep / ToolCallStep so it sits flush in the assistant bubble.
|
||||
.activityGroup {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.activityHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--l3-foreground);
|
||||
user-select: none;
|
||||
transition: color 0.12s ease;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.sparkleIcon {
|
||||
flex-shrink: 0;
|
||||
color: var(--accent-primary);
|
||||
|
||||
&.iconPulsing {
|
||||
animation: activityGroupPulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes activityGroupPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.55;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.activitySummary {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.toggleChevron {
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.activityBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2px 0 4px;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { ChevronDown, ChevronRight, Sparkles } from '@signozhq/icons';
|
||||
|
||||
import { formatTime } from 'utils/timeUtils';
|
||||
|
||||
import { StreamingToolCall } from '../../types';
|
||||
import ThinkingStep, { ThinkingContent, thinkingLabel } from '../ThinkingStep';
|
||||
import ToolCallStep, {
|
||||
getToolDisplayLabel,
|
||||
ToolCallContent,
|
||||
} from '../ToolCallStep';
|
||||
|
||||
import styles from './ActivityGroup.module.scss';
|
||||
|
||||
export type ActivityItem =
|
||||
| { id: string; kind: 'thinking'; content: string }
|
||||
| { id: string; kind: 'tool'; toolCall: StreamingToolCall };
|
||||
|
||||
interface ActivityGroupProps {
|
||||
items: ActivityItem[];
|
||||
/**
|
||||
* True only for the trailing activity group of an active stream — drives
|
||||
* the live "Working…" label and the elapsed-time tick (which re-stamps on
|
||||
* approval/clarification resume so wait time isn't counted).
|
||||
*/
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-item groups get a step-specific summary so the user doesn't see a
|
||||
* pointless "Worked through 1 step". Multi-item groups roll up into the
|
||||
* generic "Working… / Worked through N steps" treatment.
|
||||
*/
|
||||
function buildSummary(
|
||||
items: ActivityItem[],
|
||||
isLive: boolean,
|
||||
elapsed: number,
|
||||
): string {
|
||||
if (items.length === 1) {
|
||||
const [only] = items;
|
||||
if (only.kind === 'thinking') {
|
||||
return thinkingLabel(isLive);
|
||||
}
|
||||
return getToolDisplayLabel(only.toolCall);
|
||||
}
|
||||
const stepLabel = `${items.length} steps`;
|
||||
if (!isLive) {
|
||||
return `Worked through ${stepLabel}`;
|
||||
}
|
||||
// Suppress the elapsed token until ≥ 1s — the first tick fires after
|
||||
// 1s anyway, and showing "0s" or "<1s" briefly adds noise.
|
||||
return elapsed >= 1000
|
||||
? `Working… · ${formatTime(elapsed / 1000)} · ${stepLabel}`
|
||||
: `Working… · ${stepLabel}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single collapsed summary row that hides a run of thinking + tool-call steps.
|
||||
* Expands to show each underlying step inline. Used for every activity row
|
||||
* (including single-item ones) so all "what the agent did" rows share a
|
||||
* consistent ✨-led visual contract.
|
||||
*/
|
||||
export default function ActivityGroup({
|
||||
items,
|
||||
isLive = false,
|
||||
}: ActivityGroupProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// Captures the moment this live phase started. Re-stamped on every
|
||||
// false→true transition so a stream that pauses on
|
||||
// approval/clarification and resumes doesn't roll the user's wait time
|
||||
// into the elapsed counter.
|
||||
const startedAtRef = useRef<number>(Date.now());
|
||||
const wasLiveRef = useRef<boolean>(isLive);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLive && !wasLiveRef.current) {
|
||||
startedAtRef.current = Date.now();
|
||||
setElapsed(0);
|
||||
}
|
||||
wasLiveRef.current = isLive;
|
||||
|
||||
if (!isLive) {
|
||||
return undefined;
|
||||
}
|
||||
// Tick once per second — the display is integer-second precision, so
|
||||
// faster ticks would just re-render the bubble for no visible change.
|
||||
const id = window.setInterval(() => {
|
||||
setElapsed(Date.now() - startedAtRef.current);
|
||||
}, 1000);
|
||||
return (): void => window.clearInterval(id);
|
||||
}, [isLive]);
|
||||
|
||||
const summary = buildSummary(items, isLive, elapsed);
|
||||
const isSingle = items.length === 1;
|
||||
|
||||
const toggle = (): void => setExpanded((v) => !v);
|
||||
|
||||
return (
|
||||
<div className={styles.activityGroup}>
|
||||
<div className={styles.activityHeader} onClick={toggle}>
|
||||
<Sparkles
|
||||
size={12}
|
||||
className={cx(styles.sparkleIcon, { [styles.iconPulsing]: isLive })}
|
||||
/>
|
||||
<span className={styles.activitySummary}>{summary}</span>
|
||||
{expanded ? (
|
||||
<ChevronDown size={12} className={styles.toggleChevron} />
|
||||
) : (
|
||||
<ChevronRight size={12} className={styles.toggleChevron} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className={styles.activityBody}>
|
||||
{isSingle ? (
|
||||
// Single-item: the outer chevron already provides disclosure,
|
||||
// so render the underlying content directly instead of wrapping
|
||||
// it in a second collapsible step row.
|
||||
items[0].kind === 'thinking' ? (
|
||||
<ThinkingContent content={items[0].content} />
|
||||
) : (
|
||||
<ToolCallContent toolCall={items[0].toolCall} />
|
||||
)
|
||||
) : (
|
||||
items.map((item, i) => {
|
||||
// A thinking step is live only while it's the trailing item
|
||||
// in a trailing live group — once any later event (text or
|
||||
// tool) arrives, the pass is done.
|
||||
const isLastItem = i === items.length - 1;
|
||||
return item.kind === 'thinking' ? (
|
||||
<ThinkingStep
|
||||
key={item.id}
|
||||
content={item.content}
|
||||
isLive={isLive && isLastItem}
|
||||
/>
|
||||
) : (
|
||||
<ToolCallStep key={item.id} toolCall={item.toolCall} />
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './ActivityGroup';
|
||||
export type { ActivityItem } from './ActivityGroup';
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -9,11 +9,10 @@ import '../blocks';
|
||||
import { useVariant } from '../../VariantContext';
|
||||
import { Message, MessageBlock } from '../../types';
|
||||
import ActionsSection from '../ActionsSection';
|
||||
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
|
||||
import { RichCodeBlock } from '../blocks';
|
||||
import { MessageContext } from '../MessageContext';
|
||||
import MessageFeedback from '../MessageFeedback';
|
||||
import ThinkingStep from '../ThinkingStep';
|
||||
import ToolCallStep from '../ToolCallStep';
|
||||
import UserMessageActions from '../UserMessageActions';
|
||||
|
||||
import styles from './MessageBubble.module.scss';
|
||||
@@ -40,38 +39,61 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
|
||||
const MD_PLUGINS = [remarkGfm];
|
||||
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
|
||||
|
||||
/** Renders a single MessageBlock by type. */
|
||||
function renderBlock(block: MessageBlock, index: number): JSX.Element {
|
||||
switch (block.type) {
|
||||
case 'thinking':
|
||||
return <ThinkingStep key={index} content={block.content} />;
|
||||
case 'tool_call':
|
||||
// Blocks in a persisted message are always complete — done is always true.
|
||||
return (
|
||||
<ToolCallStep
|
||||
key={index}
|
||||
toolCall={{
|
||||
toolName: block.toolName,
|
||||
input: block.toolInput,
|
||||
result: block.result,
|
||||
done: true,
|
||||
displayText: block.displayText,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 'text':
|
||||
default:
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={index}
|
||||
className={styles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{block.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
type RenderGroup =
|
||||
| { kind: 'text'; id: string; content: string }
|
||||
| { kind: 'activity'; id: string; items: ActivityItem[] };
|
||||
|
||||
/**
|
||||
* Partition message blocks into render groups so consecutive thinking and
|
||||
* tool_call blocks collapse into a single ActivityGroup row. Text blocks
|
||||
* stand alone, mirroring the streaming view.
|
||||
*/
|
||||
function groupBlocks(blocks: MessageBlock[]): RenderGroup[] {
|
||||
const groups: RenderGroup[] = [];
|
||||
blocks.forEach((block, i) => {
|
||||
if (block.type === 'text') {
|
||||
groups.push({ kind: 'text', id: `text-${i}`, content: block.content });
|
||||
return;
|
||||
}
|
||||
const item: ActivityItem =
|
||||
block.type === 'thinking'
|
||||
? { id: `t-${i}`, kind: 'thinking', content: block.content }
|
||||
: {
|
||||
id: `c-${block.toolCallId}`,
|
||||
kind: 'tool',
|
||||
// Persisted blocks are always complete.
|
||||
toolCall: {
|
||||
toolName: block.toolName,
|
||||
input: block.toolInput,
|
||||
result: block.result,
|
||||
done: true,
|
||||
displayText: block.displayText,
|
||||
},
|
||||
};
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.kind === 'activity') {
|
||||
last.items.push(item);
|
||||
} else {
|
||||
groups.push({ kind: 'activity', id: `a-${i}`, items: [item] });
|
||||
}
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
function renderGroup(group: RenderGroup): JSX.Element {
|
||||
if (group.kind === 'text') {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={group.id}
|
||||
className={styles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{group.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
return <ActivityGroup key={group.id} items={group.items} />;
|
||||
}
|
||||
|
||||
interface MessageBubbleProps {
|
||||
@@ -90,6 +112,14 @@ export default function MessageBubble({
|
||||
const isUser = message.role === 'user';
|
||||
const hasBlocks = !isUser && message.blocks && message.blocks.length > 0;
|
||||
|
||||
// Recompute groups only when the blocks array identity changes — store
|
||||
// updates that don't touch this message's blocks should not re-render the
|
||||
// underlying ThinkingStep/ToolCallStep children.
|
||||
const groups = useMemo(
|
||||
() => (hasBlocks ? groupBlocks(message.blocks!) : []),
|
||||
[hasBlocks, message.blocks],
|
||||
);
|
||||
|
||||
const messageClass = cx(
|
||||
styles.message,
|
||||
isUser ? styles.user : styles.assistant,
|
||||
@@ -128,8 +158,7 @@ export default function MessageBubble({
|
||||
<p className={styles.text}>{message.content}</p>
|
||||
) : hasBlocks ? (
|
||||
<MessageContext.Provider value={{ messageId: message.id }}>
|
||||
{/* eslint-disable-next-line react/no-array-index-key */}
|
||||
{message.blocks!.map((block, i) => renderBlock(block, i))}
|
||||
{groups.map((g) => renderGroup(g))}
|
||||
</MessageContext.Provider>
|
||||
) : (
|
||||
<MessageContext.Provider value={{ messageId: message.id }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -10,11 +10,10 @@ import type {
|
||||
|
||||
import { useVariant } from '../../VariantContext';
|
||||
import { StreamingEventItem } from '../../types';
|
||||
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
|
||||
import ApprovalCard from '../ApprovalCard';
|
||||
import { RichCodeBlock } from '../blocks';
|
||||
import ClarificationForm from '../ClarificationForm';
|
||||
import ThinkingStep from '../ThinkingStep';
|
||||
import ToolCallStep from '../ToolCallStep';
|
||||
|
||||
import messageStyles from '../MessageBubble/MessageBubble.module.scss';
|
||||
import styles from './StreamingMessage.module.scss';
|
||||
@@ -33,6 +32,59 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
|
||||
const MD_PLUGINS = [remarkGfm];
|
||||
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
|
||||
|
||||
type RenderGroup =
|
||||
| { kind: 'text'; id: string; content: string }
|
||||
| {
|
||||
kind: 'activity';
|
||||
id: string;
|
||||
items: ActivityItem[];
|
||||
isTrailing: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Partition the streaming event timeline into render groups: runs of
|
||||
* consecutive thinking/tool events fold into a single activity group, text
|
||||
* events stay standalone. The last group is flagged as trailing so the
|
||||
* caller can drive a "live" indicator on it.
|
||||
*
|
||||
* Invariant relied on by the ActivityGroup elapsed-time timer: once a
|
||||
* group exists at a given array index, later events only extend its
|
||||
* `items` — they never shrink the array or re-key existing groups. That
|
||||
* keeps each ActivityGroup React instance stable across re-renders so the
|
||||
* timer's `wasLive` → `isLive` re-stamp captures the right transition.
|
||||
* The id fields below piggyback on that invariant: each event's position in
|
||||
* `events` is stable, so the derived id stays stable across re-renders.
|
||||
*/
|
||||
function groupStreamingEvents(events: StreamingEventItem[]): RenderGroup[] {
|
||||
const groups: RenderGroup[] = [];
|
||||
events.forEach((event, i) => {
|
||||
if (event.kind === 'text') {
|
||||
groups.push({ kind: 'text', id: `text-${i}`, content: event.content });
|
||||
return;
|
||||
}
|
||||
const item: ActivityItem =
|
||||
event.kind === 'thinking'
|
||||
? { id: `t-${i}`, kind: 'thinking', content: event.content }
|
||||
: { id: `c-${i}`, kind: 'tool', toolCall: event.toolCall };
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.kind === 'activity') {
|
||||
last.items.push(item);
|
||||
} else {
|
||||
groups.push({
|
||||
kind: 'activity',
|
||||
id: `a-${i}`,
|
||||
items: [item],
|
||||
isTrailing: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.kind === 'activity') {
|
||||
last.isTrailing = true;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** Human-readable labels for execution status codes shown before any events arrive. */
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
queued: 'Queued…',
|
||||
@@ -79,6 +131,11 @@ export default function StreamingMessage({
|
||||
[messageStyles.compact]: isCompact,
|
||||
});
|
||||
|
||||
// Recompute groups only when the events array identity changes. The
|
||||
// streaming reducer pushes new entries into the same array reference
|
||||
// once per tick, so this naturally invalidates as events arrive.
|
||||
const groups = useMemo(() => groupStreamingEvents(events), [events]);
|
||||
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
<div className={messageStyles.bubble}>
|
||||
@@ -88,27 +145,28 @@ export default function StreamingMessage({
|
||||
)}
|
||||
{isEmpty && !statusLabel && <TypingDots />}
|
||||
|
||||
{/* eslint-disable react/no-array-index-key */}
|
||||
{/* Events rendered in arrival order: text, thinking, and tool calls interleaved */}
|
||||
{events.map((event, i) => {
|
||||
if (event.kind === 'tool') {
|
||||
return <ToolCallStep key={i} toolCall={event.toolCall} />;
|
||||
}
|
||||
if (event.kind === 'thinking') {
|
||||
return <ThinkingStep key={i} content={event.content} />;
|
||||
{/* Runs of consecutive thinking + tool events collapse into a
|
||||
single ActivityGroup; text events render inline between
|
||||
them. The trailing group is "live" while streaming is
|
||||
active and not blocked on the user. */}
|
||||
{groups.map((group) => {
|
||||
if (group.kind === 'text') {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={group.id}
|
||||
className={messageStyles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{group.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
const groupIsLive = group.isTrailing && !isWaitingOnUser;
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={i}
|
||||
className={messageStyles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{event.content}
|
||||
</ReactMarkdown>
|
||||
<ActivityGroup key={group.id} items={group.items} isLive={groupIsLive} />
|
||||
);
|
||||
})}
|
||||
{/* eslint-enable react/no-array-index-key */}
|
||||
|
||||
{/* While events are still streaming, append the typing dots so the
|
||||
user has a clear "more is coming" signal. Hidden when the agent
|
||||
|
||||
@@ -5,11 +5,31 @@ import styles from './ThinkingStep.module.scss';
|
||||
|
||||
interface ThinkingStepProps {
|
||||
content: string;
|
||||
/**
|
||||
* When false, label reads "Thought for a few seconds" — intentionally
|
||||
* vague because the API doesn't persist precise timing, so showing
|
||||
* seconds would be inconsistent between fresh and reloaded threads.
|
||||
*/
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
/** Body of a thinking step — extracted so ActivityGroup can render it directly. */
|
||||
export function ThinkingContent({ content }: { content: string }): JSX.Element {
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<p className={styles.content}>{content}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function thinkingLabel(isLive: boolean): string {
|
||||
return isLive ? 'Thinking…' : 'Thought for a few seconds';
|
||||
}
|
||||
|
||||
/** Collapsible thinking row — chevron + label, content in the expanded body. */
|
||||
export default function ThinkingStep({
|
||||
content,
|
||||
isLive = false,
|
||||
}: ThinkingStepProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
@@ -23,14 +43,10 @@ export default function ThinkingStep({
|
||||
) : (
|
||||
<ChevronRight size={12} className={styles.chevron} />
|
||||
)}
|
||||
<span className={styles.label}>Thinking</span>
|
||||
<span className={styles.label}>{thinkingLabel(isLive)}</span>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className={styles.body}>
|
||||
<p className={styles.content}>{content}</p>
|
||||
</div>
|
||||
)}
|
||||
{expanded && <ThinkingContent content={content} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,24 +10,58 @@ interface ToolCallStepProps {
|
||||
toolCall: StreamingToolCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-supplied `displayText` is the human-friendly title the backend
|
||||
* wants surfaced. Falls back to a derived label
|
||||
* ("signoz_get_dashboard" → "Get Dashboard") when missing.
|
||||
*/
|
||||
export function getToolDisplayLabel(toolCall: StreamingToolCall): string {
|
||||
const { toolName, displayText } = toolCall;
|
||||
if (displayText && displayText.trim().length > 0) {
|
||||
return displayText;
|
||||
}
|
||||
return toolName
|
||||
.replace(/^[a-z]+_/, '') // strip prefix like "signoz_"
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/** Body of a tool-call step — extracted so ActivityGroup can render it directly. */
|
||||
export function ToolCallContent({
|
||||
toolCall,
|
||||
}: {
|
||||
toolCall: StreamingToolCall;
|
||||
}): JSX.Element {
|
||||
const { toolName, input, result, done } = toolCall;
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Tool</span>
|
||||
<span className={styles.toolName}>{toolName}</span>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Input</span>
|
||||
<pre className={styles.json}>{JSON.stringify(input, null, 2)}</pre>
|
||||
</div>
|
||||
{done && result !== undefined && (
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Output</span>
|
||||
<pre className={styles.json}>
|
||||
{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Collapsible tool-call row — chevron + label, in/out detail in the body. */
|
||||
export default function ToolCallStep({
|
||||
toolCall,
|
||||
}: ToolCallStepProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { toolName, input, result, done, displayText } = toolCall;
|
||||
|
||||
// Prefer the server-supplied `displayText` from `ToolCallEventDTO` —
|
||||
// it's the human-friendly title the backend wants surfaced. Fall back
|
||||
// to a derived label ("signoz_get_dashboard" → "Get Dashboard") when
|
||||
// the field is empty / null / missing.
|
||||
const label =
|
||||
displayText && displayText.trim().length > 0
|
||||
? displayText
|
||||
: toolName
|
||||
.replace(/^[a-z]+_/, '') // strip prefix like "signoz_"
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
const { done } = toolCall;
|
||||
const label = getToolDisplayLabel(toolCall);
|
||||
|
||||
const toggle = (): void => setExpanded((v) => !v);
|
||||
|
||||
@@ -44,26 +78,7 @@ export default function ToolCallStep({
|
||||
<span className={styles.label}>{label}</span>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Tool</span>
|
||||
<span className={styles.toolName}>{toolName}</span>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Input</span>
|
||||
<pre className={styles.json}>{JSON.stringify(input, null, 2)}</pre>
|
||||
</div>
|
||||
{done && result !== undefined && (
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Output</span>
|
||||
<pre className={styles.json}>
|
||||
{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{expanded && <ToolCallContent toolCall={toolCall} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
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({
|
||||
@@ -65,13 +54,20 @@ 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(
|
||||
<CreateAlertPage />,
|
||||
<AlertTypeSelectionPage />,
|
||||
{},
|
||||
{
|
||||
initialRoute: ROUTES.ALERTS_NEW,
|
||||
initialRoute: ROUTES.ALERT_TYPE_SELECTION,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,18 +15,6 @@ 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(() => ({
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,37 +1,13 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
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({
|
||||
@@ -72,14 +48,10 @@ 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';
|
||||
@@ -91,13 +63,8 @@ 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', () => {
|
||||
@@ -105,53 +72,18 @@ 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 when alertType is provided', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.METRICS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
it('should render new flow by default', () => {
|
||||
mockGetUrlQuery.mockReturnValue(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) {
|
||||
@@ -179,13 +111,8 @@ describe('CreateAlertRule', () => {
|
||||
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use alertType from URL over compositeQuery dataSource', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.LOGS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
it('should use alertType from compositeQuery dataSource when alertType is not in URL', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue({
|
||||
...initialQueriesMap.metrics,
|
||||
builder: {
|
||||
@@ -200,123 +127,14 @@ describe('CreateAlertRule', () => {
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.TRACES_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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,11 +208,3 @@ 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',
|
||||
};
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
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 { useMemo } from 'react';
|
||||
import { Form } from 'antd';
|
||||
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, ALERT_TYPE_BREADCRUMB_TITLE } from './defaults';
|
||||
import SelectAlertType from './SelectAlertType';
|
||||
|
||||
import './CreateAlertRule.styles.scss';
|
||||
import { ALERTS_VALUES_MAP } from './defaults';
|
||||
|
||||
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);
|
||||
@@ -36,15 +23,6 @@ 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;
|
||||
@@ -67,142 +45,22 @@ function CreateRules(): JSX.Element {
|
||||
[alertType, version],
|
||||
);
|
||||
|
||||
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],
|
||||
);
|
||||
// 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 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
return <CreateAlertV2 alertType={alertType} />;
|
||||
}
|
||||
|
||||
export default CreateRules;
|
||||
|
||||
@@ -9,7 +9,6 @@ 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';
|
||||
@@ -19,7 +18,6 @@ import './styles.scss';
|
||||
|
||||
function CreateAlertHeader(): JSX.Element {
|
||||
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
|
||||
const alertRuleContext = useAlertRuleOptional();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -76,13 +74,9 @@ function CreateAlertHeader(): JSX.Element {
|
||||
<Input
|
||||
type="text"
|
||||
value={alertState.name}
|
||||
onChange={(e): void => {
|
||||
const newName = e.target.value;
|
||||
setAlertState({ type: 'SET_ALERT_NAME', payload: newName });
|
||||
if (isEditMode && alertRuleContext?.setAlertRuleName) {
|
||||
alertRuleContext.setAlertRuleName(newName);
|
||||
}
|
||||
}}
|
||||
onChange={(e): void =>
|
||||
setAlertState({ type: 'SET_ALERT_NAME', payload: e.target.value })
|
||||
}
|
||||
className="alert-header__input title"
|
||||
placeholder="Enter alert rule name"
|
||||
data-testid="alert-name-input"
|
||||
|
||||
@@ -20,11 +20,6 @@ import {
|
||||
} from './utils';
|
||||
|
||||
import './styles.scss';
|
||||
import {
|
||||
invalidateGetRuleByID,
|
||||
invalidateListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
function Footer(): JSX.Element {
|
||||
const {
|
||||
@@ -120,7 +115,6 @@ function Footer(): JSX.Element {
|
||||
testAlertRule,
|
||||
]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const handleSaveAlert = useCallback((): void => {
|
||||
const payload = buildCreateThresholdAlertRulePayload({
|
||||
alertType,
|
||||
@@ -139,9 +133,6 @@ function Footer(): JSX.Element {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
void invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
void invalidateListRules(queryClient);
|
||||
|
||||
toast.success('Alert rule updated successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
|
||||
@@ -7,7 +7,6 @@ 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', () => ({
|
||||
@@ -65,12 +64,6 @@ const mockAlertContextState = createMockAlertContextState({
|
||||
},
|
||||
});
|
||||
|
||||
const WrappedFooter = (): JSX.Element => (
|
||||
<MockQueryClientProvider>
|
||||
<Footer />
|
||||
</MockQueryClientProvider>
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(createAlertState, 'useCreateAlertState')
|
||||
.mockReturnValue(mockAlertContextState);
|
||||
@@ -104,20 +97,20 @@ describe('Footer', () => {
|
||||
});
|
||||
|
||||
it('should render the component with 3 buttons', () => {
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
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(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(DISCARD_TEXT));
|
||||
expect(mockDiscardAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('save alert rule action works correctly', () => {
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockCreateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
@@ -127,13 +120,13 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isEditMode: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockUpdateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('test notification action works correctly', () => {
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(TEST_NOTIFICATION_TEXT));
|
||||
expect(mockTestAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
@@ -143,7 +136,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -159,7 +152,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
@@ -176,7 +169,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
@@ -196,7 +189,7 @@ describe('Footer', () => {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -224,7 +217,7 @@ describe('Footer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -252,7 +245,7 @@ describe('Footer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -268,7 +261,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// When testing alert rule, the play icon is replaced with a loader icon
|
||||
expect(
|
||||
@@ -283,7 +276,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// When updating alert rule, the check icon is replaced with a loader icon
|
||||
expect(
|
||||
@@ -298,7 +291,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// When creating alert rule, the check icon is replaced with a loader icon
|
||||
expect(
|
||||
|
||||
@@ -38,7 +38,6 @@ 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';
|
||||
@@ -93,6 +92,7 @@ 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,32 +160,6 @@ 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);
|
||||
@@ -706,7 +680,7 @@ function FormAlertRules({
|
||||
const renderBasicInfo = (): JSX.Element => (
|
||||
<BasicInfo
|
||||
alertDef={alertDef}
|
||||
setAlertDef={handleSetAlertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
isNewRule={isNewRule}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -111,7 +111,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERTS_NEW, {
|
||||
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -22,9 +22,3 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Divider } from 'antd';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Divider, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { TagContainer, TagLabel, TagValue } from './FieldRenderer.styles';
|
||||
@@ -8,10 +7,6 @@ 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);
|
||||
|
||||
@@ -19,16 +14,11 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
|
||||
<span className="field-renderer-container">
|
||||
{dataType && newField && logType ? (
|
||||
<>
|
||||
<TooltipSimple
|
||||
title={newField}
|
||||
side="left"
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
arrow
|
||||
>
|
||||
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
|
||||
<Typography.Text truncate={1} className="label">
|
||||
{newField}{' '}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<div className="tags">
|
||||
<TagContainer>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import TableView from './TableView';
|
||||
import { getBodyDisplayString, removeEscapeCharacters } from './utils';
|
||||
import { removeEscapeCharacters } from './utils';
|
||||
|
||||
import './Overview.styles.scss';
|
||||
|
||||
@@ -112,7 +112,7 @@ function Overview({
|
||||
children: (
|
||||
<div className="logs-body-content">
|
||||
<MEditor
|
||||
value={removeEscapeCharacters(getBodyDisplayString(logData.body))}
|
||||
value={removeEscapeCharacters(logData.body)}
|
||||
language="json"
|
||||
options={options}
|
||||
onChange={(): void => {}}
|
||||
|
||||
@@ -106,20 +106,10 @@ function TableView({
|
||||
isListViewPanel,
|
||||
]);
|
||||
|
||||
// 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 flattenLogData: Record<string, string> | null = useMemo(
|
||||
() => (logData ? flattenObject(logData) : null),
|
||||
[logData],
|
||||
);
|
||||
|
||||
const handleClick = (
|
||||
operator: string,
|
||||
|
||||
@@ -10,7 +10,7 @@ const MAX_BODY_BYTES = 100 * 1024; // 100 KB
|
||||
|
||||
// Hook for async JSON processing
|
||||
const useAsyncJSONProcessing = (
|
||||
value: string | Record<string, unknown>,
|
||||
value: string,
|
||||
shouldProcess: boolean,
|
||||
handleChangeSelectedView?: ChangeViewFunctionType,
|
||||
): {
|
||||
@@ -40,17 +40,11 @@ const useAsyncJSONProcessing = (
|
||||
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);
|
||||
};
|
||||
// Avoid processing if the json is too large
|
||||
const byteSize = new Blob([value]).size;
|
||||
if (byteSize > MAX_BODY_BYTES) {
|
||||
return (): void => {};
|
||||
}
|
||||
|
||||
processingRef.current = true;
|
||||
setJsonState({ isLoading: true, treeData: null, error: null });
|
||||
@@ -59,8 +53,8 @@ const useAsyncJSONProcessing = (
|
||||
const processAsync = (): void => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const parsedBody = parseBody();
|
||||
if (parsedBody && !isEmpty(parsedBody)) {
|
||||
const parsedBody = recursiveParseJSON(value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
const treeData = jsonToDataNodes(parsedBody, {
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
@@ -88,8 +82,8 @@ const useAsyncJSONProcessing = (
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
(): void => {
|
||||
try {
|
||||
const parsedBody = parseBody();
|
||||
if (parsedBody && !isEmpty(parsedBody)) {
|
||||
const parsedBody = recursiveParseJSON(value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
const treeData = jsonToDataNodes(parsedBody, {
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
|
||||
@@ -4,11 +4,7 @@ import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import dompurify from 'dompurify';
|
||||
import { uniqueId } from 'lodash-es';
|
||||
import {
|
||||
ILog,
|
||||
ILogAggregateAttributesResources,
|
||||
ILogBody,
|
||||
} from 'types/api/logs/log';
|
||||
import { ILog, ILogAggregateAttributesResources } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { FORBID_DOM_PURIFY_ATTR, FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||
|
||||
@@ -437,8 +433,3 @@ 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);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Spin } from 'antd';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Button, Spin, Tooltip } from 'antd';
|
||||
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';
|
||||
@@ -14,10 +11,6 @@ import {
|
||||
formatTimestampToReadableDate,
|
||||
} from './utils';
|
||||
|
||||
const TOOLTIP_CONTENT_PROPS = {
|
||||
className: 'metric-highlights-tooltip-content',
|
||||
};
|
||||
|
||||
function Highlights({ metricName }: HighlightsProps): JSX.Element {
|
||||
const {
|
||||
data: metricHighlightsData,
|
||||
@@ -46,13 +39,6 @@ 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 (
|
||||
@@ -104,42 +90,27 @@ function Highlights({ metricName }: HighlightsProps): JSX.Element {
|
||||
className="metric-details-grid-value"
|
||||
data-testid="metric-highlights-data-points"
|
||||
>
|
||||
<TooltipSimple
|
||||
title={metricHighlights?.dataPoints?.toLocaleString()}
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
arrow
|
||||
>
|
||||
<span>
|
||||
{formatNumberIntoHumanReadableFormat(
|
||||
metricHighlights?.dataPoints ?? 0,
|
||||
)}
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
<Tooltip title={metricHighlights?.dataPoints?.toLocaleString()}>
|
||||
{formatNumberIntoHumanReadableFormat(metricHighlights?.dataPoints ?? 0)}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className="metric-details-grid-value"
|
||||
data-testid="metric-highlights-time-series-total"
|
||||
>
|
||||
<TooltipSimple
|
||||
title="Active time series are those that have received data points in the last 1 hour."
|
||||
side="top"
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
arrow
|
||||
<Tooltip
|
||||
title="Active time series are those that have received data points in the last 1
|
||||
hour."
|
||||
placement="top"
|
||||
>
|
||||
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className="metric-details-grid-value"
|
||||
data-testid="metric-highlights-last-received"
|
||||
>
|
||||
<TooltipSimple
|
||||
title={lastReceivedTooltipText}
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
arrow
|
||||
>
|
||||
<span>{lastReceivedText}</span>
|
||||
</TooltipSimple>
|
||||
<Tooltip title={lastReceivedText}>{lastReceivedText}</Tooltip>
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -510,12 +510,6 @@
|
||||
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;
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
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',
|
||||
@@ -28,7 +16,7 @@ describe('Highlights', () => {
|
||||
});
|
||||
|
||||
it('should render all highlights data correctly', () => {
|
||||
renderHighlights(MOCK_METRIC_NAME);
|
||||
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
const dataPoints = screen.getByTestId('metric-highlights-data-points');
|
||||
const timeSeriesTotal = screen.getByTestId(
|
||||
@@ -53,7 +41,7 @@ describe('Highlights', () => {
|
||||
),
|
||||
);
|
||||
|
||||
renderHighlights(MOCK_METRIC_NAME);
|
||||
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('metric-highlights-error-state'),
|
||||
@@ -70,7 +58,7 @@ describe('Highlights', () => {
|
||||
),
|
||||
);
|
||||
|
||||
renderHighlights(MOCK_METRIC_NAME);
|
||||
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
expect(screen.getByText('SAMPLES')).toBeInTheDocument();
|
||||
expect(screen.getByText('TIME SERIES')).toBeInTheDocument();
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
.new-explorer-cta-with-badge {
|
||||
display: inline-flex;
|
||||
.new-explorer-cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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',
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Badge, Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Undo } from '@signozhq/icons';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { buttonText } from './config';
|
||||
import { buttonText, RIBBON_STYLES } from './config';
|
||||
|
||||
import './NewExplorerCTA.styles.scss';
|
||||
|
||||
@@ -71,12 +70,9 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="new-explorer-cta-with-badge">
|
||||
<Badge.Ribbon style={RIBBON_STYLES} text="New">
|
||||
{button}
|
||||
<Badge color="robin" variant="default">
|
||||
New
|
||||
</Badge>
|
||||
</span>
|
||||
</Badge.Ribbon>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -27,9 +26,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
|
||||
DATE_TIME_FORMATS.UTC_MONTH_SHORT,
|
||||
)}
|
||||
</div>
|
||||
<div className="logs-preview-list-item-body">
|
||||
{getBodyDisplayString(log.body)}
|
||||
</div>
|
||||
<div className="logs-preview-list-item-body">{log.body}</div>
|
||||
<div
|
||||
className="logs-preview-list-item-expand"
|
||||
onClick={makeLogDetailsHandler(log)}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useListDowntimeSchedules,
|
||||
} from 'api/generated/services/downtimeschedules';
|
||||
import { useListRules } from 'api/generated/services/rules';
|
||||
import type { AlertmanagertypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RuletypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -48,9 +48,7 @@ export function PlannedDowntime(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [initialValues, setInitialValues] =
|
||||
useState<Partial<AlertmanagertypesPlannedMaintenanceDTO>>(
|
||||
defaultInitialValues,
|
||||
);
|
||||
useState<Partial<RuletypesPlannedMaintenanceDTO>>(defaultInitialValues);
|
||||
|
||||
const downtimeSchedules = useListDowntimeSchedules();
|
||||
const alertOptions = React.useMemo(
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
updateDowntimeScheduleByID,
|
||||
} from 'api/generated/services/downtimeschedules';
|
||||
import type {
|
||||
AlertmanagertypesPlannedMaintenanceDTO,
|
||||
AlertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
AlertmanagertypesRecurrenceDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesPostablePlannedMaintenanceDTO,
|
||||
RuletypesRecurrenceDTO,
|
||||
} 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?: AlertmanagertypesRecurrenceDTO;
|
||||
recurrence?: RuletypesRecurrenceDTO;
|
||||
alertRules: DefaultOptionType[];
|
||||
recurrenceSelect?: AlertmanagertypesRecurrenceDTO;
|
||||
recurrenceSelect?: RuletypesRecurrenceDTO;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
|
||||
|
||||
interface PlannedDowntimeFormProps {
|
||||
initialValues: Partial<AlertmanagertypesPlannedMaintenanceDTO>;
|
||||
initialValues: Partial<RuletypesPlannedMaintenanceDTO>;
|
||||
alertOptions: DefaultOptionType[];
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
@@ -139,7 +139,7 @@ export function PlannedDowntimeForm(
|
||||
|
||||
const saveHandler = useCallback(
|
||||
async (values: PlannedDowntimeFormData) => {
|
||||
const data: AlertmanagertypesPostablePlannedMaintenanceDTO = {
|
||||
const data: RuletypesPostablePlannedMaintenanceDTO = {
|
||||
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 AlertmanagertypesRecurrenceDTO,
|
||||
} as RuletypesRecurrenceDTO,
|
||||
timezone: schedule?.timezone as string,
|
||||
};
|
||||
}, [initialValues, alertOptions]);
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { DefaultOptionType } from 'antd/es/select';
|
||||
import type {
|
||||
ListDowntimeSchedules200,
|
||||
RenderErrorResponseDTO,
|
||||
AlertmanagertypesPlannedMaintenanceDTO,
|
||||
AlertmanagertypesScheduleDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesScheduleDTO,
|
||||
} 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?: AlertmanagertypesScheduleDTO;
|
||||
schedule?: RuletypesScheduleDTO;
|
||||
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<AlertmanagertypesPlannedMaintenanceDTO>>
|
||||
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
|
||||
>;
|
||||
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteDowntime: (id: string, name: string) => void;
|
||||
@@ -281,10 +281,9 @@ export function CustomCollapseList(
|
||||
);
|
||||
}
|
||||
|
||||
export type DowntimeSchedulesTableData =
|
||||
AlertmanagertypesPlannedMaintenanceDTO & {
|
||||
alertOptions: DefaultOptionType[];
|
||||
};
|
||||
export type DowntimeSchedulesTableData = RuletypesPlannedMaintenanceDTO & {
|
||||
alertOptions: DefaultOptionType[];
|
||||
};
|
||||
|
||||
export function PlannedDowntimeList({
|
||||
downtimeSchedules,
|
||||
@@ -301,7 +300,7 @@ export function PlannedDowntimeList({
|
||||
>;
|
||||
alertOptions: DefaultOptionType[];
|
||||
setInitialValues: React.Dispatch<
|
||||
React.SetStateAction<Partial<AlertmanagertypesPlannedMaintenanceDTO>>
|
||||
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
|
||||
>;
|
||||
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteDowntime: (id: string, name: string) => void;
|
||||
|
||||
@@ -5,8 +5,8 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import type {
|
||||
DeleteDowntimeScheduleByIDPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
AlertmanagertypesPlannedMaintenanceDTO,
|
||||
AlertmanagertypesRecurrenceDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesRecurrenceDTO,
|
||||
} 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?: AlertmanagertypesRecurrenceDTO | null,
|
||||
recurrence?: RuletypesRecurrenceDTO | null,
|
||||
timezone?: string,
|
||||
): string => {
|
||||
if (!recurrence) {
|
||||
@@ -87,20 +87,19 @@ export const recurrenceInfo = (
|
||||
return `Repeats - ${repeatType} ${weeklyRepeatString} from ${formattedStartTime} ${formattedEndTime} ${durationString}`;
|
||||
};
|
||||
|
||||
export const defaultInitialValues: Partial<AlertmanagertypesPlannedMaintenanceDTO> =
|
||||
{
|
||||
name: '',
|
||||
description: '',
|
||||
schedule: {
|
||||
timezone: '',
|
||||
endTime: undefined,
|
||||
recurrence: undefined,
|
||||
startTime: undefined,
|
||||
},
|
||||
alertIds: [],
|
||||
createdAt: undefined,
|
||||
createdBy: undefined,
|
||||
};
|
||||
export const defaultInitialValues: Partial<RuletypesPlannedMaintenanceDTO> = {
|
||||
name: '',
|
||||
description: '',
|
||||
schedule: {
|
||||
timezone: '',
|
||||
endTime: undefined,
|
||||
recurrence: undefined,
|
||||
startTime: undefined,
|
||||
},
|
||||
alertIds: [],
|
||||
createdAt: undefined,
|
||||
createdBy: undefined,
|
||||
};
|
||||
|
||||
type DeleteDowntimeScheduleProps = {
|
||||
deleteDowntimeScheduleAsync: UseMutateAsyncFunction<
|
||||
@@ -216,5 +215,5 @@ export const recurrenceOptionWithSubmenu: Option[] = [
|
||||
];
|
||||
|
||||
export const isScheduleRecurring = (
|
||||
schedule?: AlertmanagertypesPlannedMaintenanceDTO['schedule'] | null,
|
||||
schedule?: RuletypesPlannedMaintenanceDTO['schedule'] | null,
|
||||
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type {
|
||||
AlertmanagertypesScheduleDTO,
|
||||
AlertmanagertypesPlannedMaintenanceDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesScheduleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
AlertmanagertypesMaintenanceKindDTO,
|
||||
AlertmanagertypesMaintenanceStatusDTO,
|
||||
RuletypesMaintenanceKindDTO,
|
||||
RuletypesMaintenanceStatusDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export const buildSchedule = (
|
||||
schedule: Partial<AlertmanagertypesScheduleDTO>,
|
||||
): AlertmanagertypesScheduleDTO => ({
|
||||
schedule: Partial<RuletypesScheduleDTO>,
|
||||
): RuletypesScheduleDTO => ({
|
||||
timezone: schedule?.timezone ?? '',
|
||||
startTime: schedule?.startTime,
|
||||
endTime: schedule?.endTime,
|
||||
@@ -17,8 +17,8 @@ export const buildSchedule = (
|
||||
});
|
||||
|
||||
export const createMockDowntime = (
|
||||
overrides: Partial<AlertmanagertypesPlannedMaintenanceDTO>,
|
||||
): AlertmanagertypesPlannedMaintenanceDTO => ({
|
||||
overrides: Partial<RuletypesPlannedMaintenanceDTO>,
|
||||
): RuletypesPlannedMaintenanceDTO => ({
|
||||
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 ?? AlertmanagertypesMaintenanceKindDTO.recurring,
|
||||
status: overrides.status ?? AlertmanagertypesMaintenanceStatusDTO.active,
|
||||
kind: overrides.kind ?? RuletypesMaintenanceKindDTO.recurring,
|
||||
status: overrides.status ?? RuletypesMaintenanceStatusDTO.active,
|
||||
});
|
||||
|
||||
@@ -33,12 +33,14 @@ function TopNav(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const isAlertCreationPage = useMemo(
|
||||
() => matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }),
|
||||
[location.pathname],
|
||||
const isNewAlertsLandingPage = useMemo(
|
||||
() =>
|
||||
matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }) &&
|
||||
!location.search,
|
||||
[location.pathname, location.search],
|
||||
);
|
||||
|
||||
if (isSignUpPage || isDisabled || isRouteToSkip || isAlertCreationPage) {
|
||||
if (isSignUpPage || isDisabled || isRouteToSkip || isNewAlertsLandingPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
margin-top: 10px;
|
||||
.divider {
|
||||
border-color: var(--l1-border);
|
||||
margin: 16px 0;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Divider } from 'antd';
|
||||
import { Breadcrumb, Button, 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';
|
||||
@@ -11,12 +10,13 @@ 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,11 +24,42 @@ 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();
|
||||
@@ -38,7 +69,7 @@ function AlertDetails(): JSX.Element {
|
||||
}, [params]);
|
||||
|
||||
const getDocumentTitle = useMemo(() => {
|
||||
const alertTitle = alertRuleName ?? alertDetailsResponse?.data?.alert;
|
||||
const alertTitle = alertDetailsResponse?.data?.alert;
|
||||
if (alertTitle) {
|
||||
return alertTitle;
|
||||
}
|
||||
@@ -49,7 +80,7 @@ function AlertDetails(): JSX.Element {
|
||||
return document.title;
|
||||
}
|
||||
return 'Alert Not Found';
|
||||
}, [alertRuleName, alertDetailsResponse?.data?.alert, isTestAlert, isLoading]);
|
||||
}, [alertDetailsResponse?.data?.alert, isTestAlert, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = getDocumentTitle;
|
||||
@@ -95,13 +126,20 @@ function AlertDetails(): JSX.Element {
|
||||
<div
|
||||
className={classNames('alert-details', { 'alert-details-v2': isV2Alert })}
|
||||
>
|
||||
<AlertBreadcrumb
|
||||
<Breadcrumb
|
||||
className="alert-details__breadcrumb"
|
||||
items={[
|
||||
{ title: 'Alert Rules', route: ROUTES.LIST_ALL_ALERT },
|
||||
{ title: ruleId, isLast: true },
|
||||
{
|
||||
title: (
|
||||
<BreadCrumbItem title="Alert Rules" route={ROUTES.LIST_ALL_ALERT} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <BreadCrumbItem title={ruleId} isLast />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Divider className="divider breadcrumb-divider" />
|
||||
|
||||
{alertRuleDetails && <AlertHeader alertDetails={alertRuleDetails} />}
|
||||
<Divider className="divider" />
|
||||
|
||||
@@ -33,12 +33,13 @@ const menuItemStyleV2: CSSProperties = {
|
||||
function AlertActionButtons({
|
||||
ruleId,
|
||||
alertDetails,
|
||||
setUpdatedName,
|
||||
}: {
|
||||
ruleId: string;
|
||||
alertDetails: AlertHeaderProps['alertDetails'];
|
||||
setUpdatedName: (name: string) => void;
|
||||
}): JSX.Element {
|
||||
const { alertRuleState, setAlertRuleState, alertRuleName, setAlertRuleName } =
|
||||
useAlertRule();
|
||||
const { alertRuleState, setAlertRuleState } = useAlertRule();
|
||||
const [intermediateName, setIntermediateName] = useState<string>(
|
||||
alertDetails.alert,
|
||||
);
|
||||
@@ -52,7 +53,7 @@ function AlertActionButtons({
|
||||
const { handleAlertDelete } = useAlertRuleDelete({ ruleId });
|
||||
const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({
|
||||
alertDetails: alertDetails as unknown as AlertDef,
|
||||
setAlertRuleName,
|
||||
setUpdatedName,
|
||||
intermediateName,
|
||||
});
|
||||
|
||||
@@ -112,12 +113,6 @@ 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), []);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useMemo, useState } 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,17 +20,8 @@ export type AlertHeaderProps = {
|
||||
};
|
||||
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
const { state, alert: alertName, labels } = alertDetails;
|
||||
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 { alertRuleState } = useAlertRule();
|
||||
const [updatedName, setUpdatedName] = useState(alertName);
|
||||
|
||||
const labelsWithoutSeverity = useMemo(() => {
|
||||
if (labels) {
|
||||
@@ -49,7 +40,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
<div className="alert-title-wrapper">
|
||||
<AlertState state={alertRuleState ?? state ?? ''} />
|
||||
<div className="alert-title">
|
||||
<LineClampedText text={displayName || ''} />
|
||||
<LineClampedText text={updatedName || alertName} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,6 +64,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
<AlertActionButtons
|
||||
alertDetails={alertDetails}
|
||||
ruleId={alertDetails?.id || ''}
|
||||
setUpdatedName={setUpdatedName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,7 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
createRule,
|
||||
deleteRuleByID,
|
||||
getGetRuleByIDQueryKey,
|
||||
invalidateGetRuleByID,
|
||||
invalidateListRules,
|
||||
updateRuleByID,
|
||||
useGetRuleByID,
|
||||
useListRules,
|
||||
@@ -492,11 +490,11 @@ export const useAlertRuleDuplicate = ({
|
||||
};
|
||||
export const useAlertRuleUpdate = ({
|
||||
alertDetails,
|
||||
setAlertRuleName,
|
||||
setUpdatedName,
|
||||
intermediateName,
|
||||
}: {
|
||||
alertDetails: AlertDef;
|
||||
setAlertRuleName: (name: string | undefined) => void;
|
||||
setUpdatedName: (name: string) => void;
|
||||
intermediateName: string;
|
||||
}): {
|
||||
handleAlertUpdate: () => void;
|
||||
@@ -504,29 +502,17 @@ 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: () => 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' });
|
||||
},
|
||||
onMutate: () => setUpdatedName(intermediateName),
|
||||
onSuccess: () =>
|
||||
notifications.success({ message: 'Alert renamed successfully' }),
|
||||
onError: (error) => {
|
||||
setAlertRuleName(alertDetails.alert);
|
||||
setUpdatedName(alertDetails.alert);
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
@@ -565,6 +551,7 @@ 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,
|
||||
|
||||
@@ -1,44 +1,15 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-tabs-nav {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.configuration-tabs {
|
||||
margin-top: -16px;
|
||||
|
||||
.ant-tabs-nav {
|
||||
.ant-tabs-nav-wrap {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-rules-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
56
frontend/src/pages/AlertTypeSelection/AlertTypeSelection.tsx
Normal file
56
frontend/src/pages/AlertTypeSelection/AlertTypeSelection.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
@@ -0,0 +1,189 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
3
frontend/src/pages/AlertTypeSelection/index.tsx
Normal file
3
frontend/src/pages/AlertTypeSelection/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import AlertTypeSelectionPage from './AlertTypeSelection';
|
||||
|
||||
export default AlertTypeSelectionPage;
|
||||
@@ -526,7 +526,7 @@ function SpanDetailsPanel({
|
||||
|
||||
const PANEL_WIDTH = 500;
|
||||
const PANEL_MARGIN_RIGHT = 20;
|
||||
const PANEL_MARGIN_TOP = 50;
|
||||
const PANEL_MARGIN_TOP = 25;
|
||||
const PANEL_MARGIN_BOTTOM = 25;
|
||||
|
||||
const content = (
|
||||
|
||||
@@ -580,9 +580,10 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Backend mode: trigger refetch via interestedSpanId
|
||||
// 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.
|
||||
setInterestedSpanId({
|
||||
spanId,
|
||||
isUncollapsed: !collapse,
|
||||
@@ -781,26 +782,19 @@ 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 (isFullDataLoaded || interestedSpanId.spanId === '') {
|
||||
return;
|
||||
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]);
|
||||
}
|
||||
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,
|
||||
]);
|
||||
}, [interestedSpanId, setSelectedSpan, spans, scrollSpanIntoView]);
|
||||
|
||||
// Covers URL-driven navigation to an already-loaded span (flamegraph /
|
||||
// filter / browser back) that the interestedSpanId-keyed effect doesn't see.
|
||||
|
||||
@@ -199,12 +199,10 @@ const mockSpans = [
|
||||
createMockSpan('span-3', 1),
|
||||
];
|
||||
|
||||
// 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.
|
||||
// Shared TestComponent for all tests
|
||||
function TestComponent(): JSX.Element {
|
||||
const [selectedSpan, setSelectedSpan] = React.useState<SpanV3 | undefined>(
|
||||
mockSpans[0],
|
||||
undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -75,7 +75,6 @@ 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.
|
||||
@@ -176,36 +175,12 @@ function TraceDetailsV3(): JSX.Element {
|
||||
}
|
||||
}, [traceData, isFullDataLoaded]);
|
||||
|
||||
// Tracks whether we've already done the initial URL→selectedSpan handoff
|
||||
//Lets `interestedSpanId` stay purely as the refetch trigger in frontend mode.
|
||||
// Frontend mode: auto-expand ancestors of the selected span so it becomes visible
|
||||
useEffect(() => {
|
||||
if (deepLinkResolvedRef.current) {
|
||||
if (!isFullDataLoaded || !interestedSpanId.spanId || allSpans.length === 0) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
const ancestors = getAncestorSpanIds(allSpans, interestedSpanId.spanId);
|
||||
if (ancestors.size === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -228,7 +203,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [isFullDataLoaded, selectedSpanId, allSpans]);
|
||||
}, [isFullDataLoaded, interestedSpanId.spanId, allSpans]);
|
||||
|
||||
const [activeKeys, setActiveKeys] = useState<string[]>(['flame', 'waterfall']);
|
||||
|
||||
@@ -242,7 +217,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
() =>
|
||||
(getLocalStorageKey(
|
||||
LOCALSTORAGE.TRACE_DETAILS_SPAN_DETAILS_POSITION,
|
||||
) as SpanDetailVariant) || SpanDetailVariant.DIALOG,
|
||||
) as SpanDetailVariant) || SpanDetailVariant.DOCKED,
|
||||
);
|
||||
|
||||
const handleVariantChange = useCallback(
|
||||
|
||||
@@ -9,8 +9,6 @@ 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>(
|
||||
@@ -25,18 +23,13 @@ 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, alertRuleName],
|
||||
[alertRuleState],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -54,7 +47,4 @@ export const useAlertRule = (): AlertRuleContextType => {
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useAlertRuleOptional = (): AlertRuleContextType | undefined =>
|
||||
useContext(AlertRuleContext);
|
||||
|
||||
export default AlertRuleProvider;
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
export interface ILogBody {
|
||||
message?: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ILog {
|
||||
date: string;
|
||||
timestamp: number | string;
|
||||
@@ -13,7 +8,7 @@ export interface ILog {
|
||||
traceFlags: number;
|
||||
severityText: string;
|
||||
severityNumber: number;
|
||||
body: string | ILogBody;
|
||||
body: string;
|
||||
resources_string: Record<string, never>;
|
||||
scope_string: Record<string, never>;
|
||||
attributesString: Record<string, never>;
|
||||
|
||||
@@ -132,6 +132,7 @@ 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
2
go.mod
@@ -11,7 +11,6 @@ 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
|
||||
@@ -113,6 +112,7 @@ 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
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -28,10 +28,12 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
)
|
||||
|
||||
// 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"
|
||||
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"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
// logger is the logger for the alertmanager
|
||||
@@ -61,25 +63,15 @@ type Server struct {
|
||||
silencer *silence.Silencer
|
||||
silences *silence.Silences
|
||||
timeIntervals map[string][]timeinterval.TimeInterval
|
||||
pipelineBuilder *pipelineBuilder
|
||||
muter *MaintenanceMuter
|
||||
marker *types.MemMarker
|
||||
pipelineBuilder *notify.PipelineBuilder
|
||||
marker *alertmanagertypes.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,
|
||||
maintenanceStore alertmanagertypes.MaintenanceStore,
|
||||
) (*Server, error) {
|
||||
func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registerer, srvConfig Config, orgID string, stateStore alertmanagertypes.StateStore, nfManager nfmanager.NotificationManager) (*Server, error) {
|
||||
server := &Server{
|
||||
logger: logger.With(slog.String("pkg", "go.signoz.io/pkg/alertmanager/alertmanagerserver")),
|
||||
registry: registry,
|
||||
@@ -92,7 +84,7 @@ func New(
|
||||
signozRegisterer := prometheus.WrapRegistererWithPrefix("signoz_", registry)
|
||||
signozRegisterer = prometheus.WrapRegistererWith(prometheus.Labels{"org_id": server.orgID}, signozRegisterer)
|
||||
// initialize marker
|
||||
server.marker = types.NewMarker(signozRegisterer)
|
||||
server.marker = alertmanagertypes.NewMarker(signozRegisterer)
|
||||
|
||||
// get silences for initial state
|
||||
state, err := server.stateStore.Get(ctx, server.orgID)
|
||||
@@ -168,6 +160,7 @@ func New(
|
||||
|
||||
return c, server.stateStore.Set(ctx, storableSilences)
|
||||
})
|
||||
|
||||
}()
|
||||
|
||||
// Start maintenance for notification logs
|
||||
@@ -203,25 +196,17 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server.muter = NewMaintenanceMuter(maintenanceStore, orgID, server.logger)
|
||||
server.pipelineBuilder = newPipelineBuilder(signozRegisterer, featurecontrol.NoopFlags{})
|
||||
server.pipelineBuilder = notify.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)
|
||||
},
|
||||
func(labels model.LabelSet) []string {
|
||||
return server.muter.MutedBy(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)
|
||||
}, params)
|
||||
}
|
||||
|
||||
func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanagertypes.PostableAlerts) error {
|
||||
@@ -305,7 +290,6 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
||||
server.silencer,
|
||||
intervener,
|
||||
server.marker,
|
||||
server.muter,
|
||||
server.nflog,
|
||||
pipelinePeer,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/common/model"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -87,25 +86,11 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
|
||||
err = notificationManager.SetNotificationConfig(orgID, "high-cpu-usage", ¬ifConfig)
|
||||
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, maintenanceStore)
|
||||
server, err := New(context.Background(), logger, registry, srvCfg, orgID, stateStore, notificationManager)
|
||||
require.NoError(t, err)
|
||||
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, orgID)
|
||||
require.NoError(t, err)
|
||||
@@ -166,16 +151,6 @@ 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)
|
||||
@@ -191,12 +166,10 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
alerts, err := server.GetAlerts(context.Background(), params)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, alerts, 4, "Expected 4 active alerts")
|
||||
require.Len(t, alerts, 3, "Expected 3 active alerts")
|
||||
|
||||
for _, alert := range alerts {
|
||||
if alert.Labels["ruleId"] != "high-cpu-usage" {
|
||||
continue
|
||||
}
|
||||
require.Equal(t, "high-cpu-usage", alert.Labels["ruleId"])
|
||||
require.NotEmpty(t, alert.Labels["severity"])
|
||||
require.Contains(t, []string{"critical", "warning"}, alert.Labels["severity"])
|
||||
require.Equal(t, "prod-cluster", alert.Labels["cluster"])
|
||||
@@ -248,20 +221,4 @@ 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,12 +10,7 @@ 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"
|
||||
@@ -28,14 +23,9 @@ 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, newTestMaintenanceStore())
|
||||
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
|
||||
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")
|
||||
@@ -47,7 +37,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, newTestMaintenanceStore())
|
||||
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
|
||||
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")
|
||||
@@ -95,7 +85,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, newTestMaintenanceStore())
|
||||
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
|
||||
require.NoError(t, err)
|
||||
|
||||
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
|
||||
@@ -143,7 +133,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, newTestMaintenanceStore())
|
||||
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
|
||||
require.NoError(t, err)
|
||||
|
||||
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
|
||||
@@ -248,7 +238,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, newTestMaintenanceStore())
|
||||
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
|
||||
require.NoError(t, err)
|
||||
|
||||
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
|
||||
|
||||
@@ -6,7 +6,6 @@ package alertmanagertest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -1846,628 +1845,3 @@ 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
|
||||
}
|
||||
|
||||
@@ -39,18 +39,16 @@ 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,
|
||||
@@ -61,7 +59,6 @@ func New(
|
||||
servers: make(map[string]*alertmanagerserver.Server),
|
||||
serversMtx: sync.RWMutex{},
|
||||
notificationManager: nfManager,
|
||||
maintenanceStore: maintenanceStore,
|
||||
}
|
||||
|
||||
return service
|
||||
@@ -180,10 +177,7 @@ 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, service.maintenanceStore,
|
||||
)
|
||||
server, err := alertmanagerserver.New(ctx, service.settings.Logger(), service.settings.PrometheusRegisterer(), service.config, orgID, service.stateStore, service.notificationManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
amConfig "github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/common/model"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
|
||||
amConfig "github.com/prometheus/alertmanager/config"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
|
||||
@@ -17,7 +20,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -28,49 +30,35 @@ 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,
|
||||
maintenanceStore alertmanagertypes.MaintenanceStore,
|
||||
) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
||||
func NewFactory(sqlstore sqlstore.SQLStore, orgGetter organization.Getter, notificationManager nfmanager.NotificationManager) 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(settings, config, sqlstore, orgGetter, notificationManager, maintenanceStore)
|
||||
return New(ctx, settings, config, sqlstore, orgGetter, notificationManager)
|
||||
})
|
||||
}
|
||||
|
||||
func New(
|
||||
providerSettings factory.ProviderSettings,
|
||||
config alertmanager.Config,
|
||||
sqlstore sqlstore.SQLStore,
|
||||
orgGetter organization.Getter,
|
||||
notificationManager nfmanager.NotificationManager,
|
||||
maintenanceStore alertmanagertypes.MaintenanceStore,
|
||||
) (*provider, error) {
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore, orgGetter organization.Getter, notificationManager nfmanager.NotificationManager) (*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{}),
|
||||
}
|
||||
|
||||
@@ -125,7 +113,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[ruletypes.LabelRuleID], set)
|
||||
match, err := provider.notificationManager.Match(ctx, orgID, alert.Labels[labels.AlertRuleIdLabel], set)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ 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"
|
||||
)
|
||||
@@ -121,8 +120,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(alertmanagertypes.ListPlannedMaintenanceParams),
|
||||
Response: make([]*alertmanagertypes.PlannedMaintenance, 0),
|
||||
RequestQuery: new(ruletypes.ListPlannedMaintenanceParams),
|
||||
Response: make([]*ruletypes.PlannedMaintenance, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
@@ -135,7 +134,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(alertmanagertypes.PlannedMaintenance),
|
||||
Response: new(ruletypes.PlannedMaintenance),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
@@ -149,9 +148,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(alertmanagertypes.PostablePlannedMaintenance),
|
||||
Request: new(ruletypes.PostablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(alertmanagertypes.PlannedMaintenance),
|
||||
Response: new(ruletypes.PlannedMaintenance),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
@@ -165,7 +164,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(alertmanagertypes.PostablePlannedMaintenance),
|
||||
Request: new(ruletypes.PostablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
|
||||
@@ -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, dashboardtypes.SourceUser, postableDashboard)
|
||||
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,16 +72,7 @@ func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboard
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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
|
||||
return dashboardtypes.NewDashboardsFromStorableDashboards(storableDashboards), nil
|
||||
}
|
||||
|
||||
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatableDashboard dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) {
|
||||
@@ -90,10 +81,6 @@ 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
|
||||
@@ -118,10 +105,6 @@ 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
|
||||
@@ -145,10 +128,6 @@ 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")
|
||||
}
|
||||
|
||||
@@ -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.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -314,12 +314,19 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeConditionCounts, err := m.getPerGroupNodeConditionCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
nodeConditionCounts, err := m.getPerGroupNodeConditionCounts(ctx, req, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
podPhaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -402,7 +409,14 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -484,14 +498,27 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
podPhaseCountsMap, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -662,7 +689,14 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
return nil, err
|
||||
}
|
||||
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -750,9 +784,16 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -840,9 +881,16 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -930,9 +978,16 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -170,29 +170,27 @@ 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,
|
||||
start, end int64,
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
req *inframonitoringtypes.PostableNodes,
|
||||
pageGroups []map[string]string,
|
||||
) (map[string]nodeConditionCounts, error) {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
if len(pageGroups) == 0 || len(req.GroupBy) == 0 {
|
||||
return map[string]nodeConditionCounts{}, nil
|
||||
}
|
||||
|
||||
// Merge user filter with page-groups IN clauses.
|
||||
userFilterExpr := ""
|
||||
if filter != nil {
|
||||
userFilterExpr = filter.Expression
|
||||
// Merged filter expression (user filter + page-groups IN clauses).
|
||||
reqFilterExpr := ""
|
||||
if req.Filter != nil {
|
||||
reqFilterExpr = req.Filter.Expression
|
||||
}
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
|
||||
filterExpr := mergeFilterExpressions(reqFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
// Resolve tables. Same convention as pods.
|
||||
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(start), uint64(end), nil,
|
||||
uint64(req.Start), uint64(req.End), nil,
|
||||
)
|
||||
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
|
||||
uint64(start), uint64(end),
|
||||
uint64(req.Start), uint64(req.End),
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
|
||||
)
|
||||
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
|
||||
@@ -203,7 +201,7 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
"fingerprint",
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(nodeNameAttrKey)),
|
||||
}
|
||||
for _, key := range groupBy {
|
||||
for _, key := range req.GroupBy {
|
||||
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", timeSeriesFPs.Var(key.Name), quoteIdentifier(key.Name)),
|
||||
)
|
||||
@@ -215,8 +213,8 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
timeSeriesFPs.GE("unix_milli", adjustedStart),
|
||||
timeSeriesFPs.L("unix_milli", adjustedEnd),
|
||||
)
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if filterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -225,7 +223,7 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
}
|
||||
}
|
||||
timeSeriesFPsGroupBy := []string{"fingerprint", "node_name"}
|
||||
for _, key := range groupBy {
|
||||
for _, key := range req.GroupBy {
|
||||
timeSeriesFPsGroupBy = append(timeSeriesFPsGroupBy, quoteIdentifier(key.Name))
|
||||
}
|
||||
timeSeriesFPs.GroupBy(timeSeriesFPsGroupBy...)
|
||||
@@ -235,7 +233,7 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
latestConditionPerNode := sqlbuilder.NewSelectBuilder()
|
||||
latestConditionPerNodeSelectCols := []string{"tsfp.node_name AS node_name"}
|
||||
latestConditionPerNodeGroupBy := []string{"node_name"}
|
||||
for _, key := range groupBy {
|
||||
for _, key := range req.GroupBy {
|
||||
col := quoteIdentifier(key.Name)
|
||||
latestConditionPerNodeSelectCols = append(latestConditionPerNodeSelectCols, fmt.Sprintf("tsfp.%s AS %s", col, col))
|
||||
latestConditionPerNodeGroupBy = append(latestConditionPerNodeGroupBy, col)
|
||||
@@ -250,17 +248,17 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
))
|
||||
latestConditionPerNode.Where(
|
||||
latestConditionPerNode.E("samples.metric_name", nodeConditionMetricName),
|
||||
latestConditionPerNode.GE("samples.unix_milli", start),
|
||||
latestConditionPerNode.L("samples.unix_milli", end),
|
||||
latestConditionPerNode.GE("samples.unix_milli", req.Start),
|
||||
latestConditionPerNode.L("samples.unix_milli", req.End),
|
||||
"tsfp.node_name != ''",
|
||||
)
|
||||
latestConditionPerNode.GroupBy(latestConditionPerNodeGroupBy...)
|
||||
latestConditionPerNodeSQL, latestConditionPerNodeArgs := latestConditionPerNode.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// ----- countNodesPerCondition (outer SELECT) -----
|
||||
countNodesPerConditionSelectCols := make([]string, 0, len(groupBy)+2)
|
||||
countNodesPerConditionGroupBy := make([]string, 0, len(groupBy))
|
||||
for _, key := range groupBy {
|
||||
countNodesPerConditionSelectCols := make([]string, 0, len(req.GroupBy)+2)
|
||||
countNodesPerConditionGroupBy := make([]string, 0, len(req.GroupBy))
|
||||
for _, key := range req.GroupBy {
|
||||
col := quoteIdentifier(key.Name)
|
||||
countNodesPerConditionSelectCols = append(countNodesPerConditionSelectCols, col)
|
||||
countNodesPerConditionGroupBy = append(countNodesPerConditionGroupBy, col)
|
||||
@@ -291,8 +289,8 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
|
||||
result := make(map[string]nodeConditionCounts)
|
||||
for rows.Next() {
|
||||
groupVals := make([]string, len(groupBy))
|
||||
scanPtrs := make([]any, 0, len(groupBy)+2)
|
||||
groupVals := make([]string, len(req.GroupBy))
|
||||
scanPtrs := make([]any, 0, len(req.GroupBy)+2)
|
||||
for i := range groupVals {
|
||||
scanPtrs = append(scanPtrs, &groupVals[i])
|
||||
}
|
||||
|
||||
@@ -189,29 +189,27 @@ 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,
|
||||
start, end int64,
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
req *inframonitoringtypes.PostablePods,
|
||||
pageGroups []map[string]string,
|
||||
) (map[string]podPhaseCounts, error) {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
if len(pageGroups) == 0 || len(req.GroupBy) == 0 {
|
||||
return map[string]podPhaseCounts{}, nil
|
||||
}
|
||||
|
||||
// Merge user filter with page-groups IN clauses.
|
||||
userFilterExpr := ""
|
||||
if filter != nil {
|
||||
userFilterExpr = filter.Expression
|
||||
// Merged filter expression (user filter + page-groups IN clauses).
|
||||
reqFilterExpr := ""
|
||||
if req.Filter != nil {
|
||||
reqFilterExpr = req.Filter.Expression
|
||||
}
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
|
||||
filterExpr := mergeFilterExpressions(reqFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
// Resolve tables. Same convention as hosts (distributed names from helpers).
|
||||
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(start), uint64(end), nil,
|
||||
uint64(req.Start), uint64(req.End), nil,
|
||||
)
|
||||
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
|
||||
uint64(start), uint64(end),
|
||||
uint64(req.Start), uint64(req.End),
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
|
||||
)
|
||||
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
|
||||
@@ -222,7 +220,7 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
"fingerprint",
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS pod_uid", timeSeriesFPs.Var(podUIDAttrKey)),
|
||||
}
|
||||
for _, key := range groupBy {
|
||||
for _, key := range req.GroupBy {
|
||||
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", timeSeriesFPs.Var(key.Name), quoteIdentifier(key.Name)),
|
||||
)
|
||||
@@ -234,8 +232,8 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
timeSeriesFPs.GE("unix_milli", adjustedStart),
|
||||
timeSeriesFPs.L("unix_milli", adjustedEnd),
|
||||
)
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if filterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -244,7 +242,7 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
}
|
||||
}
|
||||
timeSeriesFPsGroupBy := []string{"fingerprint", "pod_uid"}
|
||||
for _, key := range groupBy {
|
||||
for _, key := range req.GroupBy {
|
||||
timeSeriesFPsGroupBy = append(timeSeriesFPsGroupBy, quoteIdentifier(key.Name))
|
||||
}
|
||||
timeSeriesFPs.GroupBy(timeSeriesFPsGroupBy...)
|
||||
@@ -253,7 +251,7 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
latestPhasePerPod := sqlbuilder.NewSelectBuilder()
|
||||
latestPhasePerPodSelectCols := []string{"tsfp.pod_uid AS pod_uid"}
|
||||
latestPhasePerPodGroupBy := []string{"pod_uid"}
|
||||
for _, key := range groupBy {
|
||||
for _, key := range req.GroupBy {
|
||||
col := quoteIdentifier(key.Name)
|
||||
latestPhasePerPodSelectCols = append(latestPhasePerPodSelectCols, fmt.Sprintf("tsfp.%s AS %s", col, col))
|
||||
latestPhasePerPodGroupBy = append(latestPhasePerPodGroupBy, col)
|
||||
@@ -268,17 +266,17 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
))
|
||||
latestPhasePerPod.Where(
|
||||
latestPhasePerPod.E("samples.metric_name", podPhaseMetricName),
|
||||
latestPhasePerPod.GE("samples.unix_milli", start),
|
||||
latestPhasePerPod.L("samples.unix_milli", end),
|
||||
latestPhasePerPod.GE("samples.unix_milli", req.Start),
|
||||
latestPhasePerPod.L("samples.unix_milli", req.End),
|
||||
"tsfp.pod_uid != ''",
|
||||
)
|
||||
latestPhasePerPod.GroupBy(latestPhasePerPodGroupBy...)
|
||||
latestPhasePerPodSQL, latestPhasePerPodArgs := latestPhasePerPod.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// ----- countPodsPerPhase (outer SELECT) -----
|
||||
countPodsPerPhaseSelectCols := make([]string, 0, len(groupBy)+5)
|
||||
countPodsPerPhaseGroupBy := make([]string, 0, len(groupBy))
|
||||
for _, key := range groupBy {
|
||||
countPodsPerPhaseSelectCols := make([]string, 0, len(req.GroupBy)+5)
|
||||
countPodsPerPhaseGroupBy := make([]string, 0, len(req.GroupBy))
|
||||
for _, key := range req.GroupBy {
|
||||
col := quoteIdentifier(key.Name)
|
||||
countPodsPerPhaseSelectCols = append(countPodsPerPhaseSelectCols, col)
|
||||
countPodsPerPhaseGroupBy = append(countPodsPerPhaseGroupBy, col)
|
||||
@@ -312,8 +310,8 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
|
||||
result := make(map[string]podPhaseCounts)
|
||||
for rows.Next() {
|
||||
groupVals := make([]string, len(groupBy))
|
||||
scanPtrs := make([]any, 0, len(groupBy)+5)
|
||||
groupVals := make([]string, len(req.GroupBy))
|
||||
scanPtrs := make([]any, 0, len(req.GroupBy)+5)
|
||||
for i := range groupVals {
|
||||
scanPtrs = append(scanPtrs, &groupVals[i])
|
||||
}
|
||||
|
||||
@@ -12,10 +12,8 @@ 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 (
|
||||
@@ -24,8 +22,6 @@ 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
|
||||
@@ -397,16 +393,11 @@ 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: unmarshal bytes into map[string]any
|
||||
// Post-process JSON columns: normalize into String value
|
||||
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
|
||||
switch x := val.(type) {
|
||||
case []byte:
|
||||
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
|
||||
val = string(x)
|
||||
default:
|
||||
// already a structured type (map[string]any, []any, etc.)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,9 @@ 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.
|
||||
@@ -53,7 +50,7 @@ func getQueryName(spec any) string {
|
||||
return getqueryInfo(spec).Name
|
||||
}
|
||||
|
||||
func (q *querier) postProcessResults(ctx context.Context, orgID valuer.UUID, results map[string]any, req *qbtypes.QueryRangeRequest) (map[string]any, error) {
|
||||
func (q *querier) postProcessResults(ctx context.Context, 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 {
|
||||
@@ -72,7 +69,6 @@ func (q *querier) postProcessResults(ctx context.Context, orgID valuer.UUID, res
|
||||
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]:
|
||||
@@ -1050,33 +1046,3 @@ 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
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ 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"
|
||||
@@ -36,7 +35,6 @@ var (
|
||||
|
||||
type querier struct {
|
||||
logger *slog.Logger
|
||||
fl flagger.Flagger
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
metadataStore telemetrytypes.MetadataStore
|
||||
promEngine prometheus.Prometheus
|
||||
@@ -64,12 +62,10 @@ 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,
|
||||
@@ -688,7 +684,7 @@ func (q *querier) run(
|
||||
}
|
||||
|
||||
gomaps.Copy(results, preseededResults)
|
||||
processedResults, err := q.postProcessResults(ctx, orgID, results, req)
|
||||
processedResults, err := q.postProcessResults(ctx, results, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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"
|
||||
@@ -45,15 +44,14 @@ 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
|
||||
flaggertest.New(t), // flagger
|
||||
nil, // prometheus
|
||||
nil, // traceStmtBuilder
|
||||
nil, // logStmtBuilder
|
||||
nil, // auditStmtBuilder
|
||||
nil, // metricStmtBuilder
|
||||
nil, // meterStmtBuilder
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
)
|
||||
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
@@ -118,7 +116,6 @@ func TestQueryRange_MetricTypeFromStore(t *testing.T) {
|
||||
nil, // meterStmtBuilder
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
flaggertest.New(t), // flagger
|
||||
)
|
||||
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
|
||||
@@ -186,6 +186,5 @@ func newProvider(
|
||||
meterStmtBuilder,
|
||||
traceOperatorStmtBuilder,
|
||||
bucketCache,
|
||||
flagger,
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -348,8 +348,7 @@ func (m *Manager) GetInstalledIntegrationDashboardById(
|
||||
CreatedBy: author,
|
||||
UpdatedBy: author,
|
||||
},
|
||||
OrgID: orgId,
|
||||
Source: dashboardtypes.SourceIntegration,
|
||||
OrgID: orgId,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -388,8 +387,7 @@ func (m *Manager) GetDashboardsForInstalledIntegrations(
|
||||
CreatedBy: author,
|
||||
UpdatedBy: author,
|
||||
},
|
||||
OrgID: orgId,
|
||||
Source: dashboardtypes.SourceIntegration,
|
||||
OrgID: orgId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,28 +32,30 @@ import (
|
||||
)
|
||||
|
||||
type PrepareTaskOptions struct {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
type PrepareTestRuleOptions struct {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
const taskNameSuffix = "webAppEditor"
|
||||
@@ -87,7 +89,7 @@ type ManagerOptions struct {
|
||||
Alertmanager alertmanager.Alertmanager
|
||||
OrgGetter organization.Getter
|
||||
RuleStore ruletypes.RuleStore
|
||||
MaintenanceStore alertmanagertypes.MaintenanceStore
|
||||
MaintenanceStore ruletypes.MaintenanceStore
|
||||
SQLStore sqlstore.SQLStore
|
||||
QueryParser queryparser.QueryParser
|
||||
}
|
||||
@@ -101,7 +103,7 @@ type Manager struct {
|
||||
block chan struct{}
|
||||
// datastore to store alert definitions
|
||||
ruleStore ruletypes.RuleStore
|
||||
maintenanceStore alertmanagertypes.MaintenanceStore
|
||||
maintenanceStore ruletypes.MaintenanceStore
|
||||
|
||||
logger *slog.Logger
|
||||
cache cache.Cache
|
||||
@@ -132,6 +134,7 @@ func defaultOptions(o *ManagerOptions) *ManagerOptions {
|
||||
}
|
||||
|
||||
func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
|
||||
|
||||
rules := make([]Rule, 0)
|
||||
var task Task
|
||||
|
||||
@@ -156,6 +159,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
|
||||
WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return task, err
|
||||
}
|
||||
@@ -163,7 +167,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)
|
||||
task = newTask(TaskTypeCh, opts.TaskName, taskNameSuffix, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
|
||||
|
||||
@@ -179,6 +183,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
|
||||
WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return task, err
|
||||
}
|
||||
@@ -186,7 +191,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)
|
||||
task = newTask(TaskTypeProm, opts.TaskName, taskNameSuffix, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
@@ -232,11 +237,10 @@ func (m *Manager) RuleStore() ruletypes.RuleStore {
|
||||
return m.ruleStore
|
||||
}
|
||||
|
||||
func (m *Manager) MaintenanceStore() alertmanagertypes.MaintenanceStore {
|
||||
func (m *Manager) MaintenanceStore() ruletypes.MaintenanceStore {
|
||||
return m.maintenanceStore
|
||||
}
|
||||
|
||||
// TODO(jatinderjit): remove (unused)?
|
||||
func (m *Manager) Pause(b bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
@@ -426,17 +430,19 @@ 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,
|
||||
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,
|
||||
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,
|
||||
})
|
||||
|
||||
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")
|
||||
@@ -637,17 +643,19 @@ 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,
|
||||
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,
|
||||
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,
|
||||
})
|
||||
|
||||
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")
|
||||
@@ -695,6 +703,7 @@ 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)
|
||||
@@ -888,6 +897,7 @@ 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 {
|
||||
@@ -1019,15 +1029,16 @@ func (m *Manager) TestNotification(ctx context.Context, orgID valuer.UUID, ruleS
|
||||
}
|
||||
|
||||
alertCount, err := m.prepareTestRuleFunc(PrepareTestRuleOptions{
|
||||
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,
|
||||
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,
|
||||
})
|
||||
|
||||
return alertCount, err
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user