Compare commits

..

1 Commits

Author SHA1 Message Date
nityanandagohain
e3a22cd7cf chore: speed up python integration test setup builds 2026-05-20 13:07:52 +05:30
99 changed files with 1782 additions and 3822 deletions

View File

@@ -45,9 +45,20 @@ jobs:
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && contains(github.event.pull_request.labels.*.name, 'safe-to-e2e')
runs-on: ubuntu-latest
timeout-minutes: 30
env:
SIGNOZ_INTEGRATION_BUILD_CACHE_DIR: /tmp/signoz-e2e-buildx-cache
steps:
- name: checkout
uses: actions/checkout@v4
- name: setup-buildx
uses: docker/setup-buildx-action@v3
- name: restore-buildx-cache
uses: actions/cache@v4
with:
path: /tmp/signoz-e2e-buildx-cache
key: ${{ runner.os }}-signoz-e2e-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-signoz-e2e-buildx-
- name: python
uses: actions/setup-python@v5
with:

View File

@@ -69,9 +69,20 @@ jobs:
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && contains(github.event.pull_request.labels.*.name, 'safe-to-integrate')
runs-on: ubuntu-latest
env:
SIGNOZ_INTEGRATION_BUILD_CACHE_DIR: /tmp/signoz-integration-buildx-cache
steps:
- name: checkout
uses: actions/checkout@v4
- name: setup-buildx
uses: docker/setup-buildx-action@v3
- name: restore-buildx-cache
uses: actions/cache@v4
with:
path: /tmp/signoz-integration-buildx-cache
key: ${{ runner.os }}-signoz-integration-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-signoz-integration-buildx-
- name: python
uses: actions/setup-python@v5
with:

View File

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

View File

@@ -34,6 +34,7 @@ DOCKER_BUILD_ARCHS_ENTERPRISE = $(addprefix docker-build-enterprise-,$(ARCHS))
DOCKERFILE_ENTERPRISE = $(SRC)/cmd/enterprise/Dockerfile
DOCKER_REGISTRY_ENTERPRISE ?= docker.io/signoz/signoz
JS_BUILD_CONTEXT = $(SRC)/frontend
DOCKER_BUILDX_PRUNE_FLAGS ?= --force
##############################################################
# directories
@@ -229,6 +230,21 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
@find tests -type f -name "*.pyo" -delete 2>/dev/null || true
@echo ">> python cache cleaned"
.PHONY: py-docker-clean
py-docker-clean: ## Remove Docker image and build caches used by python integration tests
@echo ">> removing SigNoz integration test image"
@docker image rm -f signoz:integration 2>/dev/null || true
@echo ">> removing local integration buildx cache directories"
@rm -rf /tmp/signoz-integration-buildx-cache /tmp/signoz-integration-buildx-cache-next /tmp/signoz-e2e-buildx-cache /tmp/signoz-e2e-buildx-cache-next
@echo ">> pruning docker buildx cache with flags: $(DOCKER_BUILDX_PRUNE_FLAGS)"
@docker buildx prune $(DOCKER_BUILDX_PRUNE_FLAGS)
.PHONY: py-test-clean
py-test-clean: ## Tear down python test stack and remove python/Docker test caches
@$(MAKE) py-test-teardown || true
@$(MAKE) py-clean
@$(MAKE) py-docker-clean
##############################################################
# generate commands

View File

@@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1
FROM golang:1.25-bookworm
ARG OS="linux"
@@ -21,7 +23,8 @@ RUN set -eux; \
COPY go.mod go.sum ./
RUN go mod download
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY ./cmd/ ./cmd/
COPY ./ee/ ./ee/
@@ -29,7 +32,9 @@ COPY ./pkg/ ./pkg/
COPY ./templates/email /root/templates
COPY Makefile Makefile
RUN TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
RUN mv /root/linux-${TARGETARCH}/signoz /root/signoz
RUN chmod 755 /root /root/signoz

View File

@@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1
FROM node:22-bookworm AS build
WORKDIR /opt/
@@ -30,7 +32,8 @@ RUN set -eux; \
COPY go.mod go.sum ./
RUN go mod download
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY ./cmd/ ./cmd/
COPY ./ee/ ./ee/
@@ -38,7 +41,9 @@ COPY ./pkg/ ./pkg/
COPY ./templates/email /root/templates
COPY Makefile Makefile
RUN TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
RUN mv /root/linux-${TARGETARCH}/signoz /root/signoz
COPY --from=build /opt/build ./web/

View File

@@ -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:
@@ -5255,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
@@ -5282,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:
@@ -5334,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:
@@ -5345,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:
@@ -5464,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
@@ -8024,7 +8024,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
type: array
status:
type: string
@@ -8067,7 +8067,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
responses:
"201":
content:
@@ -8075,7 +8075,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
status:
type: string
required:
@@ -8178,7 +8178,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
status:
type: string
required:
@@ -8232,7 +8232,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
responses:
"204":
description: No Content

View File

@@ -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)
}

View File

@@ -34,7 +34,7 @@ export default defineConfig({
signal: true,
useOperationIdAsQueryKey: false,
},
useDates: false,
useDates: true,
useNamedParameters: true,
enumGenerationType: 'enum',
mutator: {

View File

@@ -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
> => {

View File

@@ -9,7 +9,7 @@ export interface AlertmanagertypesChannelDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -34,7 +34,7 @@ export interface AlertmanagertypesChannelDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ModelLabelSetDTO {
@@ -62,7 +62,7 @@ export interface AlertmanagertypesDeprecatedGettableAlertDTO {
* @type string
* @format date-time
*/
endsAt?: string;
endsAt?: Date;
/**
* @type string
*/
@@ -80,7 +80,7 @@ export interface AlertmanagertypesDeprecatedGettableAlertDTO {
* @type string
* @format date-time
*/
startsAt?: string;
startsAt?: Date;
status?: TypesAlertStatusDTO;
}
@@ -97,7 +97,7 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
* @type string
* @format date-time
*/
createdAt: string;
createdAt: Date;
/**
* @type string,null
*/
@@ -127,116 +127,13 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
* @type string
* @format date-time
*/
updatedAt: string;
updatedAt: Date;
/**
* @type string,null
*/
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
@@ -1953,7 +1834,7 @@ export interface AuthtypesGettableAuthDomainDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -1970,7 +1851,7 @@ export interface AuthtypesGettableAuthDomainDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface AuthtypesGettableTokenDTO {
@@ -2128,7 +2009,7 @@ export interface AuthtypesRoleDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -2153,7 +2034,7 @@ export interface AuthtypesRoleDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface AuthtypesSessionContextDTO {
@@ -2181,7 +2062,7 @@ export interface AuthtypesUserRoleDTO {
* @type string
* @format date-time
*/
createdAt: string;
createdAt: Date;
/**
* @type string
*/
@@ -2195,7 +2076,7 @@ export interface AuthtypesUserRoleDTO {
* @type string
* @format date-time
*/
updatedAt: string;
updatedAt: Date;
/**
* @type string
*/
@@ -2207,7 +2088,7 @@ export interface AuthtypesUserWithRolesDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -2236,7 +2117,7 @@ export interface AuthtypesUserWithRolesDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type array,null
*/
@@ -2403,7 +2284,7 @@ export interface CloudintegrationtypesAccountDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -2424,12 +2305,12 @@ export interface CloudintegrationtypesAccountDTO {
* @type string,null
* @format date-time
*/
removedAt: string | null;
removedAt: Date | null;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface DashboardtypesStorableDashboardDataDTO {
@@ -2560,7 +2441,7 @@ export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -2570,7 +2451,7 @@ export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
};
/**
@@ -2764,12 +2645,12 @@ export interface CloudintegrationtypesGettableAgentCheckInDTO {
* @type string,null
* @format date-time
*/
removed_at: string | null;
removed_at: Date | null;
/**
* @type string,null
* @format date-time
*/
removedAt: string | null;
removedAt: Date | null;
}
export interface CloudintegrationtypesServiceMetadataDTO {
@@ -3004,7 +2885,7 @@ export interface DashboardtypesDashboardDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -3026,7 +2907,7 @@ export interface DashboardtypesDashboardDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -3208,7 +3089,7 @@ export interface GatewaytypesLimitDTO {
* @type string
* @format date-time
*/
created_at?: string;
created_at?: Date;
/**
* @type string
*/
@@ -3230,7 +3111,7 @@ export interface GatewaytypesLimitDTO {
* @type string
* @format date-time
*/
updated_at?: string;
updated_at?: Date;
}
export interface GatewaytypesIngestionKeyDTO {
@@ -3238,12 +3119,12 @@ export interface GatewaytypesIngestionKeyDTO {
* @type string
* @format date-time
*/
created_at?: string;
created_at?: Date;
/**
* @type string
* @format date-time
*/
expires_at?: string;
expires_at?: Date;
/**
* @type string
*/
@@ -3264,7 +3145,7 @@ export interface GatewaytypesIngestionKeyDTO {
* @type string
* @format date-time
*/
updated_at?: string;
updated_at?: Date;
/**
* @type string
*/
@@ -3288,7 +3169,7 @@ export interface GatewaytypesPostableIngestionKeyDTO {
* @type string
* @format date-time
*/
expires_at?: string;
expires_at?: Date;
/**
* @type string
*/
@@ -4558,7 +4439,7 @@ export interface LlmpricingruletypesLLMPricingRuleDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -4597,13 +4478,13 @@ export interface LlmpricingruletypesLLMPricingRuleDTO {
* @type string,null
* @format date-time
*/
syncedAt?: string | null;
syncedAt?: Date | null;
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -5829,7 +5710,7 @@ export interface Querybuildertypesv5RawRowDTO {
* @type string
* @format date-time
*/
timestamp?: string;
timestamp?: Date;
}
export interface Querybuildertypesv5RawDataDTO {
@@ -6235,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
@@ -6266,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?: Date | null;
/**
* @type array,null
*/
repeatOn?: RuletypesRepeatOnDTO[] | null;
repeatType: RuletypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: Date;
}
export interface RuletypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: Date;
recurrence?: RuletypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: Date;
/**
* @type string
*/
timezone: string;
}
export interface RuletypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: RuletypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: RuletypesScheduleDTO;
status: RuletypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
}
export interface RuletypesPostablePlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
schedule: RuletypesScheduleDTO;
}
export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
@@ -6406,7 +6406,7 @@ export interface RuletypesRuleDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6455,7 +6455,7 @@ export interface RuletypesRuleDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -6474,7 +6474,7 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type integer
* @minimum 0
@@ -6488,7 +6488,7 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
* @type string
* @format date-time
*/
lastObservedAt: string;
lastObservedAt: Date;
/**
* @type string
*/
@@ -6501,7 +6501,7 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO {
@@ -6546,7 +6546,7 @@ export interface ServiceaccounttypesServiceAccountDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6571,7 +6571,7 @@ export interface ServiceaccounttypesServiceAccountDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ServiceaccounttypesServiceAccountRoleDTO {
@@ -6579,7 +6579,7 @@ export interface ServiceaccounttypesServiceAccountRoleDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6597,7 +6597,7 @@ export interface ServiceaccounttypesServiceAccountRoleDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ServiceaccounttypesServiceAccountWithRolesDTO {
@@ -6605,7 +6605,7 @@ export interface ServiceaccounttypesServiceAccountWithRolesDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6634,7 +6634,7 @@ export interface ServiceaccounttypesServiceAccountWithRolesDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
@@ -6676,7 +6676,7 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6701,7 +6701,7 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -6770,7 +6770,7 @@ export interface SpantypesSpanMapperDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6796,7 +6796,7 @@ export interface SpantypesSpanMapperDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -7163,7 +7163,7 @@ export interface TypesDeprecatedUserDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -7196,7 +7196,7 @@ export interface TypesDeprecatedUserDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface TypesIdentifiableDTO {
@@ -7211,7 +7211,7 @@ export interface TypesInviteDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -7244,7 +7244,7 @@ export interface TypesInviteDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface TypesOrganizationDTO {
@@ -7256,7 +7256,7 @@ export interface TypesOrganizationDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -7278,7 +7278,7 @@ export interface TypesOrganizationDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface TypesPostableInviteDTO {
@@ -7345,7 +7345,7 @@ export interface TypesResetPasswordTokenDTO {
* @type string
* @format date-time
*/
expiresAt?: string;
expiresAt?: Date;
/**
* @type string
*/
@@ -7372,7 +7372,7 @@ export interface TypesUserDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -7401,7 +7401,7 @@ export interface TypesUserDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ZeustypesHostDTO {
@@ -7793,7 +7793,7 @@ export type ListDowntimeSchedules200 = {
/**
* @type array
*/
data: AlertmanagertypesPlannedMaintenanceDTO[];
data: RuletypesPlannedMaintenanceDTO[];
/**
* @type string
*/
@@ -7801,7 +7801,7 @@ export type ListDowntimeSchedules200 = {
};
export type CreateDowntimeSchedule201 = {
data: AlertmanagertypesPlannedMaintenanceDTO;
data: RuletypesPlannedMaintenanceDTO;
/**
* @type string
*/
@@ -7815,7 +7815,7 @@ export type GetDowntimeScheduleByIDPathParameters = {
id: string;
};
export type GetDowntimeScheduleByID200 = {
data: AlertmanagertypesPlannedMaintenanceDTO;
data: RuletypesPlannedMaintenanceDTO;
/**
* @type string
*/

View File

@@ -59,7 +59,7 @@ function getDeleteTooltip(
function getInviteButtonLabel(
isLoading: boolean,
existingToken: { expiresAt?: string } | undefined,
existingToken: { expiresAt?: Date } | undefined,
isExpired: boolean,
notFound: boolean,
): string {

View File

@@ -1,176 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { FloatingPanel } from 'periscope/components/FloatingPanel';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import AddedFields from './AddedFields';
import OtherFields from './OtherFields';
import styles from './FieldsSelector.module.scss';
const DEFAULT_PANEL_WIDTH = 350;
const DEFAULT_PANEL_HEIGHT_OFFSET = 100;
const DEFAULT_PANEL_RIGHT_INSET = 100;
const DEFAULT_PANEL_TOP_INSET = 50;
interface FieldsSelectorProps {
isOpen: boolean;
title: string;
fields: TelemetryFieldKey[];
onFieldsChange: (fields: TelemetryFieldKey[]) => void;
onClose: () => void;
signal: DataSource;
maxFields?: number;
width?: number;
height?: number;
defaultPosition?: { x: number; y: number };
}
function FieldsSelector({
isOpen,
title,
fields,
onFieldsChange,
onClose,
signal,
maxFields,
width = DEFAULT_PANEL_WIDTH,
height,
defaultPosition,
}: FieldsSelectorProps): JSX.Element | null {
if (!isOpen) {
return null;
}
const resolvedHeight =
height ?? window.innerHeight - DEFAULT_PANEL_HEIGHT_OFFSET;
const resolvedPosition = defaultPosition ?? {
x: window.innerWidth - width - DEFAULT_PANEL_RIGHT_INSET,
y: DEFAULT_PANEL_TOP_INSET,
};
const [draftFields, setDraftFields] = useState<TelemetryFieldKey[]>(fields);
const [inputValue, setInputValue] = useState('');
const [debouncedInputValue, setDebouncedInputValue] = useState('');
const debouncedUpdate = useDebouncedFn((value) => {
setDebouncedInputValue(value as string);
}, 400);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value.trim().toLowerCase();
setInputValue(value);
debouncedUpdate(value);
},
[debouncedUpdate],
);
const handleAdd = useCallback(
(field: TelemetryFieldKey): void => {
if (maxFields !== undefined && draftFields.length >= maxFields) {
return;
}
if (draftFields.some((f) => f.name === field.name)) {
return;
}
setDraftFields((prev) => [...prev, field]);
},
[draftFields, maxFields],
);
const handleSave = useCallback((): void => {
onFieldsChange(draftFields);
toast.success('Saved successfully', {
position: 'top-right',
});
onClose();
}, [draftFields, onFieldsChange, onClose]);
const handleDiscard = useCallback((): void => {
setDraftFields(fields);
}, [fields]);
const hasUnsavedChanges = useMemo(
() =>
!(
draftFields.length === fields.length &&
draftFields.every((f, i) => f.name === fields[i]?.name)
),
[draftFields, fields],
);
const isAtLimit = maxFields !== undefined && draftFields.length >= maxFields;
return (
<FloatingPanel
isOpen
width={width}
height={resolvedHeight}
defaultPosition={resolvedPosition}
enableResizing={false}
>
<div className={styles.root}>
<div className={styles.header}>
<div className={styles.title}>
<TableColumnsSplit size={16} />
{title}
</div>
<X className={styles.closeIcon} size={16} onClick={onClose} />
</div>
<section>
<Input
className={styles.searchInput}
type="text"
value={inputValue}
placeholder="Search for a field..."
onChange={handleInputChange}
/>
</section>
<AddedFields
inputValue={inputValue}
fields={draftFields}
onFieldsChange={setDraftFields}
maxFields={maxFields}
/>
<OtherFields
signal={signal}
debouncedInputValue={debouncedInputValue}
addedFields={draftFields}
onAdd={handleAdd}
isAtLimit={isAtLimit}
/>
{hasUnsavedChanges && (
<div className={styles.footer}>
<Button
variant="outlined"
color="secondary"
onClick={handleDiscard}
prefix={<X width={14} height={14} />}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
onClick={handleSave}
prefix={<Check width={14} height={14} />}
>
Save changes
</Button>
</div>
)}
</div>
</FloatingPanel>
);
}
export default FieldsSelector;

View File

@@ -1 +0,0 @@
export { default } from './FieldsSelector';

View File

@@ -5,8 +5,6 @@ import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from 'container/AIAssistant/events';
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
import {
openAIAssistant,
useAIAssistantStore,
@@ -52,14 +50,6 @@ function HeaderRightSection({
setOpenAnnouncementsModal(false);
}, [location.pathname]);
const handleOpenAIAssistant = useCallback((): void => {
void logEvent(AIAssistantEvents.Opened, {
source: 'header',
currentPage: normalizePage(location.pathname),
});
openAIAssistant();
}, [location.pathname]);
const handleOpenShareURLModal = useCallback((): void => {
logEvent('Share: Clicked', {
page: location.pathname,
@@ -111,7 +101,7 @@ function HeaderRightSection({
<Button
variant="solid"
color="secondary"
onClick={handleOpenAIAssistant}
onClick={openAIAssistant}
aria-label={
showHeaderPendingBadge
? pendingUserInputCount === 1

View File

@@ -28,7 +28,7 @@ const mockKey: ServiceaccounttypesGettableFactorAPIKeyDTO = {
id: 'key-1',
name: 'Original Key Name',
expiresAt: 0,
lastObservedAt: null as unknown as string,
lastObservedAt: null as unknown as Date,
serviceAccountId: 'sa-1',
};

View File

@@ -29,14 +29,14 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
id: 'key-1',
name: 'Production Key',
expiresAt: 0,
lastObservedAt: null as unknown as string,
lastObservedAt: null as unknown as Date,
serviceAccountId: 'sa-1',
},
{
id: 'key-2',
name: 'Staging Key',
expiresAt: 1924905600, // 2030-12-31
lastObservedAt: '2026-03-10T10:00:00Z',
lastObservedAt: new Date('2026-03-10T10:00:00Z'),
serviceAccountId: 'sa-1',
},
];

View File

@@ -108,7 +108,4 @@ export const REACT_QUERY_KEY = {
// Dashboard Grid Card Query Keys
DASHBOARD_GRID_CARD_QUERY_RANGE: 'DASHBOARD_GRID_CARD_QUERY_RANGE',
// Fields Selector Query Keys
GET_FIELDS_SELECTOR_SUGGESTIONS: 'GET_FIELDS_SELECTOR_SUGGESTIONS',
} as const;

View File

@@ -1,20 +1,13 @@
import { useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Minus, Plus, Sparkles, X } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import HistorySidebar from '../components/ConversationsList';
import ConversationView from '../ConversationView';
import { AIAssistantEvents } from '../events';
import {
normalizePage,
useAIAssistantAnalyticsContext,
} from '../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../store/useAIAssistantStore';
import { VariantContext } from '../VariantContext';
@@ -31,7 +24,6 @@ import styles from './AIAssistantModal.module.scss';
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function AIAssistantModal(): JSX.Element | null {
const history = useHistory();
const { pathname } = useLocation();
const [showHistory, setShowHistory] = useState(false);
const isOpen = useAIAssistantStore((s) => s.isModalOpen);
@@ -44,7 +36,6 @@ export default function AIAssistantModal(): JSX.Element | null {
const startNewConversation = useAIAssistantStore(
(s) => s.startNewConversation,
);
const analyticsCtx = useAIAssistantAnalyticsContext();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent): void => {
@@ -64,10 +55,6 @@ export default function AIAssistantModal(): JSX.Element | null {
} else {
startNewConversation();
setShowHistory(false);
void logEvent(AIAssistantEvents.Opened, {
source: 'shortcut',
currentPage: normalizePage(pathname),
});
openModal();
}
return;
@@ -81,7 +68,7 @@ export default function AIAssistantModal(): JSX.Element | null {
window.addEventListener('keydown', handleKeyDown);
return (): void => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, openModal, closeModal, startNewConversation, pathname]);
}, [isOpen, openModal, closeModal, startNewConversation]);
// ── Handlers ────────────────────────────────────────────────────────────────
@@ -90,28 +77,15 @@ export default function AIAssistantModal(): JSX.Element | null {
return;
}
closeModal();
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
// the assistant was already open in the modal, so this is a surface
// switch, not a new open.
history.push(
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
{ fromInApp: true },
);
}, [activeConversationId, closeModal, history]);
const handleNew = useCallback(() => {
void logEvent(AIAssistantEvents.NewChatClicked, {
...analyticsCtx,
// useAIAssistantAnalyticsContext() runs above this component's
// VariantContext.Provider, so the hook reports the default 'page'
// mode. Override here: the modal collapses to 'sidepane' in our
// taxonomy alongside the drawer.
mode: 'sidepane',
source: 'header',
});
startNewConversation();
setShowHistory(false);
}, [startNewConversation, analyticsCtx]);
}, [startNewConversation]);
const handleHistorySelect = useCallback(() => {
setShowHistory(false);

View File

@@ -5,12 +5,8 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Plus, Sparkles, X } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import ConversationsList from '../components/ConversationsList';
import ConversationView from '../ConversationView';
import { AIAssistantEvents } from '../events';
import { useAIAssistantAnalyticsContext } from '../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../store/useAIAssistantStore';
import { VariantContext } from '../VariantContext';
@@ -36,35 +32,21 @@ export default function AIAssistantPanel(): JSX.Element | null {
const startNewConversation = useAIAssistantStore(
(s) => s.startNewConversation,
);
const analyticsCtx = useAIAssistantAnalyticsContext();
const handleExpand = useCallback(() => {
if (!activeConversationId) {
return;
}
closeDrawer();
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
// the assistant was already open in the drawer, so this is a surface
// switch, not a new open.
history.push(
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
{ fromInApp: true },
);
}, [activeConversationId, closeDrawer, history]);
const handleNew = useCallback(() => {
void logEvent(AIAssistantEvents.NewChatClicked, {
...analyticsCtx,
// useAIAssistantAnalyticsContext() runs above this component's
// VariantContext.Provider, so the hook reports the default 'page'
// mode. Override here: this handler only runs when the drawer
// itself is mounted, which is unambiguously the sidepane surface.
mode: 'sidepane',
source: 'header',
});
startNewConversation();
setShowHistory(false);
}, [startNewConversation, analyticsCtx]);
}, [startNewConversation]);
// When user picks a conversation from the list, close the sidebar
const handleHistorySelect = useCallback(() => {

View File

@@ -1,13 +1,9 @@
import { useCallback } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { Bot } from '@signozhq/icons';
import { AIAssistantEvents } from '../events';
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
import {
openAIAssistant,
useAIAssistantStore,
@@ -29,14 +25,6 @@ export default function AIAssistantTrigger(): JSX.Element | null {
exact: true,
});
const handleOpen = useCallback((): void => {
void logEvent(AIAssistantEvents.Opened, {
source: 'icon',
currentPage: normalizePage(pathname),
});
openAIAssistant();
}, [pathname]);
if (isDrawerOpen || isModalOpen || isFullScreenPage) {
return null;
}
@@ -47,7 +35,7 @@ export default function AIAssistantTrigger(): JSX.Element | null {
variant="solid"
color="primary"
className={styles.trigger}
onClick={handleOpen}
onClick={openAIAssistant}
aria-label="Open AI Assistant"
>
<Bot size={20} />

View File

@@ -1,15 +1,11 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import cx from 'classnames';
import logEvent from 'api/common/logEvent';
import ChatInput, { autoContextKey } from '../components/ChatInput';
import ConversationSkeleton from '../components/ConversationSkeleton';
import VirtualizedMessages from '../components/VirtualizedMessages';
import { AIAssistantEvents } from '../events';
import { getAutoContexts } from '../getAutoContexts';
import { useAIAssistantAnalyticsContext } from '../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../store/useAIAssistantStore';
import { MessageAttachment } from '../types';
import { MessageContext } from '../../../api/ai-assistant/chat';
@@ -43,7 +39,6 @@ export default function ConversationView({
);
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const cancelStream = useAIAssistantStore((s) => s.cancelStream);
const analyticsCtx = useAIAssistantAnalyticsContext(conversationId);
// Auto-derived contexts come from the route the user is currently looking
// at (dashboard detail, service metrics, an explorer, …). Skip when the
@@ -87,50 +82,14 @@ export default function ConversationView({
attachments?: MessageAttachment[],
contexts?: MessageContext[],
) => {
const hasAuto = contexts?.some((c) => c.source === 'auto') ?? false;
const hasManual = contexts?.some((c) => c.source === 'mention') ?? false;
let contextType: 'manual' | 'auto' | 'both' | undefined;
if (hasAuto && hasManual) {
contextType = 'both';
} else if (hasAuto) {
contextType = 'auto';
} else if (hasManual) {
contextType = 'manual';
}
void logEvent(AIAssistantEvents.MessageSent, {
...analyticsCtx,
queryLength: text.length,
hasContext: hasAuto || hasManual,
contextType,
respondingToClarification: Boolean(pendingClarificationHere),
});
void sendMessage(text, attachments, contexts);
},
[sendMessage, analyticsCtx, pendingClarificationHere],
[sendMessage],
);
// Wall-clock timestamp of the current streaming start, used to compute
// `secondsSinceStart` on Cancel clicked. Cleared whenever streaming ends.
const streamStartedAtRef = useRef<number | null>(null);
useEffect(() => {
if (!isStreamingHere) {
streamStartedAtRef.current = null;
return;
}
if (streamStartedAtRef.current === null) {
streamStartedAtRef.current = Date.now();
}
}, [isStreamingHere]);
const handleCancel = useCallback(() => {
const startedAt = streamStartedAtRef.current;
void logEvent(AIAssistantEvents.CancelClicked, {
threadId: analyticsCtx.threadId,
secondsSinceStart:
startedAt !== null ? Math.round((Date.now() - startedAt) / 1000) : null,
});
cancelStream(conversationId);
}, [cancelStream, conversationId, analyticsCtx.threadId]);
}, [cancelStream, conversationId]);
const messages = conversation?.messages ?? [];
const showDisclaimer = messages.length > 0;
@@ -175,7 +134,6 @@ export default function ConversationView({
conversationId={conversationId}
messages={messages}
isStreaming={isStreamingHere}
onSendSuggestedPrompt={(text): void => handleSend(text)}
/>
{showDisclaimer && (
<div className={disclaimerClass} role="note" aria-live="polite">

View File

@@ -41,68 +41,12 @@ import {
Undo,
} from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import styles from './ActionsSection.module.scss';
interface ActionsSectionProps {
actions: MessageActionDTO[];
/** ID of the assistant message these actions belong to — used in analytics. */
messageId: string;
}
/**
* Resource-type strings the backend uses for `open_resource` and rollback
* actions. Centralized here so the route/module lookups below stay in sync.
*/
const ResourceType = {
dashboard: 'dashboard',
alert: 'alert',
service: 'service',
saved_view: 'saved_view',
logs_explorer: 'logs_explorer',
traces_explorer: 'traces_explorer',
metrics_explorer: 'metrics_explorer',
} as const;
/** Maps an open_resource action's resourceType to its product module name. */
function targetModuleForResource(resourceType: string): string | null {
switch (resourceType) {
case ResourceType.dashboard:
return 'dashboards';
case ResourceType.alert:
return 'alerts';
case ResourceType.service:
return 'apm';
case ResourceType.saved_view:
return 'savedViews';
case ResourceType.logs_explorer:
return 'logs';
case ResourceType.traces_explorer:
return 'traces';
case ResourceType.metrics_explorer:
return 'metrics';
default:
return null;
}
}
/** Maps an apply_filter signal to its product module name. */
function targetModuleForSignal(signal: ApplyFilterSignalDTO): string | null {
switch (signal) {
case ApplyFilterSignalDTO.logs:
return 'logs';
case ApplyFilterSignalDTO.traces:
return 'traces';
case ApplyFilterSignalDTO.metrics:
return 'metrics';
default:
return null;
}
}
type ChipState = 'idle' | 'loading' | 'success' | 'error';
@@ -150,23 +94,23 @@ function resourceRoute(
resourceId: string,
): string | null {
switch (resourceType) {
case ResourceType.dashboard:
case 'dashboard':
return ROUTES.DASHBOARD.replace(':dashboardId', resourceId);
case ResourceType.alert: {
case 'alert': {
const params = new URLSearchParams({ [QueryParams.ruleId]: resourceId });
return `${ROUTES.EDIT_ALERTS}?${params.toString()}`;
}
case ResourceType.service:
case 'service':
return ROUTES.SERVICE_METRICS.replace(':servicename', resourceId);
case ResourceType.saved_view:
case 'saved_view':
// No detail route — saved views land on the list page.
// Caller may provide signal-aware metadata in future; default to logs.
return ROUTES.LOGS_SAVE_VIEWS;
case ResourceType.logs_explorer:
case 'logs_explorer':
return ROUTES.LOGS_EXPLORER;
case ResourceType.traces_explorer:
case 'traces_explorer':
return ROUTES.TRACES_EXPLORER;
case ResourceType.metrics_explorer:
case 'metrics_explorer':
return ROUTES.METRICS_EXPLORER_EXPLORER;
default:
return null;
@@ -280,24 +224,6 @@ function actionKey(action: MessageActionDTO, index: number): string {
: `${action.kind}:${action.label}:${index}`;
}
/**
* Resolves the prompt to send for a follow_up action. The chip's `label` is
* the short display text (e.g. "Python setup"); the real prompt lives in
* `input.intent` per the schema doc. Falls back to label defensively so a
* malformed server payload doesn't drop the click silently. Both branches
* are trimmed so whitespace-only payloads don't become whitespace messages.
*/
function followUpIntent(action: MessageActionDTO): string {
const intent = action.input?.intent;
if (typeof intent === 'string') {
const trimmed = intent.trim();
if (trimmed.length > 0) {
return trimmed;
}
}
return action.label.trim();
}
/** Maps a signal to its target explorer route. */
function explorerRouteForSignal(signal: ApplyFilterSignalDTO): string | null {
switch (signal) {
@@ -427,12 +353,10 @@ function rollbackCall(
*/
export default function ActionsSection({
actions,
messageId,
}: ActionsSectionProps): JSX.Element | null {
const history = useHistory();
const { pathname } = useLocation();
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
const { redirectWithQueryBuilderData, handleSetQueryData } = useQueryBuilder();
// Per-chip click state, keyed by chip key (see `key` below). Persists
@@ -506,39 +430,13 @@ export default function ActionsSection({
switch (action.kind) {
case MessageActionKindDTO.open_docs: {
if (action.url) {
void logEvent(AIAssistantEvents.DocOpened, {
threadId,
messageId,
docPath: action.url,
});
openInNewTab(action.url);
}
break;
}
case MessageActionKindDTO.follow_up: {
const intent = followUpIntent(action);
if (intent) {
// Fire SuggestedPromptClicked + MessageSent so analytics can compute
// both the click-through rate against follow-ups offered *and* keep
// the unified send funnel intact. `category` distinguishes server-
// emitted follow-ups from the empty-state grid. `promptId` stays the
// label so dashboards group identical chip texts together regardless
// of the dynamic intent payload.
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
threadId,
messageId,
promptId: action.label,
category: SuggestedPromptCategory.FollowUp,
});
void logEvent(AIAssistantEvents.MessageSent, {
threadId,
page,
mode,
queryLength: intent.length,
hasContext: false,
respondingToClarification: false,
});
void sendMessage(intent);
if (action.label) {
void sendMessage(action.label);
}
break;
}
@@ -546,12 +444,6 @@ export default function ActionsSection({
if (action.resourceType && action.resourceId) {
const path = resourceRoute(action.resourceType, action.resourceId);
if (path) {
void logEvent(AIAssistantEvents.ResourceOpened, {
threadId,
messageId,
targetModule: targetModuleForResource(action.resourceType),
resourceId: action.resourceId,
});
history.push(path);
}
}
@@ -564,13 +456,6 @@ export default function ActionsSection({
break;
}
case MessageActionKindDTO.apply_filter: {
if (action.signal) {
void logEvent(AIAssistantEvents.ApplyFilterClicked, {
threadId,
messageId,
targetModule: targetModuleForSignal(action.signal),
});
}
applyFilter(action, {
history,
pathname,

View File

@@ -5,17 +5,13 @@ import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { toast } from '@signozhq/ui/sonner';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import type { UploadFile } from 'antd';
import getSessionStorage from 'api/browser/sessionstorage/get';
import setSessionStorage from 'api/browser/sessionstorage/set';
import {
getListRulesQueryKey,
useListRules,
} from 'api/generated/services/rules';
import type { ListRules200 } from 'api/generated/services/sigNoz.schemas';
import logEvent from 'api/common/logEvent';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useQueryService } from 'hooks/useQueryService';
@@ -26,8 +22,6 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { AIAssistantEvents, getBrowserInfo } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
import { MessageAttachment } from '../../types';
import { MessageContext } from '../../../../api/ai-assistant/chat';
@@ -143,8 +137,6 @@ function autoContextCategory(ctx: MessageContext): string {
const MAX_INPUT_LENGTH = 20000;
const WARNING_THRESHOLD = 15000;
const HOME_SERVICES_INTERVAL = 30 * 60 * 1000;
/** sessionStorage key for the "voice input failed this tab" flag. */
const VOICE_UNAVAILABLE_KEY = 'ai-assistant-voice-unavailable';
const CONTEXT_CATEGORIES = ['Dashboards', 'Alerts', 'Services'] as const;
@@ -376,28 +368,6 @@ export default function ChatInput({
// ── Voice input ────────────────────────────────────────────────────────────
const analyticsCtx = useAIAssistantAnalyticsContext();
// Captured at the start of a voice session, consumed when it ends.
// Tracks both the trigger (button vs. PTT shortcut) and the wall-clock
// start time so we can attribute `durationMs` on the Voice input used
// event regardless of which control ended the session.
const voiceStartedAtRef = useRef<number | null>(null);
const voiceSourceRef = useRef<'button' | 'shortcut' | null>(null);
// Set to true after a `network`, `not-allowed`, or `not-supported` failure
// so we hide the mic button for the rest of the tab session — silent
// retries don't help, and Chromium derivatives without the Google Speech
// API key always fail with `network` no matter how many times the user
// clicks. Persisted to sessionStorage so a page reload doesn't surface the
// button again (closing the tab still resets, in case the user fixed
// permissions or switched browsers).
const [voiceUnavailable, setVoiceUnavailable] = useState(
() => getSessionStorage(VOICE_UNAVAILABLE_KEY) === 'true',
);
const markVoiceUnavailable = useCallback((): void => {
setVoiceUnavailable(true);
setSessionStorage(VOICE_UNAVAILABLE_KEY, 'true');
}, []);
const {
isListening,
isSupported,
@@ -418,81 +388,9 @@ export default function ChatInput({
setText(capText(committedTextRef.current + separator + transcriptText));
}
},
onError: (error) => {
// Guard against double-fire: Chrome can fire `onerror` more than
// once per session when `continuous = true` (it retries internally
// before giving up). Only fire the analytics event for the first
// error in a given session — voiceSourceRef being null means we've
// already handled it.
const source = voiceSourceRef.current;
if (source === null) {
return;
}
voiceStartedAtRef.current = null;
voiceSourceRef.current = null;
void logEvent(AIAssistantEvents.VoiceInputFailed, {
...analyticsCtx,
...getBrowserInfo(),
source,
errorType: error,
});
if (error === 'network') {
markVoiceUnavailable();
toast.error('Voice input unavailable in this browser', {
description:
'This browser cannot reach the speech recognition service. Try Google Chrome or Microsoft Edge.',
});
} else if (error === 'not-allowed') {
markVoiceUnavailable();
toast.error('Microphone access denied', {
description:
'Grant microphone permission in your browser settings to use voice input.',
});
} else if (error === 'not-supported') {
markVoiceUnavailable();
toast.error('Voice input is not supported in this browser.');
}
// `no-speech` is benign (just silence) — don't toast or hide.
},
});
const showMic = isSupported && micPermission !== 'denied' && !voiceUnavailable;
const startVoiceInput = useCallback(
(source: 'button' | 'shortcut') => {
// Defense in depth: the button is hidden when `voiceUnavailable` is
// true, but the PTT shortcut listener can still call us. Bailing here
// keeps a single source of truth and prevents repeat `Voice input
// failed` events in the same session.
if (voiceUnavailable) {
return;
}
voiceStartedAtRef.current = Date.now();
voiceSourceRef.current = source;
start();
},
[start, voiceUnavailable],
);
const fireVoiceInputEvent = useCallback(
(outcome: 'sent' | 'discarded') => {
const startedAt = voiceStartedAtRef.current;
const source = voiceSourceRef.current;
voiceStartedAtRef.current = null;
voiceSourceRef.current = null;
if (startedAt === null || source === null) {
return;
}
void logEvent(AIAssistantEvents.VoiceInputUsed, {
...analyticsCtx,
...getBrowserInfo(),
source,
outcome,
durationMs: Date.now() - startedAt,
});
},
[analyticsCtx],
);
const showMic = isSupported && micPermission !== 'denied';
// Stop recording and immediately send whatever is in the textarea.
const handleStopAndSend = useCallback(async () => {
@@ -500,17 +398,15 @@ export default function ChatInput({
committedTextRef.current = capText(text);
// Stop recognition without triggering onTranscript again (would double-append).
discard();
fireVoiceInputEvent('sent');
await handleSend();
}, [text, discard, handleSend, capText, fireVoiceInputEvent]);
}, [text, discard, handleSend, capText]);
// Stop recording and revert the textarea to what it was before voice started.
const handleDiscard = useCallback(() => {
discard();
fireVoiceInputEvent('discarded');
setText(committedTextRef.current);
textareaRef.current?.focus();
}, [discard, fireVoiceInputEvent]);
}, [discard]);
// ── Push-to-talk (Cmd/Ctrl + Shift + Space) ────────────────────────────────
// Hold the combo to record; release Space to submit. We track which key
@@ -519,7 +415,7 @@ export default function ChatInput({
// "session active" ref so a held key only calls `start()` once.
const pttActiveRef = useRef(false);
useEffect(() => {
if (!isSupported || micPermission === 'denied' || voiceUnavailable) {
if (!isSupported || micPermission === 'denied') {
return undefined;
}
@@ -536,7 +432,7 @@ export default function ChatInput({
return; // ignore auto-repeat
}
pttActiveRef.current = true;
startVoiceInput('shortcut');
start();
};
const handleKeyUp = (e: KeyboardEvent): void => {
@@ -570,10 +466,9 @@ export default function ChatInput({
}, [
isSupported,
micPermission,
voiceUnavailable,
disabled,
isStreaming,
startVoiceInput,
start,
handleStopAndSend,
]);
@@ -1008,7 +903,7 @@ export default function ChatInput({
<Button
variant="ghost"
size="icon"
onClick={(): void => startVoiceInput('button')}
onClick={start}
disabled={disabled}
aria-label="Start voice input"
className={styles.micBtn}

View File

@@ -9,7 +9,6 @@ import {
SelectItem,
SelectTrigger,
} from '@signozhq/ui/select';
import logEvent from 'api/common/logEvent';
import { ClarificationFieldTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import type {
ClarificationEventDTO,
@@ -17,8 +16,6 @@ import type {
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { CircleHelp, Send, X } from '@signozhq/icons';
import { AIAssistantEvents } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import styles from './ClarificationForm.module.scss';
@@ -47,8 +44,6 @@ export default function ClarificationForm({
const isStreaming = useAIAssistantStore(
(s) => s.streams[conversationId]?.isStreaming ?? false,
);
const { threadId, page, mode } =
useAIAssistantAnalyticsContext(conversationId);
const fields = clarification.fields ?? [];
const initialAnswers = Object.fromEntries(
@@ -65,18 +60,6 @@ export default function ClarificationForm({
const handleSubmit = async (): Promise<void> => {
setSubmitted(true);
// Approximate queryLength as the JSON encoding of the form answers — the
// clarification API doesn't render a single user-visible string, but the
// JSON size is a reasonable stand-in for "how much did the user provide".
const queryLength = JSON.stringify(answers).length;
void logEvent(AIAssistantEvents.MessageSent, {
threadId,
page,
mode,
queryLength,
hasContext: false,
respondingToClarification: true,
});
await submitClarification(
conversationId,
clarification.clarificationId,
@@ -86,10 +69,6 @@ export default function ClarificationForm({
const handleCancel = (): void => {
setCancelled(true);
void logEvent(AIAssistantEvents.CancelClicked, {
threadId,
secondsSinceStart: null,
});
cancelStream(conversationId);
};

View File

@@ -5,9 +5,6 @@ import { Input } from '@signozhq/ui/input';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Plus, Search } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from '../../events';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import { Conversation } from '../../types';
import { useVariant } from '../../VariantContext';
@@ -139,17 +136,6 @@ export default function ConversationsList({
const handleSelect = (id: string): void => {
const conv = conversations[id];
// Skip re-selecting the currently active thread — Notion-style click on
// the highlighted row in the history list shouldn't inflate the funnel.
const isReselectingActive = id === activeConversationId;
if (conv?.threadId && !isReselectingActive) {
void logEvent(AIAssistantEvents.ThreadOpenedFromHistory, {
threadId: conv.threadId,
threadAgeDays: Math.floor(
(Date.now() - conv.createdAt) / (24 * 60 * 60 * 1000),
),
});
}
if (conv?.threadId) {
// Always load from backend — refreshes messages and reconnects
// to active execution if the thread is still busy.

View File

@@ -144,7 +144,7 @@ export default function MessageBubble({
)}
{!isUser && message.actions && message.actions.length > 0 && (
<ActionsSection actions={message.actions} messageId={message.id} />
<ActionsSection actions={message.actions} />
)}
</div>
</div>

View File

@@ -8,10 +8,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { Check, Copy, RefreshCw, ThumbsDown, ThumbsUp } from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import { FeedbackRating, Message } from '../../types';
@@ -58,7 +54,6 @@ export default function MessageFeedback({
const submitMessageFeedback = useAIAssistantStore(
(s) => s.submitMessageFeedback,
);
const { threadId } = useAIAssistantAnalyticsContext();
const { formatTimezoneAdjustedTimestamp } = useTimezone();
@@ -96,21 +91,10 @@ export default function MessageFeedback({
}, [message.createdAt]);
const handleCopy = useCallback((): void => {
void logEvent(AIAssistantEvents.MessageCopied, {
role: message.role,
messageId: message.id,
hadToolCalls: Boolean(message.blocks?.some((b) => b.type === 'tool_call')),
});
copyToClipboard(message.content);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, [
copyToClipboard,
message.content,
message.id,
message.role,
message.blocks,
]);
}, [copyToClipboard, message.content]);
const handleVote = useCallback(
(rating: FeedbackRating): void => {
@@ -123,31 +107,20 @@ export default function MessageFeedback({
return;
}
setVote(rating);
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
messageId: message.id,
threadId,
rating: 'up',
hasComment: false,
commentLength: 0,
});
submitMessageFeedback(message.id, rating);
},
[vote, message.id, submitMessageFeedback, threadId],
[vote, message.id, submitMessageFeedback],
);
const handleSubmitNegative = useCallback((): void => {
setVote('negative');
setIsNegativeDialogOpen(false);
const trimmed = negativeComment.trim();
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
messageId: message.id,
threadId,
rating: 'down',
hasComment: trimmed.length > 0,
commentLength: trimmed.length,
});
submitMessageFeedback(message.id, 'negative', trimmed || undefined);
}, [message.id, negativeComment, submitMessageFeedback, threadId]);
submitMessageFeedback(
message.id,
'negative',
negativeComment.trim() || undefined,
);
}, [message.id, negativeComment, submitMessageFeedback]);
return (
<>

View File

@@ -4,9 +4,6 @@ import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Check, Copy } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from '../../events';
import { Message } from '../../types';
import styles from './UserMessageActions.module.scss';
@@ -28,15 +25,10 @@ export default function UserMessageActions({
const [, copyToClipboard] = useCopyToClipboard();
const handleCopy = useCallback((): void => {
void logEvent(AIAssistantEvents.MessageCopied, {
role: message.role,
messageId: message.id,
hadToolCalls: false,
});
copyToClipboard(message.content);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, [copyToClipboard, message.content, message.id, message.role]);
}, [copyToClipboard, message.content]);
return (
<div className={styles.actions}>

View File

@@ -10,10 +10,6 @@ import {
Sparkles,
} from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import { Message, StreamingEventItem } from '../../types';
import MessageBubble from '../MessageBubble';
@@ -50,24 +46,17 @@ interface VirtualizedMessagesProps {
conversationId: string;
messages: Message[];
isStreaming: boolean;
/**
* Called when a user clicks an empty-state suggested prompt. Routed
* through the parent so analytics (Message sent) fire with the same
* page/mode/context attribution as a normal send.
*/
onSendSuggestedPrompt: (text: string) => void;
}
export default function VirtualizedMessages({
conversationId,
messages,
isStreaming,
onSendSuggestedPrompt,
}: VirtualizedMessagesProps): JSX.Element {
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const regenerateAssistantMessage = useAIAssistantStore(
(s) => s.regenerateAssistantMessage,
);
const { threadId } = useAIAssistantAnalyticsContext(conversationId);
const streamingStatus = useAIAssistantStore(
(s) => s.streams[conversationId]?.streamingStatus ?? '',
);
@@ -96,13 +85,9 @@ export default function VirtualizedMessages({
if (isStreaming) {
return;
}
void logEvent(AIAssistantEvents.RegenerateClicked, {
messageId,
threadId,
});
void regenerateAssistantMessage(conversationId, messageId);
},
[conversationId, isStreaming, regenerateAssistantMessage, threadId],
[conversationId, isStreaming, regenerateAssistantMessage],
);
// Scroll all the way to the actual bottom — including the 64px of bottom
@@ -161,11 +146,7 @@ export default function VirtualizedMessages({
color="secondary"
className={styles.emptyChip}
onClick={(): void => {
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
promptId: s.text,
category: SuggestedPromptCategory.EmptyState,
});
onSendSuggestedPrompt(s.text);
sendMessage(s.text);
}}
prefix={<s.icon size={14} />}
>

View File

@@ -1,10 +1,7 @@
import cx from 'classnames';
import { Button } from '@signozhq/ui/button';
import logEvent from 'api/common/logEvent';
import { Check, X } from '@signozhq/icons';
import { AIAssistantEvents } from '../../../events';
import { useAIAssistantAnalyticsContext } from '../../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../../store/useAIAssistantStore';
import { useMessageContext } from '../../MessageContext';
@@ -40,7 +37,6 @@ export default function ConfirmBlock({
const answeredBlocks = useAIAssistantStore((s) => s.answeredBlocks);
const markBlockAnswered = useAIAssistantStore((s) => s.markBlockAnswered);
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
// Durable answered state — survives re-renders/remounts
const answeredChoice = messageId ? answeredBlocks[messageId] : undefined;
@@ -51,14 +47,6 @@ export default function ConfirmBlock({
if (messageId) {
markBlockAnswered(messageId, choice);
}
void logEvent(AIAssistantEvents.MessageSent, {
threadId,
page,
mode,
queryLength: responseText.length,
hasContext: false,
respondingToClarification: false,
});
sendMessage(responseText);
};

View File

@@ -1,11 +1,8 @@
import { useState } from 'react';
import cx from 'classnames';
import { Button } from '@signozhq/ui/button';
import logEvent from 'api/common/logEvent';
import { Checkbox, Radio } from 'antd';
import { AIAssistantEvents } from '../../../events';
import { useAIAssistantAnalyticsContext } from '../../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../../store/useAIAssistantStore';
import { useMessageContext } from '../../MessageContext';
@@ -39,7 +36,6 @@ export default function InteractiveQuestion({
const answeredBlocks = useAIAssistantStore((s) => s.answeredBlocks);
const markBlockAnswered = useAIAssistantStore((s) => s.markBlockAnswered);
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
// Persist selected state locally only for the pending (not-yet-submitted) case
const [selected, setSelected] = useState<string[]>([]);
@@ -56,14 +52,6 @@ export default function InteractiveQuestion({
if (messageId) {
markBlockAnswered(messageId, answer);
}
void logEvent(AIAssistantEvents.MessageSent, {
threadId,
page,
mode,
queryLength: answer.length,
hasContext: false,
respondingToClarification: false,
});
sendMessage(answer);
};

View File

@@ -1,81 +0,0 @@
/**
* Analytics event names for the AI Assistant feature. Backend-emitted events
* (Execution finished, Approval resolved, Resource mutated, Clarification
* requested, Limit hit) are not declared here — they fire from the AI service.
*/
export interface BrowserInfo {
browserName: string;
browserVersion: string;
}
type NavigatorWithBrandHints = Navigator & {
userAgentData?: { brands: { brand: string; version: string }[] };
brave?: { isBrave: () => Promise<boolean> };
};
/**
* We mainly need to distinguish Chrome / Edge (Speech API works) from Chromium
* derivatives (no Google API key → voice fails with `network`). UA sniffing is
* the source of truth for derivative identification; `userAgentData` is used
* only as a fast happy path for Chrome / Edge. Brave needs its own probe — it
* advertises Chrome in both UA and brand hints.
*/
export function getBrowserInfo(): BrowserInfo {
if (typeof navigator === 'undefined') {
return { browserName: 'unknown', browserVersion: 'unknown' };
}
const nav = navigator as NavigatorWithBrandHints;
const ua = nav.userAgent;
// Order matters: derivatives put "Chrome" in their UA; Chrome puts "Safari".
const matchers: { name: string; re: RegExp }[] = [
{ name: 'Edge', re: /Edg(?:e|A|iOS)?\/([\d.]+)/ },
{ name: 'Opera', re: /OPR\/([\d.]+)/ },
{ name: 'Vivaldi', re: /Vivaldi\/([\d.]+)/ },
{ name: 'Chrome', re: /Chrome\/([\d.]+)/ },
{ name: 'Firefox', re: /Firefox\/([\d.]+)/ },
{ name: 'Safari', re: /Version\/([\d.]+).*Safari/ },
];
let browserName = 'unknown';
let browserVersion = 'unknown';
for (const { name, re } of matchers) {
const m = ua.match(re);
if (m) {
browserName = name;
browserVersion = m[1];
break;
}
}
// Brave hides as Chrome in UA + brand hints; its probe is the only tell.
if (nav.brave?.isBrave) {
browserName = 'Brave';
}
return { browserName, browserVersion };
}
export const SuggestedPromptCategory = {
FollowUp: 'follow_up',
EmptyState: 'empty_state',
} as const;
export type SuggestedPromptCategory =
(typeof SuggestedPromptCategory)[keyof typeof SuggestedPromptCategory];
export enum AIAssistantEvents {
Opened = 'AI Assistant: Opened',
MessageSent = 'AI Assistant: Message sent',
SuggestedPromptClicked = 'AI Assistant: Suggested prompt clicked',
CancelClicked = 'AI Assistant: Cancel clicked',
RegenerateClicked = 'AI Assistant: Regenerate clicked',
MessageCopied = 'AI Assistant: Message copied',
FeedbackSubmitted = 'AI Assistant: Feedback submitted',
ResourceOpened = 'AI Assistant: Resource opened',
DocOpened = 'AI Assistant: Doc opened',
ApplyFilterClicked = 'AI Assistant: Apply filter clicked',
ThreadOpenedFromHistory = 'AI Assistant: Thread opened from history',
VoiceInputUsed = 'AI Assistant: Voice input used',
VoiceInputFailed = 'AI Assistant: Voice input failed',
NewChatClicked = 'AI Assistant: New chat clicked',
}

View File

@@ -1,60 +0,0 @@
import { matchPath, useLocation } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { useAIAssistantStore } from '../store/useAIAssistantStore';
import { useVariant } from '../VariantContext';
export interface AIAssistantAnalyticsContext {
/** Backend thread ID for the resolved conversation; undefined before the first send. */
threadId: string | undefined;
/**
* Normalised route template for the current page (e.g. `/dashboard/:dashboardId`).
* Falls back to the raw pathname for routes not in ROUTES. We normalise to keep
* analytics cardinality bounded and avoid leaking customer identifiers
* (dashboard IDs, service names, trace IDs, conversation IDs) into the event.
*/
page: string;
/** Surface the assistant is rendered on. `panel` / `modal` collapse to `sidepane`. */
mode: 'sidepane' | 'full_screen';
}
// Pre-sorted longest-first so more specific templates match before their
// less specific siblings (e.g. `/services/:s/top-level-operations` wins
// over `/services/:s`). Module-level — ROUTES is static.
const ROUTE_TEMPLATES = Object.values(ROUTES).sort(
(a, b) => b.length - a.length,
);
export function normalizePage(pathname: string): string {
for (const template of ROUTE_TEMPLATES) {
if (matchPath(pathname, { path: template, exact: true })) {
return template;
}
}
return pathname;
}
/**
* Shared base attributes for AI Assistant analytics events (Message sent,
* Cancel clicked, Feedback submitted, Resource/Doc/Apply filter, …).
*
* Pass `conversationId` when the caller is scoped to a specific
* conversation (e.g. `ClarificationForm`, `VirtualizedMessages`); omit
* to fall back to the store's active conversation.
*/
export function useAIAssistantAnalyticsContext(
conversationId?: string,
): AIAssistantAnalyticsContext {
const { pathname } = useLocation();
const variant = useVariant();
const threadId = useAIAssistantStore((s) => {
const id = conversationId ?? s.activeConversationId;
return id ? s.conversations[id]?.threadId : undefined;
});
return {
threadId,
page: normalizePage(pathname),
mode: variant === 'page' ? 'full_screen' : 'sidepane',
};
}

View File

@@ -438,7 +438,9 @@ function MultiIngestionSettings(): JSX.Element {
data: {
name: values.name,
tags: updatedTags,
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
expires_at: new Date(
dayjs(values.expires_at).endOf('day').toISOString(),
),
},
},
{
@@ -469,11 +471,13 @@ function MultiIngestionSettings(): JSX.Element {
const requestPayload = {
name: values.name,
tags: updatedTags,
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
expires_at: new Date(dayjs(values.expires_at).endOf('day').toISOString()),
};
createIngestionKey(
{ data: requestPayload },
{
data: requestPayload,
},
{
onSuccess: (_data) => {
notifications.success({

View File

@@ -79,12 +79,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: 'Key One',
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret',
workspace_id: TEST_WORKSPACE_ID,
id: 'k1',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [
{
@@ -160,12 +160,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: 'Key Logs',
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret',
workspace_id: TEST_WORKSPACE_ID,
id: 'k2',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [
{
@@ -238,12 +238,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: KEY_NAME,
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret',
workspace_id: TEST_WORKSPACE_ID,
id: 'k1',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [
{
@@ -299,12 +299,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: 'Key Regular',
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret1',
workspace_id: TEST_WORKSPACE_ID,
id: 'k1',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [],
},
@@ -319,12 +319,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: 'Key Search Result',
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret2',
workspace_id: TEST_WORKSPACE_ID,
id: 'k2',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [],
},

View File

@@ -13,9 +13,9 @@ describe('filterAlerts', () => {
const mockAlertBase: Partial<RuletypesRuleDTO> = {
state: 'active' as RuletypesAlertStateDTO,
disabled: false,
createdAt: '2024-01-01T00:00:00Z',
createdAt: new Date('2024-01-01T00:00:00Z'),
createdBy: 'test-user',
updatedAt: '2024-01-01T00:00:00Z',
updatedAt: new Date('2024-01-01T00:00:00Z'),
updatedBy: 'test-user',
version: '1',
condition: {

View File

@@ -20,7 +20,7 @@ const mockUsers: TypesUserDTO[] = [
displayName: 'Alice Smith',
email: 'alice@signoz.io',
status: 'active',
createdAt: '2024-01-01T00:00:00.000Z',
createdAt: new Date('2024-01-01T00:00:00.000Z'),
orgId: 'org-1',
},
{
@@ -28,7 +28,7 @@ const mockUsers: TypesUserDTO[] = [
displayName: 'Bob Jones',
email: 'bob@signoz.io',
status: 'active',
createdAt: '2024-01-02T00:00:00.000Z',
createdAt: new Date('2024-01-02T00:00:00.000Z'),
orgId: 'org-1',
},
{
@@ -36,7 +36,7 @@ const mockUsers: TypesUserDTO[] = [
displayName: '',
email: 'charlie@signoz.io',
status: 'pending_invite',
createdAt: '2024-01-03T00:00:00.000Z',
createdAt: new Date('2024-01-03T00:00:00.000Z'),
orgId: 'org-1',
},
{
@@ -44,7 +44,7 @@ const mockUsers: TypesUserDTO[] = [
displayName: 'Dave Deleted',
email: 'dave@signoz.io',
status: 'deleted',
createdAt: '2024-01-04T00:00:00.000Z',
createdAt: new Date('2024-01-04T00:00:00.000Z'),
orgId: 'org-1',
},
];

View File

@@ -1,53 +0,0 @@
import { SolidAlertTriangle } from '@signozhq/icons';
import { ConfirmDialog } from '@signozhq/ui/dialog';
import { Typography } from '@signozhq/ui/typography';
export interface DiscardChangesModalProps {
open: boolean;
isNewPanel: boolean;
panelTitle?: string;
dashboardTitle?: string;
onDiscard: () => void;
onClose: () => void;
}
export default function DiscardChangesModal({
open,
isNewPanel,
panelTitle,
dashboardTitle,
onDiscard,
onClose,
}: DiscardChangesModalProps): JSX.Element {
const dashboardName = dashboardTitle ? (
<>
{' '}
to <strong>{dashboardTitle}</strong>
</>
) : null;
const panelLabel = panelTitle ? <strong>{panelTitle}</strong> : 'this panel';
return (
<ConfirmDialog
open={open}
onOpenChange={(next): void => {
if (!next) {
onClose();
}
}}
title="Discard changes?"
titleIcon={<SolidAlertTriangle size={14} color="#fdd600" />}
confirmText="Discard"
confirmColor="destructive"
cancelText="Keep editing"
onConfirm={onDiscard}
onCancel={onClose}
>
{isNewPanel ? (
<Typography>This new panel won&apos;t be added{dashboardName}.</Typography>
) : (
<Typography>Your unsaved edits to {panelLabel} will be lost.</Typography>
)}
</ConfirmDialog>
);
}

View File

@@ -1,17 +1,13 @@
import {
initialAutocompleteData,
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { cloneDeep } from 'lodash-es';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import type { PartialPanelTypes } from '../utils';
import { getIsQueryModified, handleQueryChange } from '../utils';
import { handleQueryChange } from '../utils';
const buildSupersetQuery = (extras?: Record<string, unknown>): Query => ({
queryType: EQueryType.QUERY_BUILDER,
@@ -41,128 +37,6 @@ const buildSupersetQuery = (extras?: Record<string, unknown>): Query => ({
},
});
const buildMetricsQuery = (
overrides?: Partial<{
metricName: string;
aggregateAttributeKey: string;
legend: string;
groupByKey: string;
}>,
): Query => ({
queryType: EQueryType.QUERY_BUILDER,
promql: [],
clickhouse_sql: [],
id: 'query-id',
unit: '',
builder: {
queryFormulas: [],
queryData: [
{
...initialQueryBuilderFormValuesMap[DataSource.METRICS],
queryName: 'A',
aggregateAttribute: overrides?.aggregateAttributeKey
? {
...initialAutocompleteData,
key: overrides.aggregateAttributeKey,
type: 'tag',
dataType: DataTypes.Float64,
}
: cloneDeep(initialAutocompleteData),
aggregations: [
{
metricName: overrides?.metricName ?? 'system.cpu.load',
temporality: '',
timeAggregation: 'rate',
spaceAggregation: 'sum',
reduceTo: 'avg',
} as MetricAggregation,
],
legend: overrides?.legend ?? '',
groupBy: overrides?.groupByKey
? [
{
...initialAutocompleteData,
key: overrides.groupByKey,
type: 'tag',
dataType: DataTypes.String,
},
]
: [],
},
],
queryTraceOperator: [],
},
});
describe('getIsQueryModified', () => {
it('returns false when baseline is null (new unsaved panel with no edits anchor)', () => {
const current = buildMetricsQuery();
expect(getIsQueryModified(current, null)).toBe(false);
});
it('returns false when baseline is undefined', () => {
const current = buildMetricsQuery();
expect(getIsQueryModified(current, undefined)).toBe(false);
});
it('returns false when current only differs by auto-backfilled aggregateAttribute', () => {
// saved widget query: aggregateAttribute is the v5-style empty initial value
// (stripped from persisted spec; spread back in as initialAutocompleteData on load)
const savedQuery = buildMetricsQuery({ metricName: 'system.cpu.load' });
// after MetricNameSelector edit-mode backfill, currentQuery has the populated
// aggregateAttribute while the rest of the query is identical
const currentQuery = buildMetricsQuery({
metricName: 'system.cpu.load',
aggregateAttributeKey: 'system.cpu.load',
});
expect(getIsQueryModified(currentQuery, savedQuery)).toBe(false);
});
it('returns true when the user edits the legend', () => {
const baseline = buildMetricsQuery({ metricName: 'system.cpu.load' });
const edited = buildMetricsQuery({
metricName: 'system.cpu.load',
legend: 'cpu-load',
});
expect(getIsQueryModified(edited, baseline)).toBe(true);
});
it('returns true when the user picks a different metric (aggregations diverges)', () => {
const baseline = buildMetricsQuery({ metricName: 'system.cpu.load' });
const edited = buildMetricsQuery({ metricName: 'system.memory.usage' });
expect(getIsQueryModified(edited, baseline)).toBe(true);
});
it('returns true when the user adds a groupBy', () => {
const baseline = buildMetricsQuery({ metricName: 'system.cpu.load' });
const edited = buildMetricsQuery({
metricName: 'system.cpu.load',
groupByKey: 'host.name',
});
expect(getIsQueryModified(edited, baseline)).toBe(true);
});
it('returns true on existing widget when current diverges from saved (Stage-and-Run silent-loss flow)', () => {
// After Edit → Stage and Run, stagedQuery is reset to match currentQuery.
// The dirty check must compare against the SAVED widget query, not stagedQuery.
const savedQuery = buildMetricsQuery({ metricName: 'system.cpu.load' });
const currentQuery = buildMetricsQuery({ metricName: 'system.memory.usage' });
expect(getIsQueryModified(currentQuery, savedQuery)).toBe(true);
});
it('returns false for a new panel where currentQuery still matches stagedQuery baseline', () => {
const stagedQuery = buildMetricsQuery();
const currentQuery = buildMetricsQuery();
expect(getIsQueryModified(currentQuery, stagedQuery)).toBe(false);
});
it('returns true for a new panel where currentQuery has been edited away from stagedQuery', () => {
const stagedQuery = buildMetricsQuery();
const currentQuery = buildMetricsQuery({ legend: 'custom' });
expect(getIsQueryModified(currentQuery, stagedQuery)).toBe(true);
});
});
describe('handleQueryChange', () => {
it('sets list-specific fields when switching to LIST', () => {
const superset = buildSupersetQuery();

View File

@@ -1,17 +1,18 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { Check, X } from '@signozhq/icons';
import { Check, SolidAlertTriangle, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@signozhq/ui/resizable';
import { Flex } from 'antd';
import { Flex, Modal, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
@@ -68,6 +69,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType, getGraphTypeForFormat } from 'utils/getGraphType';
import LeftContainer from './LeftContainer';
import QueryTypeTag from './LeftContainer/QueryTypeTag';
import RightContainer from './RightContainer';
import { ThresholdProps } from './RightContainer/Threshold/types';
import TimeItems, { timePreferance } from './RightContainer/timeItems';
@@ -80,7 +82,6 @@ import {
placeWidgetAtBottom,
placeWidgetBetweenRows,
} from './utils';
import DiscardChangesModal from './WidgetModals/DiscardChangesModal';
import './NewWidget.styles.scss';
@@ -97,6 +98,8 @@ function NewWidget({
const { dashboardVariables } = useDashboardVariables();
const { t } = useTranslation(['dashboard']);
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const {
@@ -107,6 +110,11 @@ function NewWidget({
setSupersetQuery,
} = useQueryBuilder();
const isQueryModified = useMemo(
() => getIsQueryModified(currentQuery, stagedQuery),
[currentQuery, stagedQuery],
);
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
@@ -131,23 +139,6 @@ function NewWidget({
const query = useUrlQuery();
// For existing widgets, compare currentQuery against the saved widget query
// (stable across Stage-and-Run cycles). For new panels with no saved baseline,
// fall back to stagedQuery so initial edits still trigger the warning.
const savedWidgetQuery = useMemo(() => {
const widgetId = query.get('widgetId');
const match = widgets?.find((w) => w.id === widgetId);
if (!match || match.panelTypes === PANEL_GROUP_TYPES.ROW) {
return null;
}
return (match as Widgets).query ?? null;
}, [widgets, query]);
const isQueryModified = useMemo(
() => getIsQueryModified(currentQuery, savedWidgetQuery ?? stagedQuery),
[currentQuery, savedWidgetQuery, stagedQuery],
);
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
const logEventCalledRef = useRef(false);
@@ -237,6 +228,7 @@ function NewWidget({
Record<string, string>
>(selectedWidget?.customLegendColors || {});
const [saveModal, setSaveModal] = useState(false);
const [discardModal, setDiscardModal] = useState(false);
const [bucketWidth, setBucketWidth] = useState<number>(
@@ -348,6 +340,7 @@ function NewWidget({
]);
const closeModal = (): void => {
setSaveModal(false);
setDiscardModal(false);
};
@@ -600,7 +593,7 @@ function NewWidget({
},
};
return updateDashboardMutation.mutateAsync(dashboard, {
updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: () => {
setToScrollWidgetId(selectedWidget?.id || '');
navigateToDashboardPage();
@@ -695,9 +688,9 @@ function NewWidget({
})),
}),
});
onClickSaveHandler();
setSaveModal(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onClickSaveHandler]);
}, [isNewPanel]);
const isNewTraceLogsAvailable =
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
@@ -958,14 +951,57 @@ function NewWidget({
</ResizablePanel>
</ResizablePanelGroup>
</PanelContainer>
<DiscardChangesModal
<Modal
title={
isQueryModified ? (
<Space>
<SolidAlertTriangle size={16} color="#fdd600" />
Unsaved Changes
</Space>
) : (
'Save Widget'
)
}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={closeModal}
onOk={onClickSaveHandler}
confirmLoading={updateDashboardMutation.isLoading}
centered
open={saveModal}
width={600}
>
{!isQueryModified ? (
<Typography>
{t('your_graph_build_with')}{' '}
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
{t('dashboard_ok_confirm')}
</Typography>
) : (
<Typography>{t('dashboard_unsave_changes')} </Typography>
)}
</Modal>
<Modal
title={
<Space>
<SolidAlertTriangle size={16} color="#fdd600" />
Unsaved Changes
</Space>
}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={closeModal}
onOk={discardChanges}
centered
open={discardModal}
isNewPanel={isNewPanel}
panelTitle={title}
dashboardTitle={dashboardData?.data?.title}
onDiscard={discardChanges}
onClose={closeModal}
/>
width={600}
>
<Typography>{t('dashboard_unsave_changes')}</Typography>
</Modal>
</Container>
);
}

View File

@@ -1,4 +1,5 @@
import { Layout } from 'react-grid-layout';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import { PrecisionOptionsEnum } from 'components/Graph/types';
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
import {
@@ -25,84 +26,16 @@ import { DataSource } from 'types/common/queryBuilder';
import { getCategoryName } from './RightContainer/dataFormatCategories';
// Asks "would saving the current panel change the persisted widget spec?".
//
// `adjustQueryForV5` is deliberately not reused here: in addition to stripping
// the legacy v4 fields, it also resurrects them onto each metric
// `aggregations[i]`. That migration step is correct on save but bleeds
// asymmetrically across a comparator — the live query still carries the
// legacy defaults from `initialQueryBuilderFormValuesMap` while a previously
// saved widget had them stripped.
const stripQueryDataForCompare = (
queryData: IBuilderQuery,
): Record<string, unknown> => {
const {
aggregateAttribute: _aggregateAttribute,
aggregateOperator: _aggregateOperator,
timeAggregation: _timeAggregation,
spaceAggregation: _spaceAggregation,
reduceTo: _reduceTo,
filters: _filters,
...retained
} = queryData ?? ({} as IBuilderQuery);
const groupBy = (retained.groupBy ?? []).map((entry) => {
const { id: _id, ...rest } = entry;
return rest;
});
return {
...retained,
groupBy,
source: retained.source || '',
};
};
const normalizeForDirtyCheck = (query: Query): Record<string, unknown> => {
const { id: _id, unit, builder, ...rest } = query;
return {
...rest,
// `id` is regenerated on every Stage and Run; `unit` flips between ''
// and undefined depending on whether the user has touched the selector.
unit: unit || '',
builder: {
...builder,
queryData: (builder?.queryData ?? []).map(stripQueryDataForCompare),
},
};
};
// `lodash.isEqual` distinguishes `{a: undefined}` from `{}`; for the dirty
// check those are the same. Initial-values spreads on the live query
// frequently leave such explicit-undefined keys.
const stripUndefined = (value: unknown): unknown => {
if (Array.isArray(value)) {
return value.map(stripUndefined);
}
if (value && typeof value === 'object') {
const out: Record<string, unknown> = {};
Object.entries(value as Record<string, unknown>).forEach(([k, v]) => {
if (v === undefined) {
return;
}
out[k] = stripUndefined(v);
});
return out;
}
return value;
};
export const getIsQueryModified = (
currentQuery: Query,
baselineQuery: Query | null | undefined,
stagedQuery: Query | null,
): boolean => {
if (!baselineQuery) {
if (!stagedQuery) {
return false;
}
return !isEqual(
stripUndefined(normalizeForDirtyCheck(baselineQuery)),
stripUndefined(normalizeForDirtyCheck(currentQuery)),
);
const omitIdFromStageQuery = omitIdFromQuery(stagedQuery);
const omitIdFromCurrentQuery = omitIdFromQuery(currentQuery);
return !isEqual(omitIdFromStageQuery, omitIdFromCurrentQuery);
};
export type PartialPanelTypes = {

View File

@@ -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';
@@ -24,7 +24,7 @@ import { PlannedDowntimeDeleteModal } from './PlannedDowntimeDeleteModal';
import { PlannedDowntimeForm } from './PlannedDowntimeForm';
import { PlannedDowntimeList } from './PlannedDowntimeList';
import {
defaultInitialValues,
defautlInitialValues,
deleteDowntimeHandler,
} from './PlannedDowntimeutils';
@@ -48,8 +48,8 @@ export function PlannedDowntime(): JSX.Element {
const urlQuery = useUrlQuery();
const [initialValues, setInitialValues] =
useState<Partial<AlertmanagertypesPlannedMaintenanceDTO>>(
defaultInitialValues,
useState<Partial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>>(
defautlInitialValues,
);
const downtimeSchedules = useListDowntimeSchedules();
@@ -148,7 +148,7 @@ export function PlannedDowntime(): JSX.Element {
<Button
type="primary"
onClick={(): void => {
setInitialValues(defaultInitialValues);
setInitialValues({ ...defautlInitialValues, editMode: false });
setIsOpen(true);
setEditMode(false);
form.resetFields();

View File

@@ -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';
@@ -46,6 +46,8 @@ import { AlertRuleTags } from './PlannedDowntimeList';
import {
getAlertOptionsFromIds,
getDurationInfo,
getEndTime,
handleTimeConversion,
isScheduleRecurring,
recurrenceOptions,
recurrenceOptionWithSubmenu,
@@ -62,28 +64,24 @@ const TIME_FORMAT = DATE_TIME_FORMATS.TIME;
const DATE_FORMAT = DATE_TIME_FORMATS.ORDINAL_DATE;
const ORDINAL_FORMAT = DATE_TIME_FORMATS.ORDINAL_ONLY;
const TZ_OPTIONS: DefaultOptionType[] = ALL_TIME_ZONES.map(
(timezone: string) => ({
label: timezone,
value: timezone,
key: timezone,
}),
);
interface PlannedDowntimeFormData {
name: string;
startTime: dayjs.Dayjs | null;
endTime: dayjs.Dayjs | null;
recurrence?: AlertmanagertypesRecurrenceDTO;
startTime: dayjs.Dayjs | string;
endTime: dayjs.Dayjs | string;
recurrence?: RuletypesRecurrenceDTO | null;
alertRules: DefaultOptionType[];
recurrenceSelect?: AlertmanagertypesRecurrenceDTO;
recurrenceSelect?: RuletypesRecurrenceDTO;
timezone?: string;
}
const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
interface PlannedDowntimeFormProps {
initialValues: Partial<AlertmanagertypesPlannedMaintenanceDTO>;
initialValues: Partial<
RuletypesPlannedMaintenanceDTO & {
editMode: boolean;
}
>;
alertOptions: DefaultOptionType[];
isError: boolean;
isLoading: boolean;
@@ -91,7 +89,7 @@ interface PlannedDowntimeFormProps {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
refetchAllSchedules: () => void;
isEditMode: boolean;
form: FormInstance;
form: FormInstance<any>;
}
export function PlannedDowntimeForm(
@@ -109,46 +107,66 @@ export function PlannedDowntimeForm(
form,
} = props;
const [selectedTags, setSelectedTags] = React.useState<DefaultOptionType[]>(
[],
);
const [selectedTags, setSelectedTags] = React.useState<
DefaultOptionType | DefaultOptionType[]
>([]);
const alertRuleFormName = 'alertRules';
const [saveLoading, setSaveLoading] = useState(false);
const [durationUnit, setDurationUnit] = useState<string>(
getDurationInfo(initialValues.schedule?.recurrence?.duration)?.unit || 'm',
getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
?.unit || 'm',
);
const [formData, setFormData] = useState<Partial<PlannedDowntimeFormData>>({
timezone: initialValues.schedule?.timezone,
});
const [recurrenceType, setRecurrenceType] = useState<string>(
initialValues.schedule?.recurrence?.repeatType ||
const [recurrenceType, setRecurrenceType] = useState<string | null>(
(initialValues.schedule?.recurrence?.repeatType as string) ||
recurrenceOptions.doesNotRepeat.value,
);
const timezoneInitialValue = !isEmpty(initialValues.schedule?.timezone)
? (initialValues.schedule?.timezone as string)
: undefined;
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const requiredFieldRule = [{ required: true }];
const datePickerFooter = (mode: any): any =>
mode === 'time' ? (
<span style={{ color: 'gray' }}>Please select the time</span>
) : null;
const saveHandler = useCallback(
const saveHanlder = useCallback(
async (values: PlannedDowntimeFormData) => {
const data: AlertmanagertypesPostablePlannedMaintenanceDTO = {
const shouldKeepLocalTime = !isEditMode;
const data: RuletypesPostablePlannedMaintenanceDTO = {
alertIds: values.alertRules
.map((alert) => alert.value)
.filter((alert) => alert !== undefined) as string[],
name: values.name,
schedule: {
startTime: values.startTime?.format(),
endTime: values.endTime?.format(),
timezone: values.timezone!,
recurrence: values.recurrence,
startTime: new Date(
handleTimeConversion(
values.startTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
),
timezone: values.timezone as string,
endTime: values.endTime
? new Date(
handleTimeConversion(
values.endTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
)
: undefined,
recurrence: values.recurrence as RuletypesRecurrenceDTO,
},
};
@@ -180,58 +198,50 @@ export function PlannedDowntimeForm(
notifications,
refetchAllSchedules,
setIsOpen,
timezoneInitialValue,
showErrorModal,
],
);
const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => {
const { recurrence } = values;
const recurrenceData =
!recurrence ||
recurrence.repeatType === recurrenceOptions.doesNotRepeat.value
values?.recurrence?.repeatType === recurrenceOptions.doesNotRepeat.value
? undefined
: {
duration: recurrence.duration
? `${recurrence.duration}${durationUnit}`
: '',
startTime: values.startTime!.format(),
endTime: values.endTime?.format(),
repeatOn: recurrence.repeatOn,
repeatType: recurrence.repeatType,
duration: values.recurrence?.duration
? `${values.recurrence?.duration}${durationUnit}`
: undefined,
endTime: !isEmpty(values.endTime)
? handleTimeConversion(
values.endTime,
timezoneInitialValue,
values.timezone,
!isEditMode,
)
: undefined,
startTime: handleTimeConversion(
values.startTime,
timezoneInitialValue,
values.timezone,
!isEditMode,
),
repeatOn: !values.recurrence?.repeatOn?.length
? undefined
: values.recurrence?.repeatOn,
repeatType: values.recurrence?.repeatType,
};
await saveHandler({
const payloadValues = {
...values,
recurrence: recurrenceData,
});
recurrence: recurrenceData as RuletypesRecurrenceDTO | undefined,
};
await saveHanlder(payloadValues);
};
const handleFormData = (data: Partial<PlannedDowntimeFormData>): void => {
const { startTime, endTime, timezone } = data;
const update: Partial<PlannedDowntimeFormData> = {};
// If the set timezone doesn't match, update it.
if (
startTime &&
timezone &&
startTime.format() !== startTime.tz(timezone, true).format()
) {
update.startTime = startTime.tz(timezone, true);
}
if (
endTime &&
timezone &&
endTime.format() !== endTime.tz(timezone, true).format()
) {
update.endTime = endTime.tz(timezone, true);
}
if (!isEmpty(update)) {
data = { ...data, ...update };
form.setFieldsValue({ ...update });
}
setFormData(data);
};
const formValidationRules = [
{
required: true,
},
];
const handleOk = async (): Promise<void> => {
await form.validateFields().catch(() => {
@@ -239,11 +249,16 @@ export function PlannedDowntimeForm(
});
};
const handleCancel = (): void => setIsOpen(false);
const handleCancel = (): void => {
setIsOpen(false);
};
const handleAlertRulesChange: SelectProps['onChange'] = (_value, options) => {
const handleChange = (
_value: string,
options: DefaultOptionType | DefaultOptionType[],
): void => {
form.setFieldValue(alertRuleFormName, options);
setSelectedTags(Array.isArray(options) ? options : [options]);
setSelectedTags(options);
};
const noTagRenderer: SelectProps['tagRender'] = () => <></>;
@@ -252,51 +267,113 @@ export function PlannedDowntimeForm(
if (!removedTag) {
return;
}
const newTags = selectedTags.filter((tag) => tag.value !== removedTag);
const newTags = selectedTags.filter(
(tag: DefaultOptionType) => tag.value !== removedTag,
);
form.setFieldValue(alertRuleFormName, newTags);
setSelectedTags(newTags);
};
const formattedInitialValues = useMemo((): PlannedDowntimeFormData => {
const { schedule } = initialValues;
const startTime = schedule?.recurrence?.startTime || schedule?.startTime;
const endTime = schedule?.recurrence?.endTime || schedule?.endTime;
return {
const formatedInitialValues = useMemo(() => {
const formData: PlannedDowntimeFormData = {
name: defaultTo(initialValues.name, ''),
alertRules: getAlertOptionsFromIds(
initialValues.alertIds || [],
alertOptions,
),
startTime: startTime ? dayjs(startTime).tz(schedule.timezone) : null,
endTime: endTime ? dayjs(endTime).tz(schedule.timezone) : null,
endTime: getEndTime(initialValues) ? dayjs(getEndTime(initialValues)) : '',
startTime: initialValues.schedule?.startTime
? dayjs(initialValues.schedule?.startTime)
: '',
recurrence: {
...schedule?.recurrence,
repeatType: !isScheduleRecurring(schedule)
...initialValues.schedule?.recurrence,
repeatType: (!isScheduleRecurring(initialValues?.schedule)
? recurrenceOptions.doesNotRepeat.value
: schedule?.recurrence?.repeatType,
duration: getDurationInfo(schedule?.recurrence?.duration)?.value ?? '',
} as AlertmanagertypesRecurrenceDTO,
timezone: schedule?.timezone as string,
: initialValues.schedule?.recurrence
?.repeatType) as RuletypesRecurrenceDTO['repeatType'],
duration: String(
getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
?.value ?? '',
),
} as RuletypesRecurrenceDTO,
timezone: initialValues.schedule?.timezone as string,
};
return formData;
}, [initialValues, alertOptions]);
useEffect(() => {
setSelectedTags(formattedInitialValues.alertRules);
form.setFieldsValue({ ...formattedInitialValues });
}, [form, formattedInitialValues, initialValues]);
setSelectedTags(formatedInitialValues.alertRules);
form.setFieldsValue({ ...formatedInitialValues });
}, [form, formatedInitialValues, initialValues]);
const timeZoneItems: DefaultOptionType[] = ALL_TIME_ZONES.map(
(timezone: string) => ({
label: timezone,
value: timezone,
key: timezone,
}),
);
const getTimezoneFormattedTime = (
time: string | dayjs.Dayjs,
timeZone?: string,
isEditMode?: boolean,
format?: string,
): string => {
if (!time) {
return '';
}
if (!timeZone) {
return dayjs(time).format(format);
}
return dayjs(time).tz(timeZone, isEditMode).format(format);
};
const startTimeText = useMemo((): string => {
const startTime = formData.startTime;
let startTime = formData?.startTime;
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
startTime =
(formData?.recurrence?.startTime
? dayjs(formData.recurrence.startTime).toISOString()
: '') ||
formData?.startTime ||
'';
}
if (!startTime) {
return '';
}
const daysOfWeek = formData.recurrence?.repeatOn;
if (formData.timezone) {
startTime = handleTimeConversion(
startTime,
timezoneInitialValue,
formData?.timezone,
!isEditMode,
);
}
const daysOfWeek = formData?.recurrence?.repeatOn;
const formattedStartTime = startTime.format(TIME_FORMAT);
const formattedStartDate = startTime.format(DATE_FORMAT);
const ordinalFormat = startTime.format(ORDINAL_FORMAT);
const formattedStartTime = getTimezoneFormattedTime(
startTime,
formData.timezone,
!isEditMode,
TIME_FORMAT,
);
const formattedStartDate = getTimezoneFormattedTime(
startTime,
formData.timezone,
!isEditMode,
DATE_FORMAT,
);
const ordinalFormat = getTimezoneFormattedTime(
startTime,
formData.timezone,
!isEditMode,
ORDINAL_FORMAT,
);
const formattedDaysOfWeek = daysOfWeek?.join(', ');
switch (recurrenceType) {
@@ -311,18 +388,49 @@ export function PlannedDowntimeForm(
default:
return `Scheduled for ${formattedStartDate} starting at ${formattedStartTime}.`;
}
}, [formData, recurrenceType, timezone]);
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
const endTimeText = useMemo((): string => {
const endTime = formData.endTime;
let endTime = formData?.endTime;
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
endTime =
(formData?.recurrence?.endTime
? dayjs(formData.recurrence.endTime).toISOString()
: '') || '';
if (!isEditMode && !endTime) {
endTime = formData?.endTime || '';
}
}
if (!endTime) {
return '';
}
const formattedEndTime = endTime.format(TIME_FORMAT);
const formattedEndDate = endTime.format(DATE_FORMAT);
if (formData.timezone) {
endTime = handleTimeConversion(
endTime,
timezoneInitialValue,
formData?.timezone,
!isEditMode,
);
}
const formattedEndTime = getTimezoneFormattedTime(
endTime,
formData.timezone,
!isEditMode,
TIME_FORMAT,
);
const formattedEndDate = getTimezoneFormattedTime(
endTime,
formData.timezone,
!isEditMode,
DATE_FORMAT,
);
return `Scheduled to end maintenance on ${formattedEndDate} at ${formattedEndTime}.`;
}, [formData, recurrenceType, timezone]);
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
return (
<Modal
@@ -338,28 +446,33 @@ export function PlannedDowntimeForm(
footer={null}
>
<Form<PlannedDowntimeFormData>
name={isEditMode ? 'edit-form' : 'create-form'}
name={initialValues.editMode ? 'edit-form' : 'create-form'}
form={form}
layout="vertical"
className="createForm"
onFinish={onFinish}
onValuesChange={(): void => {
setRecurrenceType(form.getFieldValue('recurrence')?.repeatType as string);
handleFormData(form.getFieldsValue());
setFormData(form.getFieldsValue());
}}
autoComplete="off"
>
<Form.Item label="Name" name="name" rules={requiredFieldRule}>
<Form.Item label="Name" name="name" rules={formValidationRules}>
<Input placeholder="e.g. Upgrade downtime" />
</Form.Item>
<Form.Item
label="Starts from"
name="startTime"
rules={requiredFieldRule}
rules={formValidationRules}
className={!isEmpty(startTimeText) ? 'formItemWithBullet' : ''}
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
>
<DatePicker
format={(date) => date.format(customFormat)}
format={(date): string =>
dayjs(date).tz(timezoneInitialValue).format(customFormat)
}
showTime
renderExtraFooter={datePickerFooter}
showNow={false}
@@ -372,7 +485,7 @@ export function PlannedDowntimeForm(
<Form.Item
label="Repeats every"
name={['recurrence', 'repeatType']}
rules={requiredFieldRule}
rules={formValidationRules}
>
<Select
placeholder="Select option..."
@@ -383,7 +496,7 @@ export function PlannedDowntimeForm(
<Form.Item
label="Weekly occurernce"
name={['recurrence', 'repeatOn']}
rules={requiredFieldRule}
rules={formValidationRules}
>
<Select
placeholder="Select option..."
@@ -397,14 +510,16 @@ export function PlannedDowntimeForm(
<Form.Item
label="Duration"
name={['recurrence', 'duration']}
rules={requiredFieldRule}
rules={formValidationRules}
>
<Input
addonAfter={
<Select
defaultValue="m"
value={durationUnit}
onChange={(value): void => setDurationUnit(value)}
onChange={(value): void => {
setDurationUnit(value);
}}
>
<Select.Option value="m">Mins</Select.Option>
<Select.Option value="h">Hours</Select.Option>
@@ -418,8 +533,8 @@ export function PlannedDowntimeForm(
/>
</Form.Item>
)}
<Form.Item label="Timezone" name="timezone" rules={requiredFieldRule}>
<Select options={TZ_OPTIONS} placeholder="Select timezone" showSearch />
<Form.Item label="Timezone" name="timezone" rules={formValidationRules}>
<Select options={timeZoneItems} placeholder="Select timezone" showSearch />
</Form.Item>
<Form.Item
label="Ends on"
@@ -431,9 +546,14 @@ export function PlannedDowntimeForm(
},
]}
className={!isEmpty(endTimeText) ? 'formItemWithBullet' : ''}
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
>
<DatePicker
format={(date) => date.format(customFormat)}
format={(date): string =>
dayjs(date).tz(timezoneInitialValue).format(customFormat)
}
showTime
showNow={false}
renderExtraFooter={datePickerFooter}
@@ -464,7 +584,7 @@ export function PlannedDowntimeForm(
status={isError ? 'error' : undefined}
loading={isLoading}
tagRender={noTagRenderer}
onChange={handleAlertRulesChange}
onChange={handleChange}
showSearch
options={alertOptions}
filterOption={(input, option): boolean =>

View File

@@ -1,4 +1,4 @@
import React, { ReactNode, useEffect } from 'react';
import { ReactNode, useEffect } from 'react';
import { UseQueryResult } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Collapse, Flex, Space, Table, TableProps, Tag, Tooltip } from 'antd';
@@ -7,8 +7,8 @@ import type { DefaultOptionType } from 'antd/es/select';
import type {
ListDowntimeSchedules200,
RenderErrorResponseDTO,
AlertmanagertypesPlannedMaintenanceDTO,
AlertmanagertypesScheduleDTO,
RuletypesPlannedMaintenanceDTO,
RuletypesRecurrenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import cx from 'classnames';
@@ -19,11 +19,12 @@ import { CalendarClock, PenLine, Trash2 } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import { showErrorNotification } from 'utils/error';
import { showErrorNotification } from '../../utils/error';
import {
formatDateTime,
getAlertOptionsFromIds,
getDuration,
getEndTime,
recurrenceInfo,
} from './PlannedDowntimeutils';
@@ -125,28 +126,29 @@ export function CollapseListContent({
created_at,
created_by_name,
created_by_email,
schedule,
timeframe,
repeats,
updated_at,
updated_by_name,
alertOptions,
timezone,
}: {
created_at?: string;
created_by_name?: string;
created_by_email?: string;
schedule?: AlertmanagertypesScheduleDTO;
timeframe: [string | undefined | null, string | undefined | null];
repeats?: RuletypesRecurrenceDTO | null;
updated_at?: string;
updated_by_name?: string;
alertOptions?: DefaultOptionType[];
timezone?: string;
}): JSX.Element {
const repeats = schedule?.recurrence;
const renderItems = (title: string, value: ReactNode): JSX.Element => (
<div className="render-item-collapse-list">
<Typography>{title}</Typography>
<div className="render-item-value">{value}</div>
</div>
);
const startTime = formatDateTime(schedule?.startTime, schedule?.timezone);
const endTime = formatDateTime(schedule?.endTime, schedule?.timezone);
return (
<Flex vertical>
@@ -181,20 +183,16 @@ export function CollapseListContent({
{renderItems(
'Timeframe',
schedule?.startTime ? (
<Typography>{`${startTime}${endTime}`}</Typography>
timeframe[0] || timeframe[1] ? (
<Typography>{`${formatDateTime(timeframe[0])}${formatDateTime(
timeframe[1],
)}`}</Typography>
) : (
'-'
),
)}
{renderItems(
'Timezone',
<Typography>{schedule?.timezone || '-'}</Typography>,
)}
{renderItems(
'Repeats',
<Typography>{recurrenceInfo(repeats, schedule?.timezone)}</Typography>,
)}
{renderItems('Timezone', <Typography>{timezone || '-'}</Typography>)}
{renderItems('Repeats', <Typography>{recurrenceInfo(repeats)}</Typography>)}
{renderItems(
'Alerts silenced',
alertOptions?.length ? (
@@ -214,7 +212,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;
@@ -234,12 +232,22 @@ export function CustomCollapseList(
setModalOpen,
handleDeleteDowntime,
setEditMode,
kind,
} = props;
const scheduleTime = schedule?.startTime
? dayjs(schedule.startTime).tz(schedule.timezone)
: createdAt || '';
const formattedDateAndTime = `Start time ⎯ ${formatDateTime(scheduleTime)} ${schedule?.timezone}`;
? dayjs(schedule.startTime).toISOString()
: createdAt
? dayjs(createdAt).toISOString()
: '';
// Combine time and date
const formattedDateAndTime = `Start time ⎯ ${formatDateTime(
defaultTo(scheduleTime, ''),
)} ${schedule?.timezone}`;
const endTime = getEndTime({
kind,
schedule,
} as Partial<RuletypesPlannedMaintenanceDTO>);
return (
<>
@@ -249,16 +257,21 @@ export function CustomCollapseList(
<HeaderComponent
duration={
schedule?.recurrence?.duration
? schedule.recurrence.duration
: getDuration(schedule?.startTime || '', schedule?.endTime || '')
? (schedule?.recurrence?.duration as string)
: getDuration(
schedule?.startTime ? dayjs(schedule.startTime).toISOString() : '',
schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '',
)
}
name={defaultTo(name, '')}
handleEdit={() => {
handleEdit={(): void => {
setInitialValues({ ...props });
setModalOpen(true);
setEditMode(true);
}}
handleDelete={() => handleDeleteDowntime(id ?? '', name || '')}
handleDelete={(): void => {
handleDeleteDowntime(id ?? '', name || '');
}}
/>
}
key={id ?? ''}
@@ -266,10 +279,17 @@ export function CustomCollapseList(
<CollapseListContent
created_at={createdAt ? dayjs(createdAt).toISOString() : ''}
created_by_name={defaultTo(createdBy, '')}
schedule={schedule}
timeframe={[
schedule?.startTime?.toString(),
typeof endTime === 'string' ? endTime : endTime?.toString(),
]}
repeats={
schedule?.recurrence as RuletypesRecurrenceDTO | null | undefined
}
updated_at={updatedAt ? dayjs(updatedAt).toISOString() : ''}
updated_by_name={defaultTo(updatedBy, '')}
alertOptions={alertOptions}
timezone={defaultTo(schedule?.timezone, '')}
/>
</Panel>
</Collapse>
@@ -281,10 +301,9 @@ export function CustomCollapseList(
);
}
export type DowntimeSchedulesTableData =
AlertmanagertypesPlannedMaintenanceDTO & {
alertOptions: DefaultOptionType[];
};
export type DowntimeSchedulesTableData = RuletypesPlannedMaintenanceDTO & {
alertOptions: DefaultOptionType[];
};
export function PlannedDowntimeList({
downtimeSchedules,
@@ -301,7 +320,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;

View File

@@ -5,14 +5,14 @@ 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';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs, { Dayjs } from 'dayjs';
import { isEmpty } from 'lodash-es';
import dayjs from 'dayjs';
import { isEmpty, isEqual } from 'lodash-es';
import APIError from 'types/api/error';
type DateTimeString = string | null | undefined;
@@ -38,20 +38,14 @@ export const getDuration = (
return `${hours} hours`;
};
export const formatDateTime = (
dateTimeString?: string | Dayjs | null,
timezone?: string,
): string => {
export const formatDateTime = (dateTimeString?: string | null): string => {
if (!dateTimeString) {
return 'N/A';
}
let dt = dayjs(dateTimeString);
if (timezone) {
dt = dt.tz(timezone);
}
return dt.format(DATE_TIME_FORMATS.MONTH_DATETIME);
return dayjs(dateTimeString.slice(0, 19)).format(
DATE_TIME_FORMATS.MONTH_DATETIME,
);
};
export const getAlertOptionsFromIds = (
@@ -66,8 +60,7 @@ export const getAlertOptionsFromIds = (
);
export const recurrenceInfo = (
recurrence?: AlertmanagertypesRecurrenceDTO | null,
timezone?: string,
recurrence?: RuletypesRecurrenceDTO | null,
): string => {
if (!recurrence) {
return 'No';
@@ -76,10 +69,10 @@ export const recurrenceInfo = (
const { startTime, duration, repeatOn, repeatType, endTime } = recurrence;
const formattedStartTime = startTime
? formatDateTime(startTime, timezone)
? formatDateTime(dayjs(startTime).toISOString())
: '';
const formattedEndTime = endTime
? `to ${formatDateTime(endTime, timezone)}`
? `to ${formatDateTime(dayjs(endTime).toISOString())}`
: '';
const weeklyRepeatString = repeatOn ? `on ${repeatOn.join(', ')}` : '';
const durationString = duration ? `- Duration: ${duration}` : '';
@@ -87,20 +80,22 @@ 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 defautlInitialValues: Partial<
RuletypesPlannedMaintenanceDTO & { editMode: boolean }
> = {
name: '',
description: '',
schedule: {
timezone: '',
endTime: undefined,
recurrence: undefined,
startTime: undefined,
},
alertIds: [],
createdAt: undefined,
createdBy: undefined,
editMode: false,
};
type DeleteDowntimeScheduleProps = {
deleteDowntimeScheduleAsync: UseMutateAsyncFunction<
@@ -215,6 +210,75 @@ export const recurrenceOptionWithSubmenu: Option[] = [
recurrenceOptions.monthly,
];
export const getRecurrenceOptionFromValue = (
value?: string | Option | null,
): Option | null | undefined => {
if (!value) {
return null;
}
if (typeof value === 'string') {
return Object.values(recurrenceOptions).find(
(option) => option.value === value,
);
}
return value;
};
export const getEndTime = ({
kind,
schedule,
}: Partial<
RuletypesPlannedMaintenanceDTO & {
editMode: boolean;
}
>): string | dayjs.Dayjs => {
if (kind === 'fixed') {
return schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '';
}
return schedule?.recurrence?.endTime
? dayjs(schedule.recurrence.endTime).toISOString()
: '';
};
export const isScheduleRecurring = (
schedule?: AlertmanagertypesPlannedMaintenanceDTO['schedule'] | null,
schedule?: RuletypesPlannedMaintenanceDTO['schedule'] | null,
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);
function convertUtcOffsetToTimezoneOffset(offsetMinutes: number): string {
const sign = offsetMinutes >= 0 ? '+' : '-';
const absOffset = Math.abs(offsetMinutes);
const hours = String(Math.floor(absOffset / 60)).padStart(2, '0');
const minutes = String(absOffset % 60).padStart(2, '0');
return `${sign}${hours}:${minutes}`;
}
export function formatWithTimezone(
dateValue?: string | dayjs.Dayjs,
timezone?: string,
): string {
const parsedDate =
typeof dateValue === 'string' ? dateValue : dateValue?.format();
// Get the target timezone offset
const targetOffset = convertUtcOffsetToTimezoneOffset(
dayjs(dateValue).tz(timezone).utcOffset(),
);
return `${parsedDate?.substring(0, 19)}${targetOffset}`;
}
export function handleTimeConversion(
dateValue: string | dayjs.Dayjs,
timezoneInit?: string,
timezone?: string,
shouldKeepLocalTime?: boolean,
): string {
const timezoneChanged = !isEqual(timezoneInit, timezone);
const initialTime = dayjs(dateValue).tz(timezoneInit);
const formattedTime = formatWithTimezone(initialTime, timezone);
return timezoneChanged
? formattedTime
: dayjs(dateValue).tz(timezone, shouldKeepLocalTime).format();
}

View File

@@ -27,10 +27,10 @@ const MOCK_DATE_3 = '2024-01-03';
const MOCK_DOWNTIME_1 = createMockDowntime({
id: '1',
name: MOCK_DOWNTIME_1_NAME,
createdAt: MOCK_DATE_1,
updatedAt: MOCK_DATE_1,
createdAt: new Date(MOCK_DATE_1),
updatedAt: new Date(MOCK_DATE_1),
schedule: buildSchedule({
startTime: MOCK_DATE_1,
startTime: new Date(MOCK_DATE_1),
timezone: 'UTC',
}),
alertIds: [],
@@ -39,10 +39,10 @@ const MOCK_DOWNTIME_1 = createMockDowntime({
const MOCK_DOWNTIME_2 = createMockDowntime({
id: '2',
name: MOCK_DOWNTIME_2_NAME,
createdAt: MOCK_DATE_2,
updatedAt: MOCK_DATE_2,
createdAt: new Date(MOCK_DATE_2),
updatedAt: new Date(MOCK_DATE_2),
schedule: buildSchedule({
startTime: MOCK_DATE_2,
startTime: new Date(MOCK_DATE_2),
timezone: 'UTC',
}),
alertIds: [],
@@ -51,10 +51,10 @@ const MOCK_DOWNTIME_2 = createMockDowntime({
const MOCK_DOWNTIME_3 = createMockDowntime({
id: '3',
name: MOCK_DOWNTIME_3_NAME,
createdAt: MOCK_DATE_3,
updatedAt: MOCK_DATE_3,
createdAt: new Date(MOCK_DATE_3),
updatedAt: new Date(MOCK_DATE_3),
schedule: buildSchedule({
startTime: MOCK_DATE_3,
startTime: new Date(MOCK_DATE_3),
timezone: 'UTC',
}),
alertIds: [],

View File

@@ -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,14 +17,14 @@ export const buildSchedule = (
});
export const createMockDowntime = (
overrides: Partial<AlertmanagertypesPlannedMaintenanceDTO>,
): AlertmanagertypesPlannedMaintenanceDTO => ({
overrides: Partial<RuletypesPlannedMaintenanceDTO>,
): RuletypesPlannedMaintenanceDTO => ({
id: overrides.id ?? '0',
name: overrides.name ?? '',
description: overrides.description ?? '',
schedule: buildSchedule({
timezone: 'UTC',
startTime: '2024-01-01',
startTime: new Date('2024-01-01'),
...overrides.schedule,
}),
alertIds: overrides.alertIds ?? [],
@@ -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,
});

View File

@@ -16,31 +16,35 @@ enum SpanScope {
ENTRYPOINT_SPANS = 'entrypoint_spans',
}
interface SpanFilterConfig {
key: string;
type: string;
}
interface SpanScopeSelectorProps {
onChange?: (value: TagFilter) => void;
query?: IBuilderQuery;
skipQueryBuilderRedirect?: boolean;
}
const SPAN_FILTER_KEY: Record<SpanScope, string | null> = {
const SPAN_FILTER_CONFIG: Record<SpanScope, SpanFilterConfig | null> = {
[SpanScope.ALL_SPANS]: null,
[SpanScope.ROOT_SPANS]: 'isRoot',
[SpanScope.ENTRYPOINT_SPANS]: 'isEntryPoint',
[SpanScope.ROOT_SPANS]: {
key: 'isRoot',
type: 'spanSearchScope',
},
[SpanScope.ENTRYPOINT_SPANS]: {
key: 'isEntryPoint',
type: 'spanSearchScope',
},
};
const SCOPE_FILTER_KEYS = Object.values(SPAN_FILTER_KEY).filter(
(key): key is string => key !== null,
);
const isScopeFilter = (filter: TagFilterItem, key: string): boolean =>
filter.key?.key === key && String(filter.value) === 'true';
const createFilterItem = (key: string): TagFilterItem => ({
const createFilterItem = (config: SpanFilterConfig): TagFilterItem => ({
id: uuid().slice(0, 8),
key: {
key,
key: config.key,
dataType: undefined,
type: '',
type: config?.type,
},
op: '=',
value: 'true',
@@ -66,7 +70,12 @@ function SpanScopeSelector({
filters: TagFilterItem[] = [],
): SpanScope => {
const hasFilter = (key: string): boolean =>
filters?.some((filter) => isScopeFilter(filter, key));
filters?.some(
(filter) =>
filter.key?.type === 'spanSearchScope' &&
filter.key.key === key &&
filter.value === 'true',
);
if (hasFilter('isRoot')) {
return SpanScope.ROOT_SPANS;
@@ -104,21 +113,28 @@ function SpanScopeSelector({
const nonScopeFilters = currentFilters.filter(
(filter) =>
!SCOPE_FILTER_KEYS.some((scopeKey) => isScopeFilter(filter, scopeKey)),
!(
filter.key?.type === 'spanSearchScope' &&
(filter.key.key === 'isRoot' || filter.key.key === 'isEntryPoint')
),
);
const scopeKey = SPAN_FILTER_KEY[newScope];
const newScopeFilter = scopeKey !== null ? [createFilterItem(scopeKey)] : [];
const config = SPAN_FILTER_CONFIG[newScope];
const newScopeFilter = config !== null ? [createFilterItem(config)] : [];
return [...nonScopeFilters, ...newScopeFilter];
};
const keysToRemove = Object.values(SPAN_FILTER_CONFIG)
.map((config) => config?.key)
.filter((key): key is string => typeof key === 'string');
newQuery.builder.queryData = newQuery.builder.queryData.map((item) => ({
...item,
filter: {
expression: removeKeysFromExpression(
item.filter?.expression ?? '',
SCOPE_FILTER_KEYS,
keysToRemove,
),
},
filters: {

View File

@@ -20,16 +20,12 @@ import SpanScopeSelector from '../SpanScopeSelector';
const mockRedirectWithQueryBuilderData = jest.fn();
const SCOPE_KEYS = ['isRoot', 'isEntryPoint'];
const isScopeFilter = (filter: TagFilterItem): boolean =>
SCOPE_KEYS.includes(filter.key?.key ?? '') && String(filter.value) === 'true';
// Helper to create filter items
const createSpanScopeFilter = (key: string): TagFilterItem => ({
id: 'span-filter',
key: {
key,
type: '',
type: 'spanSearchScope',
},
op: '=',
value: 'true',
@@ -147,6 +143,7 @@ describe('SpanScopeSelector', () => {
expect.objectContaining({
key: expect.objectContaining({
key: expectedKey,
type: 'spanSearchScope',
}),
op: '=',
value: 'true',
@@ -165,7 +162,11 @@ describe('SpanScopeSelector', () => {
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
const updatedQuery = mockRedirectWithQueryBuilderData.mock.calls[0][0];
const filters = updatedQuery.builder.queryData[0].filters.items;
expect(filters.some(isScopeFilter)).toBe(false);
expect(filters).not.toContainEqual(
expect.objectContaining({
key: expect.objectContaining({ type: 'spanSearchScope' }),
}),
);
});
it('should add isRoot filter when selecting ROOT_SPANS', async () => {
@@ -205,27 +206,6 @@ describe('SpanScopeSelector', () => {
await expect(screen.findByText(expectedText)).resolves.toBeInTheDocument();
},
);
// Round-trip from filter.expression can deserialize the value as a boolean
// `true` (unquoted in the expression) instead of the string `'true'` produced
// by the dropdown. The dropdown must still recognize that as the scope filter.
it.each([
['Root Spans', 'isRoot'],
['Entrypoint Spans', 'isEntryPoint'],
])(
'should initialize with %s selected when %s = true (boolean value)',
async (expectedText, filterKey) => {
const booleanScopeFilter: TagFilterItem = {
id: 'span-filter',
key: { key: filterKey, type: '' },
op: '=',
value: true as unknown as string,
};
const queryWithFilter = createQueryWithFilters([booleanScopeFilter]);
renderWithContext(queryWithFilter, undefined, defaultQueryBuilderQuery);
await expect(screen.findByText(expectedText)).resolves.toBeInTheDocument();
},
);
});
describe('when onChange and query props are provided', () => {
@@ -253,7 +233,9 @@ describe('SpanScopeSelector', () => {
expect(items).toContainEqual(nonScopeItem);
});
const scopeFiltersInPayload = items.filter(isScopeFilter);
const scopeFiltersInPayload = items.filter(
(filter) => filter.key?.type === 'spanSearchScope',
);
if (expectedScopeKey) {
expect(scopeFiltersInPayload).toHaveLength(1);
@@ -452,7 +434,9 @@ describe('SpanScopeSelector', () => {
items: [],
};
// Count non-scope filters
const nonScopeFilters = items.filter((filter) => !isScopeFilter(filter));
const nonScopeFilters = items.filter(
(filter) => filter.key?.type !== 'spanSearchScope',
);
expect(nonScopeFilters).toHaveLength(1);
expect(nonScopeFilters).toContainEqual(

View File

@@ -5,8 +5,8 @@ const orgId = '019ba2bb-2fa1-7b24-8159-cfca08617ef9';
export const managedRoles: AuthtypesRoleDTO[] = [
{
id: '019c24aa-2248-756f-9833-984f1ab63819',
createdAt: '2026-02-03T18:00:55.624356Z',
updatedAt: '2026-02-03T18:00:55.624356Z',
createdAt: new Date('2026-02-03T18:00:55.624356Z'),
updatedAt: new Date('2026-02-03T18:00:55.624356Z'),
name: 'signoz-admin',
description:
'Role assigned to users who have full administrative access to SigNoz resources.',
@@ -15,8 +15,8 @@ export const managedRoles: AuthtypesRoleDTO[] = [
},
{
id: '019c24aa-2248-757c-9faf-7b1e899751e0',
createdAt: '2026-02-03T18:00:55.624359Z',
updatedAt: '2026-02-03T18:00:55.624359Z',
createdAt: new Date('2026-02-03T18:00:55.624359Z'),
updatedAt: new Date('2026-02-03T18:00:55.624359Z'),
name: 'signoz-editor',
description:
'Role assigned to users who can create, edit, and manage SigNoz resources but do not have full administrative privileges.',
@@ -25,8 +25,8 @@ export const managedRoles: AuthtypesRoleDTO[] = [
},
{
id: '019c24aa-2248-7585-a129-4188b3473c27',
createdAt: '2026-02-03T18:00:55.624362Z',
updatedAt: '2026-02-03T18:00:55.624362Z',
createdAt: new Date('2026-02-03T18:00:55.624362Z'),
updatedAt: new Date('2026-02-03T18:00:55.624362Z'),
name: 'signoz-viewer',
description:
'Role assigned to users who have read-only access to SigNoz resources.',
@@ -38,8 +38,8 @@ export const managedRoles: AuthtypesRoleDTO[] = [
export const customRoles: AuthtypesRoleDTO[] = [
{
id: '019c24aa-3333-0001-aaaa-111111111111',
createdAt: '2026-02-10T10:30:00.000Z',
updatedAt: '2026-02-12T14:20:00.000Z',
createdAt: new Date('2026-02-10T10:30:00.000Z'),
updatedAt: new Date('2026-02-12T14:20:00.000Z'),
name: 'billing-manager',
description: 'Custom role for managing billing and invoices.',
type: 'custom',
@@ -47,8 +47,8 @@ export const customRoles: AuthtypesRoleDTO[] = [
},
{
id: '019c24aa-3333-0002-bbbb-222222222222',
createdAt: '2026-02-11T09:00:00.000Z',
updatedAt: '2026-02-13T11:45:00.000Z',
createdAt: new Date('2026-02-11T09:00:00.000Z'),
updatedAt: new Date('2026-02-13T11:45:00.000Z'),
name: 'dashboard-creator',
description: 'Custom role allowing users to create and manage dashboards.',
type: 'custom',

View File

@@ -1,12 +1,9 @@
import { useCallback, useEffect, useRef } from 'react';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { useHistory, useParams } from 'react-router-dom';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import ConversationView from 'container/AIAssistant/ConversationView';
import { AIAssistantEvents } from 'container/AIAssistant/events';
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
import { VariantContext } from 'container/AIAssistant/VariantContext';
import { Sparkles } from '@signozhq/icons';
@@ -20,27 +17,8 @@ interface RouteParams {
export default function AIAssistantPage(): JSX.Element {
const history = useHistory();
const location = useLocation<{ fromInApp?: boolean } | undefined>();
const { pathname } = location;
const { conversationId } = useParams<RouteParams>();
// Skip the mount-time Opened fire when the user expanded an already-open
// drawer/modal — that surface already emitted Opened with the right source.
// Router state (vs a module flag) survives StrictMode double-mount and
// aborted navigations.
const fromInApp = location.state?.fromInApp === true;
useEffect(() => {
if (fromInApp) {
return;
}
void logEvent(AIAssistantEvents.Opened, {
source: 'deeplink',
currentPage: normalizePage(pathname),
});
// Only on mount; route param changes inside the same page aren't a re-open.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const conversations = useAIAssistantStore((s) => s.conversations);
const activeConversationId = useAIAssistantStore(
(s) => s.activeConversationId,
@@ -93,14 +71,9 @@ export default function AIAssistantPage(): JSX.Element {
);
const handleNewConversation = useCallback(() => {
void logEvent(AIAssistantEvents.NewChatClicked, {
page: normalizePage(pathname),
mode: 'full_screen',
source: 'history_list',
});
const newId = startNewConversation();
history.push(ROUTES.AI_ASSISTANT.replace(':conversationId', newId));
}, [startNewConversation, history, pathname]);
}, [startNewConversation, history]);
// Prefer the URL param, but fall back to the store's `activeConversationId`
// for the brief render after a re-key (client UUID → backend threadId), so

View File

@@ -112,11 +112,11 @@ export function SpanHoverCard({
}
const span = spans[idx];
const previewRows: SpanPreviewRow[] = previewFields
.filter((f) => !RESERVED_PREVIEW_KEYS.has(f.name))
.filter((f) => !RESERVED_PREVIEW_KEYS.has(f.key))
.map((f) => {
const value = getSpanAttribute(span, f.name);
const value = getSpanAttribute(span, f.key);
return value !== undefined && value !== ''
? { key: f.name, value: String(value) }
? { key: f.key, value: String(value) }
: null;
})
.filter((r): r is SpanPreviewRow => r !== null);

View File

@@ -10,7 +10,6 @@ import {
import { Skeleton } from 'antd';
import setLocalStorageKey from 'api/browser/localstorage/set';
import cx from 'classnames';
import FieldsSelector from 'components/FieldsSelector';
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
@@ -24,10 +23,12 @@ import {
Server,
Timer,
} from '@signozhq/icons';
import { FloatingPanel } from 'periscope/components/FloatingPanel';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { TraceDetailV2URLProps } from 'types/api/trace/getTraceV2';
import { DataSource } from 'types/common/queryBuilder';
import FieldsSettings from '../components/FieldsSettings/FieldsSettings';
import { useTraceStore } from '../stores/traceStore';
import AnalyticsPanel from '../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel';
import Filters from '../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters';
@@ -225,15 +226,26 @@ function TraceDetailsHeader({
</div>
)}
<FieldsSelector
isOpen={isPreviewFieldsOpen}
title="Preview fields"
fields={previewFields}
onFieldsChange={setPreviewFields}
onClose={(): void => setIsPreviewFieldsOpen(false)}
signal={DataSource.TRACES}
maxFields={10}
/>
{isPreviewFieldsOpen && (
<FloatingPanel
isOpen
width={350}
height={window.innerHeight - 100}
defaultPosition={{
x: window.innerWidth - 350 - 100,
y: 50,
}}
enableResizing={false}
>
<FieldsSettings
title="Preview fields"
fields={previewFields}
onFieldsChange={setPreviewFields}
onClose={(): void => setIsPreviewFieldsOpen(false)}
dataSource={DataSource.TRACES}
/>
</FloatingPanel>
)}
<AnalyticsPanel
isOpen={isAnalyticsOpen}

View File

@@ -9,7 +9,10 @@ import { SpanV3 } from 'types/api/trace/getTraceV3';
import { COLOR_BY_FIELDS } from '../constants';
import { useTraceStore } from '../stores/traceStore';
import Error from '../TraceWaterfall/TraceWaterfallStates/Error/Error';
import { mergeTelemetryFieldKeys } from '../utils/previewFields';
import {
mergeTelemetryFieldKeys,
toTelemetryFieldKey,
} from '../utils/previewFields';
import { FLAMEGRAPH_SPAN_LIMIT } from './constants';
import FlamegraphCanvas from './FlamegraphCanvas';
import { useVisualLayoutWorker } from './hooks/useVisualLayoutWorker';
@@ -57,7 +60,11 @@ function TraceFlamegraph({
// Color-by fields baseline + user-picked preview fields. De-duped by `name`,
// color-by entries first so their canonical metadata wins on collision.
const flamegraphSelectFields = useMemo(
() => mergeTelemetryFieldKeys(COLOR_BY_FIELDS, previewFields),
() =>
mergeTelemetryFieldKeys(
COLOR_BY_FIELDS,
previewFields.map(toTelemetryFieldKey),
),
[previewFields],
);

View File

@@ -144,14 +144,14 @@ export function useFlamegraphHover(
const buildPreviewRows = useCallback(
(span: FlamegraphSpan): SpanPreviewRowData[] =>
previewFields
.filter((field) => !RESERVED_PREVIEW_KEYS.has(field.name))
.filter((field) => !RESERVED_PREVIEW_KEYS.has(field.key))
.map((field) => {
const value = getSpanAttribute(
{ resource: span.resource, attributes: span.attributes },
field.name,
field.key,
);
return value !== undefined && value !== ''
? { key: field.name, value: String(value) }
? { key: field.key, value: String(value) }
: null;
})
.filter((r): r is SpanPreviewRowData => r !== null),

View File

@@ -18,22 +18,21 @@ import { Button } from '@signozhq/ui/button';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical } from '@signozhq/icons';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { Typography } from '@signozhq/ui/typography';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import styles from './FieldsSelector.module.scss';
import styles from './FieldsSettings.module.scss';
function SortableField({
field,
onRemove,
allowDrag,
}: {
field: TelemetryFieldKey;
onRemove: (field: TelemetryFieldKey) => void;
field: BaseAutocompleteData;
onRemove: (field: BaseAutocompleteData) => void;
allowDrag: boolean;
}): JSX.Element {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: field.name });
useSortable({ id: field.key });
const style = {
transform: CSS.Transform.toString(transform),
@@ -51,7 +50,7 @@ function SortableField({
>
<div {...attributes} {...listeners} className={styles.dragHandle}>
{allowDrag && <GripVertical size={14} />}
<span className={styles.fieldKey}>{field.name}</span>
<span className={styles.fieldKey}>{field.key}</span>
</div>
<Button
className={cx(styles.removeBtn, 'periscope-btn')}
@@ -68,52 +67,41 @@ function SortableField({
interface AddedFieldsProps {
inputValue: string;
fields: TelemetryFieldKey[];
onFieldsChange: (fields: TelemetryFieldKey[]) => void;
maxFields?: number;
fields: BaseAutocompleteData[];
onFieldsChange: (fields: BaseAutocompleteData[]) => void;
}
function AddedFields({
inputValue,
fields,
onFieldsChange,
maxFields,
}: AddedFieldsProps): JSX.Element {
const sensors = useSensors(useSensor(PointerSensor));
const handleDragEnd = (event: DragEndEvent): void => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = fields.findIndex((f) => f.name === active.id);
const newIndex = fields.findIndex((f) => f.name === over.id);
const oldIndex = fields.findIndex((f) => f.key === active.id);
const newIndex = fields.findIndex((f) => f.key === over.id);
onFieldsChange(arrayMove(fields, oldIndex, newIndex));
}
};
const filteredFields = useMemo(
() =>
fields.filter((f) =>
f.name.toLowerCase().includes(inputValue.toLowerCase()),
),
fields.filter((f) => f.key.toLowerCase().includes(inputValue.toLowerCase())),
[fields, inputValue],
);
const handleRemove = (field: TelemetryFieldKey): void => {
onFieldsChange(fields.filter((f) => f.name !== field.name));
const handleRemove = (field: BaseAutocompleteData): void => {
onFieldsChange(fields.filter((f) => f.key !== field.key));
};
const allowDrag = inputValue.length === 0;
return (
<div className={cx(styles.section, styles.sectionAdded)}>
<div className={styles.sectionHeader}>
<span>ADDED FIELDS</span>
{maxFields !== undefined && (
<Typography.Text size="sm" weight="medium" color="muted">
Max Allowed: {maxFields}
</Typography.Text>
)}
</div>
<div className={styles.sectionHeader}>ADDED FIELDS</div>
<div className={styles.addedList}>
<OverlayScrollbar>
<DndContext
@@ -125,13 +113,13 @@ function AddedFields({
<div className={styles.noValues}>No values found</div>
) : (
<SortableContext
items={fields.map((f) => f.name)}
items={fields.map((f) => f.key)}
strategy={verticalListSortingStrategy}
disabled={!allowDrag}
>
{filteredFields.map((field) => (
<SortableField
key={field.name}
key={field.key}
field={field}
onRemove={handleRemove}
allowDrag={allowDrag}

View File

@@ -56,14 +56,12 @@
}
.sectionHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--muted-foreground);
font-size: 11px;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
padding: 8px 12px;
}
@@ -91,6 +89,13 @@
font-size: 12px;
}
.limitHint {
padding: 8px 12px;
text-align: center;
color: var(--muted-foreground);
font-size: 11px;
}
.fieldItem {
display: flex;
align-items: center;

View File

@@ -0,0 +1,149 @@
import { useCallback, useMemo, useState } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import AddedFields from './AddedFields';
import OtherFields from './OtherFields';
import styles from './FieldsSettings.module.scss';
const MAX_FIELDS_DEFAULT = 10;
interface FieldsSettingsProps {
title: string;
// Picker's native shape (`BaseAutocompleteData`) is preserved end-to-end so
// downstream consumers (flamegraph `selectFields`, hover popovers) get full
// field metadata without a lossy conversion at add-time.
fields: BaseAutocompleteData[];
onFieldsChange: (fields: BaseAutocompleteData[]) => void;
onClose: () => void;
dataSource: DataSource;
maxFields?: number;
}
function FieldsSettings({
title,
fields,
onFieldsChange,
onClose,
dataSource,
maxFields = MAX_FIELDS_DEFAULT,
}: FieldsSettingsProps): JSX.Element {
// Local draft state — changes here don't persist until Save
const [draftFields, setDraftFields] = useState<BaseAutocompleteData[]>(fields);
const [inputValue, setInputValue] = useState('');
const [debouncedInputValue, setDebouncedInputValue] = useState('');
const debouncedUpdate = useDebouncedFn((value) => {
setDebouncedInputValue(value as string);
}, 400);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value.trim().toLowerCase();
setInputValue(value);
debouncedUpdate(value);
},
[debouncedUpdate],
);
const handleAdd = useCallback(
(field: BaseAutocompleteData): void => {
if (draftFields.length >= maxFields) {
return;
}
if (draftFields.some((f) => f.key === field.key)) {
return;
}
setDraftFields((prev) => [...prev, field]);
},
[draftFields, maxFields],
);
const handleSave = useCallback((): void => {
onFieldsChange(draftFields);
toast.success('Saved successfully', {
position: 'top-right',
});
onClose();
}, [draftFields, onFieldsChange, onClose]);
const handleDiscard = useCallback((): void => {
setDraftFields(fields);
}, [fields]);
const hasUnsavedChanges = useMemo(
() =>
!(
draftFields.length === fields.length &&
draftFields.every((f, i) => f.key === fields[i]?.key)
),
[draftFields, fields],
);
const isAtLimit = draftFields.length >= maxFields;
return (
<div className={styles.root}>
<div className={styles.header}>
<div className={styles.title}>
<TableColumnsSplit size={16} />
{title}
</div>
<X className={styles.closeIcon} size={16} onClick={onClose} />
</div>
<section>
<Input
className={styles.searchInput}
type="text"
value={inputValue}
placeholder="Search for a field..."
onChange={handleInputChange}
/>
</section>
<AddedFields
inputValue={inputValue}
fields={draftFields}
onFieldsChange={setDraftFields}
/>
<OtherFields
dataSource={dataSource}
debouncedInputValue={debouncedInputValue}
addedFields={draftFields}
onAdd={handleAdd}
isAtLimit={isAtLimit}
/>
{hasUnsavedChanges && (
<div className={styles.footer}>
<Button
variant="outlined"
color="secondary"
onClick={handleDiscard}
prefix={<X width={14} height={14} />}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
onClick={handleSave}
prefix={<Check width={14} height={14} />}
>
Save changes
</Button>
</div>
)}
</div>
);
}
export default FieldsSettings;

View File

@@ -4,58 +4,51 @@ import { Skeleton } from 'antd';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import {
FieldContext,
FieldDataType,
SignalType,
TelemetryFieldKey,
} from 'types/api/v5/queryRange';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import styles from './FieldsSelector.module.scss';
import styles from './FieldsSettings.module.scss';
interface OtherFieldsProps {
signal: DataSource;
dataSource: DataSource;
debouncedInputValue: string;
addedFields: TelemetryFieldKey[];
onAdd: (field: TelemetryFieldKey) => void;
addedFields: BaseAutocompleteData[];
onAdd: (field: BaseAutocompleteData) => void;
isAtLimit: boolean;
}
function OtherFields({
signal,
dataSource,
debouncedInputValue,
addedFields,
onAdd,
isAtLimit,
}: OtherFieldsProps): JSX.Element {
const { data, isFetching } = useGetQueryKeySuggestions(
// API call to get available attribute keys
const { data, isFetching } = useGetAggregateKeys(
{
signal,
searchText: debouncedInputValue,
dataSource,
aggregateOperator: 'noop',
aggregateAttribute: '',
tagType: '',
},
{
queryKey: [
REACT_QUERY_KEY.GET_FIELDS_SELECTOR_SUGGESTIONS,
signal,
REACT_QUERY_KEY.GET_OTHER_FILTERS,
'preview-fields',
debouncedInputValue,
],
enabled: true,
},
);
const otherFields: TelemetryFieldKey[] = useMemo(() => {
const suggestions = Object.values(data?.data.data.keys || {}).flat();
const addedNames = new Set(addedFields.map((f) => f.name));
return suggestions
.filter((attr) => !addedNames.has(attr.name))
.map((attr) => ({
...attr,
signal: attr.signal as SignalType,
fieldContext: attr.fieldContext as FieldContext,
fieldDataType: attr.fieldDataType as FieldDataType,
}));
// Filter out already-added fields, match on .key from API response objects
const otherFields = useMemo(() => {
const attributes = data?.payload?.attributeKeys || [];
const addedKeys = new Set(addedFields.map((f) => f.key));
return attributes.filter((attr) => !addedKeys.has(attr.key));
}, [data, addedFields]);
if (isFetching) {
@@ -83,10 +76,10 @@ function OtherFields({
) : (
otherFields.map((attr) => (
<div
key={attr.name}
key={attr.key}
className={cx(styles.fieldItem, styles.otherFieldItem)}
>
<span className={styles.fieldKey}>{attr.name}</span>
<span className={styles.fieldKey}>{attr.key}</span>
{!isAtLimit && (
<Button
className={cx(styles.addBtn, 'periscope-btn')}
@@ -101,6 +94,7 @@ function OtherFields({
</div>
))
)}
{isAtLimit && <div className={styles.limitHint}>Maximum 10 fields</div>}
</>
</OverlayScrollbar>
</div>

View File

@@ -15,7 +15,6 @@ import {
AGGREGATIONS,
getAggregationMap as findAggregationMap,
} from '../utils/aggregations';
import { toTelemetryFieldKey } from '../utils/previewFields';
interface MutateOptions {
onSuccess?: () => void;
@@ -38,7 +37,7 @@ interface TraceStoreState {
// --- Derived state (cached for reference stability) ---
colorByField: TelemetryFieldKey;
availableColorByOptions: ColorByOption[];
previewFields: TelemetryFieldKey[];
previewFields: BaseAutocompleteData[];
// --- Setters used only by TraceStoreSync ---
setAggregations: (
@@ -52,7 +51,7 @@ interface TraceStoreState {
// --- Public actions (called from components) ---
setColorByField: (field: TelemetryFieldKey) => void;
setPreviewFields: (next: TelemetryFieldKey[]) => void;
setPreviewFields: (next: BaseAutocompleteData[]) => void;
}
/**
@@ -106,31 +105,21 @@ function deriveColorState(
}
/**
* Reads preview fields from user preferences and normalizes them to
* `TelemetryFieldKey`. Legacy entries persisted as `BaseAutocompleteData` (with
* a `.key` instead of `.name`) are upgraded in-place so existing users don't
* lose their saved preview-field selection.
* Reads preview fields from user preferences and filters out malformed entries.
*/
function derivePreviewFields(
userPreferences: UserPreference[] | null,
): TelemetryFieldKey[] {
): BaseAutocompleteData[] {
const pref = userPreferences?.find(
(p) => p.name === USER_PREFERENCES.SPAN_DETAILS_PREVIEW_ATTRIBUTES,
);
const raw = (pref?.value as unknown[] | undefined) ?? [];
const result: TelemetryFieldKey[] = [];
for (const entry of raw) {
if (typeof entry !== 'object' || entry === null) {
continue;
}
const candidate = entry as { name?: unknown; key?: unknown };
if (typeof candidate.name === 'string') {
result.push(entry as TelemetryFieldKey);
} else if (typeof candidate.key === 'string') {
result.push(toTelemetryFieldKey(entry as BaseAutocompleteData));
}
}
return result;
const raw = (pref?.value as BaseAutocompleteData[] | undefined) ?? [];
return raw.filter(
(f): f is BaseAutocompleteData =>
typeof f === 'object' &&
f !== null &&
typeof (f as { key?: unknown }).key === 'string',
);
}
export const useTraceStore = create<TraceStoreState>()((set, get) => ({

View File

@@ -41,9 +41,8 @@ function mapFieldDataType(
}
/**
* Upgrades a legacy `BaseAutocompleteData`-shaped preview field (persisted by
* pre-migration clients) to the current `TelemetryFieldKey` shape. Kept around
* for the read-side compatibility shim in `traceStore.derivePreviewFields`.
* Convert a picker-shaped field to the API's `TelemetryFieldKey` shape used
* for `selectFields` on the flamegraph request.
*/
export function toTelemetryFieldKey(
field: BaseAutocompleteData,
@@ -52,7 +51,6 @@ export function toTelemetryFieldKey(
name: field.key,
fieldContext: mapFieldContext(field.type),
fieldDataType: mapFieldDataType(field.dataType),
isIndexed: field.isIndexed,
};
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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,
)

View File

@@ -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", &notifConfig)
require.NoError(t, err)
mwID := valuer.GenerateUUID()
maintenanceStore := alertmanagertypestest.NewMockMaintenanceStore(t)
maintenanceStore.On("ListPlannedMaintenance", mock.Anything, orgID).Return(
[]*alertmanagertypes.PlannedMaintenance{{
ID: mwID,
Schedule: &alertmanagertypes.Schedule{
Timezone: "UTC",
StartTime: time.Now().Add(-time.Hour),
EndTime: time.Now().Add(time.Hour),
},
RuleIDs: []string{"high-cpu-usage"},
}}, nil,
)
srvCfg := NewConfig()
stateStore := alertmanagertypestest.NewStateStore()
registry := prometheus.NewRegistry()
logger := slog.New(slog.DiscardHandler)
server, err := New(context.Background(), logger, registry, srvCfg, orgID, stateStore, notificationManager, 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)
}
}
})
}

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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},

View File

@@ -231,7 +231,7 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
return nil, err
}
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.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
}

View File

@@ -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])
}

View File

@@ -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])
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package sqlalertmanagerstore
package sqlrulestore
import (
"context"
@@ -9,8 +9,8 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -19,15 +19,15 @@ type maintenance struct {
logger *slog.Logger
}
func NewMaintenanceStore(store sqlstore.SQLStore, providerSettings factory.ProviderSettings) alertmanagertypes.MaintenanceStore {
func NewMaintenanceStore(store sqlstore.SQLStore, providerSettings factory.ProviderSettings) ruletypes.MaintenanceStore {
return &maintenance{
sqlstore: store,
logger: providerSettings.Logger,
}
}
func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string) ([]*alertmanagertypes.PlannedMaintenance, error) {
gettableMaintenancesRules := make([]*alertmanagertypes.PlannedMaintenanceWithRules, 0)
func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string) ([]*ruletypes.PlannedMaintenance, error) {
gettableMaintenancesRules := make([]*ruletypes.PlannedMaintenanceWithRules, 0)
err := r.sqlstore.
BunDB().
NewSelect().
@@ -39,7 +39,7 @@ func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string)
return nil, err
}
gettablePlannedMaintenance := make([]*alertmanagertypes.PlannedMaintenance, 0)
gettablePlannedMaintenance := make([]*ruletypes.PlannedMaintenance, 0)
for _, gettableMaintenancesRule := range gettableMaintenancesRules {
m := gettableMaintenancesRule.ToPlannedMaintenance()
gettablePlannedMaintenance = append(gettablePlannedMaintenance, m)
@@ -51,8 +51,8 @@ func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string)
return gettablePlannedMaintenance, nil
}
func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.UUID) (*alertmanagertypes.PlannedMaintenance, error) {
storableMaintenanceRule := new(alertmanagertypes.PlannedMaintenanceWithRules)
func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.UUID) (*ruletypes.PlannedMaintenance, error) {
storableMaintenanceRule := new(ruletypes.PlannedMaintenanceWithRules)
err := r.sqlstore.
BunDB().
NewSelect().
@@ -67,13 +67,13 @@ func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.U
return storableMaintenanceRule.ToPlannedMaintenance(), nil
}
func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance *alertmanagertypes.PostablePlannedMaintenance) (*alertmanagertypes.PlannedMaintenance, error) {
func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance *ruletypes.PostablePlannedMaintenance) (*ruletypes.PlannedMaintenance, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
storablePlannedMaintenance := alertmanagertypes.StorablePlannedMaintenance{
storablePlannedMaintenance := ruletypes.StorablePlannedMaintenance{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -91,14 +91,14 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
OrgID: claims.OrgID,
}
maintenanceRules := make([]*alertmanagertypes.StorablePlannedMaintenanceRule, 0)
maintenanceRules := make([]*ruletypes.StorablePlannedMaintenanceRule, 0)
for _, ruleIDStr := range maintenance.AlertIds {
ruleID, err := valuer.NewUUID(ruleIDStr)
if err != nil {
return nil, err
}
maintenanceRules = append(maintenanceRules, &alertmanagertypes.StorablePlannedMaintenanceRule{
maintenanceRules = append(maintenanceRules, &ruletypes.StorablePlannedMaintenanceRule{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -135,7 +135,7 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
return nil, err
}
return &alertmanagertypes.PlannedMaintenance{
return &ruletypes.PlannedMaintenance{
ID: storablePlannedMaintenance.ID,
Name: storablePlannedMaintenance.Name,
Description: storablePlannedMaintenance.Description,
@@ -152,7 +152,7 @@ func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UU
_, err := r.sqlstore.
BunDB().
NewDelete().
Model(new(alertmanagertypes.StorablePlannedMaintenance)).
Model(new(ruletypes.StorablePlannedMaintenance)).
Where("id = ?", id.StringValue()).
Exec(ctx)
if err != nil {
@@ -162,7 +162,7 @@ func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UU
return nil
}
func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance *alertmanagertypes.PostablePlannedMaintenance, id valuer.UUID) error {
func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance *ruletypes.PostablePlannedMaintenance, id valuer.UUID) error {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return err
@@ -173,7 +173,7 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
return err
}
storablePlannedMaintenance := alertmanagertypes.StorablePlannedMaintenance{
storablePlannedMaintenance := ruletypes.StorablePlannedMaintenance{
Identifiable: types.Identifiable{
ID: id,
},
@@ -191,14 +191,14 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
OrgID: claims.OrgID,
}
storablePlannedMaintenanceRules := make([]*alertmanagertypes.StorablePlannedMaintenanceRule, 0)
storablePlannedMaintenanceRules := make([]*ruletypes.StorablePlannedMaintenanceRule, 0)
for _, ruleIDStr := range maintenance.AlertIds {
ruleID, err := valuer.NewUUID(ruleIDStr)
if err != nil {
return err
}
storablePlannedMaintenanceRules = append(storablePlannedMaintenanceRules, &alertmanagertypes.StorablePlannedMaintenanceRule{
storablePlannedMaintenanceRules = append(storablePlannedMaintenanceRules, &ruletypes.StorablePlannedMaintenanceRule{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -221,7 +221,7 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
_, err = r.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(alertmanagertypes.StorablePlannedMaintenanceRule)).
Model(new(ruletypes.StorablePlannedMaintenanceRule)).
Where("planned_maintenance_id = ?", storablePlannedMaintenance.ID.StringValue()).
Exec(ctx)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,8 @@ import (
"testing"
"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/analytics"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
@@ -61,11 +59,9 @@ func TestNewProviderFactories(t *testing.T) {
})
assert.NotPanics(t, func() {
store := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)
orgGetter := implorganization.NewGetter(implorganization.NewStore(store), nil)
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)), nil)
notificationManager := nfmanagertest.NewMock()
maintenanceStore := sqlalertmanagerstore.NewMaintenanceStore(store, factorytest.NewSettings())
NewAlertmanagerProviderFactories(store, orgGetter, notificationManager, maintenanceStore)
NewAlertmanagerProviderFactories(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), orgGetter, notificationManager)
})
assert.NotPanics(t, func() {

View File

@@ -5,7 +5,6 @@ import (
"log/slog"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/sqlroutingstore"
"github.com/SigNoz/signoz/pkg/analytics"
@@ -376,14 +375,12 @@ func New(
return nil, err
}
maintenanceStore := sqlalertmanagerstore.NewMaintenanceStore(sqlstore, providerSettings)
// Initialize alertmanager from the available alertmanager provider factories
alertmanager, err := factory.NewProviderFromNamedMap(
ctx,
providerSettings,
config.Alertmanager,
NewAlertmanagerProviderFactories(sqlstore, orgGetter, nfManager, maintenanceStore),
NewAlertmanagerProviderFactories(sqlstore, orgGetter, nfManager),
config.Alertmanager.Provider,
)
if err != nil {

View File

@@ -11,7 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
@@ -61,7 +61,7 @@ type existingMaintenance struct {
Name string `bun:"name,type:text,notnull"`
Description string `bun:"description,type:text"`
AlertIDs *AlertIds `bun:"alert_ids,type:text"`
Schedule *alertmanagertypes.Schedule `bun:"schedule,type:text,notnull"`
Schedule *ruletypes.Schedule `bun:"schedule,type:text,notnull"`
CreatedAt time.Time `bun:"created_at,type:datetime,notnull"`
CreatedBy string `bun:"created_by,type:text,notnull"`
UpdatedAt time.Time `bun:"updated_at,type:datetime,notnull"`
@@ -75,7 +75,7 @@ type newMaintenance struct {
types.UserAuditable
Name string `bun:"name,type:text,notnull"`
Description string `bun:"description,type:text"`
Schedule *alertmanagertypes.Schedule `bun:"schedule,type:text,notnull"`
Schedule *ruletypes.Schedule `bun:"schedule,type:text,notnull"`
OrgID string `bun:"org_id,type:text"`
}

View File

@@ -1,81 +0,0 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addIntegrationDashboard struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddIntegrationDashboardFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("add_integration_dashboard"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addIntegrationDashboard{sqlstore: sqlstore, sqlschema: sqlschema}, nil
},
)
}
func (m *addIntegrationDashboard) Register(migrations *migrate.Migrations) error {
return migrations.Register(m.Up, m.Down)
}
func (m *addIntegrationDashboard) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
sqls := m.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "integration_dashboard",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "dashboard_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "provider", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "slug", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("dashboard_id"),
ReferencedTableName: sqlschema.TableName("dashboard"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
sqls = append(sqls, m.sqlschema.Operator().CreateIndex(
&sqlschema.UniqueIndex{
TableName: "integration_dashboard",
ColumnNames: []sqlschema.ColumnName{"dashboard_id"},
},
)...)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (m *addIntegrationDashboard) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -170,7 +170,6 @@ func NewGettableAlertsFromAlertProvider(
cfg *Config,
getAlertStatusFunc func(model.Fingerprint) types.AlertStatus,
setAlertStatusFunc func(model.LabelSet),
mutedByFunc func(model.LabelSet) []string,
params GettableAlertsParams,
) (GettableAlerts, error) {
res := GettableAlerts{}
@@ -220,7 +219,7 @@ func NewGettableAlertsFromAlertProvider(
continue
}
alert := v2.AlertToOpenAPIAlert(alertData, getAlertStatusFunc(alertData.Fingerprint()), receivers, mutedByFunc(alertData.Labels))
alert := v2.AlertToOpenAPIAlert(alertData, getAlertStatusFunc(alertData.Fingerprint()), receivers, nil)
res = append(res, alert)
}

View File

@@ -1,364 +0,0 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package alertmanagertypestest
import (
"context"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer"
mock "github.com/stretchr/testify/mock"
)
// NewMockMaintenanceStore creates a new instance of MockMaintenanceStore. 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 NewMockMaintenanceStore(t interface {
mock.TestingT
Cleanup(func())
}) *MockMaintenanceStore {
mock := &MockMaintenanceStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// MockMaintenanceStore is an autogenerated mock type for the MaintenanceStore type
type MockMaintenanceStore struct {
mock.Mock
}
type MockMaintenanceStore_Expecter struct {
mock *mock.Mock
}
func (_m *MockMaintenanceStore) EXPECT() *MockMaintenanceStore_Expecter {
return &MockMaintenanceStore_Expecter{mock: &_m.Mock}
}
// CreatePlannedMaintenance provides a mock function for the type MockMaintenanceStore
func (_mock *MockMaintenanceStore) CreatePlannedMaintenance(context1 context.Context, postablePlannedMaintenance *alertmanagertypes.PostablePlannedMaintenance) (*alertmanagertypes.PlannedMaintenance, error) {
ret := _mock.Called(context1, postablePlannedMaintenance)
if len(ret) == 0 {
panic("no return value specified for CreatePlannedMaintenance")
}
var r0 *alertmanagertypes.PlannedMaintenance
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *alertmanagertypes.PostablePlannedMaintenance) (*alertmanagertypes.PlannedMaintenance, error)); ok {
return returnFunc(context1, postablePlannedMaintenance)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *alertmanagertypes.PostablePlannedMaintenance) *alertmanagertypes.PlannedMaintenance); ok {
r0 = returnFunc(context1, postablePlannedMaintenance)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*alertmanagertypes.PlannedMaintenance)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *alertmanagertypes.PostablePlannedMaintenance) error); ok {
r1 = returnFunc(context1, postablePlannedMaintenance)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockMaintenanceStore_CreatePlannedMaintenance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePlannedMaintenance'
type MockMaintenanceStore_CreatePlannedMaintenance_Call struct {
*mock.Call
}
// CreatePlannedMaintenance is a helper method to define mock.On call
// - context1 context.Context
// - postablePlannedMaintenance *alertmanagertypes.PostablePlannedMaintenance
func (_e *MockMaintenanceStore_Expecter) CreatePlannedMaintenance(context1 interface{}, postablePlannedMaintenance interface{}) *MockMaintenanceStore_CreatePlannedMaintenance_Call {
return &MockMaintenanceStore_CreatePlannedMaintenance_Call{Call: _e.mock.On("CreatePlannedMaintenance", context1, postablePlannedMaintenance)}
}
func (_c *MockMaintenanceStore_CreatePlannedMaintenance_Call) Run(run func(context1 context.Context, postablePlannedMaintenance *alertmanagertypes.PostablePlannedMaintenance)) *MockMaintenanceStore_CreatePlannedMaintenance_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *alertmanagertypes.PostablePlannedMaintenance
if args[1] != nil {
arg1 = args[1].(*alertmanagertypes.PostablePlannedMaintenance)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockMaintenanceStore_CreatePlannedMaintenance_Call) Return(plannedMaintenance *alertmanagertypes.PlannedMaintenance, err error) *MockMaintenanceStore_CreatePlannedMaintenance_Call {
_c.Call.Return(plannedMaintenance, err)
return _c
}
func (_c *MockMaintenanceStore_CreatePlannedMaintenance_Call) RunAndReturn(run func(context1 context.Context, postablePlannedMaintenance *alertmanagertypes.PostablePlannedMaintenance) (*alertmanagertypes.PlannedMaintenance, error)) *MockMaintenanceStore_CreatePlannedMaintenance_Call {
_c.Call.Return(run)
return _c
}
// DeletePlannedMaintenance provides a mock function for the type MockMaintenanceStore
func (_mock *MockMaintenanceStore) DeletePlannedMaintenance(context1 context.Context, uUID valuer.UUID) error {
ret := _mock.Called(context1, uUID)
if len(ret) == 0 {
panic("no return value specified for DeletePlannedMaintenance")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, valuer.UUID) error); ok {
r0 = returnFunc(context1, uUID)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockMaintenanceStore_DeletePlannedMaintenance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePlannedMaintenance'
type MockMaintenanceStore_DeletePlannedMaintenance_Call struct {
*mock.Call
}
// DeletePlannedMaintenance is a helper method to define mock.On call
// - context1 context.Context
// - uUID valuer.UUID
func (_e *MockMaintenanceStore_Expecter) DeletePlannedMaintenance(context1 interface{}, uUID interface{}) *MockMaintenanceStore_DeletePlannedMaintenance_Call {
return &MockMaintenanceStore_DeletePlannedMaintenance_Call{Call: _e.mock.On("DeletePlannedMaintenance", context1, uUID)}
}
func (_c *MockMaintenanceStore_DeletePlannedMaintenance_Call) Run(run func(context1 context.Context, uUID valuer.UUID)) *MockMaintenanceStore_DeletePlannedMaintenance_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 valuer.UUID
if args[1] != nil {
arg1 = args[1].(valuer.UUID)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockMaintenanceStore_DeletePlannedMaintenance_Call) Return(err error) *MockMaintenanceStore_DeletePlannedMaintenance_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockMaintenanceStore_DeletePlannedMaintenance_Call) RunAndReturn(run func(context1 context.Context, uUID valuer.UUID) error) *MockMaintenanceStore_DeletePlannedMaintenance_Call {
_c.Call.Return(run)
return _c
}
// GetPlannedMaintenanceByID provides a mock function for the type MockMaintenanceStore
func (_mock *MockMaintenanceStore) GetPlannedMaintenanceByID(context1 context.Context, uUID valuer.UUID) (*alertmanagertypes.PlannedMaintenance, error) {
ret := _mock.Called(context1, uUID)
if len(ret) == 0 {
panic("no return value specified for GetPlannedMaintenanceByID")
}
var r0 *alertmanagertypes.PlannedMaintenance
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, valuer.UUID) (*alertmanagertypes.PlannedMaintenance, error)); ok {
return returnFunc(context1, uUID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, valuer.UUID) *alertmanagertypes.PlannedMaintenance); ok {
r0 = returnFunc(context1, uUID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*alertmanagertypes.PlannedMaintenance)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, valuer.UUID) error); ok {
r1 = returnFunc(context1, uUID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockMaintenanceStore_GetPlannedMaintenanceByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPlannedMaintenanceByID'
type MockMaintenanceStore_GetPlannedMaintenanceByID_Call struct {
*mock.Call
}
// GetPlannedMaintenanceByID is a helper method to define mock.On call
// - context1 context.Context
// - uUID valuer.UUID
func (_e *MockMaintenanceStore_Expecter) GetPlannedMaintenanceByID(context1 interface{}, uUID interface{}) *MockMaintenanceStore_GetPlannedMaintenanceByID_Call {
return &MockMaintenanceStore_GetPlannedMaintenanceByID_Call{Call: _e.mock.On("GetPlannedMaintenanceByID", context1, uUID)}
}
func (_c *MockMaintenanceStore_GetPlannedMaintenanceByID_Call) Run(run func(context1 context.Context, uUID valuer.UUID)) *MockMaintenanceStore_GetPlannedMaintenanceByID_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 valuer.UUID
if args[1] != nil {
arg1 = args[1].(valuer.UUID)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockMaintenanceStore_GetPlannedMaintenanceByID_Call) Return(plannedMaintenance *alertmanagertypes.PlannedMaintenance, err error) *MockMaintenanceStore_GetPlannedMaintenanceByID_Call {
_c.Call.Return(plannedMaintenance, err)
return _c
}
func (_c *MockMaintenanceStore_GetPlannedMaintenanceByID_Call) RunAndReturn(run func(context1 context.Context, uUID valuer.UUID) (*alertmanagertypes.PlannedMaintenance, error)) *MockMaintenanceStore_GetPlannedMaintenanceByID_Call {
_c.Call.Return(run)
return _c
}
// ListPlannedMaintenance provides a mock function for the type MockMaintenanceStore
func (_mock *MockMaintenanceStore) ListPlannedMaintenance(context1 context.Context, s string) ([]*alertmanagertypes.PlannedMaintenance, error) {
ret := _mock.Called(context1, s)
if len(ret) == 0 {
panic("no return value specified for ListPlannedMaintenance")
}
var r0 []*alertmanagertypes.PlannedMaintenance
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]*alertmanagertypes.PlannedMaintenance, error)); ok {
return returnFunc(context1, s)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []*alertmanagertypes.PlannedMaintenance); ok {
r0 = returnFunc(context1, s)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*alertmanagertypes.PlannedMaintenance)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(context1, s)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockMaintenanceStore_ListPlannedMaintenance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPlannedMaintenance'
type MockMaintenanceStore_ListPlannedMaintenance_Call struct {
*mock.Call
}
// ListPlannedMaintenance is a helper method to define mock.On call
// - context1 context.Context
// - s string
func (_e *MockMaintenanceStore_Expecter) ListPlannedMaintenance(context1 interface{}, s interface{}) *MockMaintenanceStore_ListPlannedMaintenance_Call {
return &MockMaintenanceStore_ListPlannedMaintenance_Call{Call: _e.mock.On("ListPlannedMaintenance", context1, s)}
}
func (_c *MockMaintenanceStore_ListPlannedMaintenance_Call) Run(run func(context1 context.Context, s string)) *MockMaintenanceStore_ListPlannedMaintenance_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockMaintenanceStore_ListPlannedMaintenance_Call) Return(plannedMaintenances []*alertmanagertypes.PlannedMaintenance, err error) *MockMaintenanceStore_ListPlannedMaintenance_Call {
_c.Call.Return(plannedMaintenances, err)
return _c
}
func (_c *MockMaintenanceStore_ListPlannedMaintenance_Call) RunAndReturn(run func(context1 context.Context, s string) ([]*alertmanagertypes.PlannedMaintenance, error)) *MockMaintenanceStore_ListPlannedMaintenance_Call {
_c.Call.Return(run)
return _c
}
// UpdatePlannedMaintenance provides a mock function for the type MockMaintenanceStore
func (_mock *MockMaintenanceStore) UpdatePlannedMaintenance(context1 context.Context, postablePlannedMaintenance *alertmanagertypes.PostablePlannedMaintenance, uUID valuer.UUID) error {
ret := _mock.Called(context1, postablePlannedMaintenance, uUID)
if len(ret) == 0 {
panic("no return value specified for UpdatePlannedMaintenance")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *alertmanagertypes.PostablePlannedMaintenance, valuer.UUID) error); ok {
r0 = returnFunc(context1, postablePlannedMaintenance, uUID)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockMaintenanceStore_UpdatePlannedMaintenance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePlannedMaintenance'
type MockMaintenanceStore_UpdatePlannedMaintenance_Call struct {
*mock.Call
}
// UpdatePlannedMaintenance is a helper method to define mock.On call
// - context1 context.Context
// - postablePlannedMaintenance *alertmanagertypes.PostablePlannedMaintenance
// - uUID valuer.UUID
func (_e *MockMaintenanceStore_Expecter) UpdatePlannedMaintenance(context1 interface{}, postablePlannedMaintenance interface{}, uUID interface{}) *MockMaintenanceStore_UpdatePlannedMaintenance_Call {
return &MockMaintenanceStore_UpdatePlannedMaintenance_Call{Call: _e.mock.On("UpdatePlannedMaintenance", context1, postablePlannedMaintenance, uUID)}
}
func (_c *MockMaintenanceStore_UpdatePlannedMaintenance_Call) Run(run func(context1 context.Context, postablePlannedMaintenance *alertmanagertypes.PostablePlannedMaintenance, uUID valuer.UUID)) *MockMaintenanceStore_UpdatePlannedMaintenance_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *alertmanagertypes.PostablePlannedMaintenance
if args[1] != nil {
arg1 = args[1].(*alertmanagertypes.PostablePlannedMaintenance)
}
var arg2 valuer.UUID
if args[2] != nil {
arg2 = args[2].(valuer.UUID)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *MockMaintenanceStore_UpdatePlannedMaintenance_Call) Return(err error) *MockMaintenanceStore_UpdatePlannedMaintenance_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockMaintenanceStore_UpdatePlannedMaintenance_Call) RunAndReturn(run func(context1 context.Context, postablePlannedMaintenance *alertmanagertypes.PostablePlannedMaintenance, uUID valuer.UUID) error) *MockMaintenanceStore_UpdatePlannedMaintenance_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
)
func NewMarker(r prometheus.Registerer) *types.MemMarker {
type MemMarker = types.MemMarker
func NewMarker(r prometheus.Registerer) *MemMarker {
return types.NewMarker(r)
}

View File

@@ -1,92 +0,0 @@
package alertmanagertypes
import (
"database/sql/driver"
"encoding/json"
"reflect"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
type RepeatType struct {
valuer.String
}
var (
RepeatTypeDaily = RepeatType{valuer.NewString("daily")}
RepeatTypeWeekly = RepeatType{valuer.NewString("weekly")}
RepeatTypeMonthly = RepeatType{valuer.NewString("monthly")}
)
// Enum implements jsonschema.Enum; returns the acceptable values for RepeatType.
func (RepeatType) Enum() []any {
return []any{
RepeatTypeDaily,
RepeatTypeWeekly,
RepeatTypeMonthly,
}
}
type RepeatOn struct {
valuer.String
}
var (
RepeatOnSunday = RepeatOn{valuer.NewString("sunday")}
RepeatOnMonday = RepeatOn{valuer.NewString("monday")}
RepeatOnTuesday = RepeatOn{valuer.NewString("tuesday")}
RepeatOnWednesday = RepeatOn{valuer.NewString("wednesday")}
RepeatOnThursday = RepeatOn{valuer.NewString("thursday")}
RepeatOnFriday = RepeatOn{valuer.NewString("friday")}
RepeatOnSaturday = RepeatOn{valuer.NewString("saturday")}
)
// Enum implements jsonschema.Enum; returns the acceptable values for RepeatOn.
func (RepeatOn) Enum() []any {
return []any{
RepeatOnSunday,
RepeatOnMonday,
RepeatOnTuesday,
RepeatOnWednesday,
RepeatOnThursday,
RepeatOnFriday,
RepeatOnSaturday,
}
}
var RepeatOnAllMap = map[RepeatOn]time.Weekday{
RepeatOnSunday: time.Sunday,
RepeatOnMonday: time.Monday,
RepeatOnTuesday: time.Tuesday,
RepeatOnWednesday: time.Wednesday,
RepeatOnThursday: time.Thursday,
RepeatOnFriday: time.Friday,
RepeatOnSaturday: time.Saturday,
}
type Recurrence struct {
StartTime time.Time `json:"startTime" required:"true"`
EndTime *time.Time `json:"endTime,omitempty"`
Duration valuer.TextDuration `json:"duration" required:"true"`
RepeatType RepeatType `json:"repeatType" required:"true"`
RepeatOn []RepeatOn `json:"repeatOn"`
}
func (r *Recurrence) Scan(src interface{}) error {
switch data := src.(type) {
case []byte:
return json.Unmarshal(data, r)
case string:
return json.Unmarshal([]byte(data), r)
case nil:
return nil
default:
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "recurrence: (unsupported \"%s\")", reflect.TypeOf(data).String())
}
}
func (r *Recurrence) Value() (driver.Value, error) {
return json.Marshal(r)
}

View File

@@ -1,4 +1,4 @@
package alertmanagertypes
package ruletypes
import (
"context"

View File

@@ -1,4 +1,4 @@
package alertmanagertypes
package ruletypes
import (
"testing"
@@ -633,7 +633,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
Schedule: &Schedule{
Timezone: "UTC",
// These fixed fields should be ignored when Recurrence is set.
StartTime: time.Date(2026, 4, 1, 14, 0, 0, 0, time.UTC),
StartTime: time.Date(2026, 4, 1, 10, 0, 0, 0, time.UTC),
EndTime: time.Date(2026, 4, 30, 18, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
StartTime: time.Date(2026, 4, 1, 14, 0, 0, 0, time.UTC), // daily at 14:00
@@ -642,7 +642,8 @@ func TestShouldSkipMaintenance(t *testing.T) {
},
},
},
// 2026-04-15 11:00 is inside the fixed range but outside the daily 14:00-16:00 window.
// 11:00 is inside the fixed range but outside the daily 14:00-16:00 window.
// Before the fix this returned true (bug); after fix it returns false.
ts: time.Date(2026, 4, 15, 11, 0, 0, 0, time.UTC),
skip: false,
},
@@ -651,17 +652,16 @@ func TestShouldSkipMaintenance(t *testing.T) {
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2026, 4, 1, 14, 0, 0, 0, time.UTC),
StartTime: time.Date(2026, 4, 1, 10, 0, 0, 0, time.UTC),
EndTime: time.Date(2026, 4, 30, 18, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
StartTime: time.Date(2026, 4, 1, 14, 0, 0, 0, time.UTC),
EndTime: timePtr(time.Date(2026, 4, 30, 18, 0, 0, 0, time.UTC)),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
},
},
// 15:00 is inside the daily 14:00-16:00 window. Should skip.
// 15:00 is inside the daily 14:00-16:00 window — should skip.
ts: time.Date(2026, 4, 15, 15, 0, 0, 0, time.UTC),
skip: true,
},

View File

@@ -1,4 +1,4 @@
package alertmanagertypes
package ruletypes
import (
"database/sql/driver"
@@ -7,30 +7,88 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Schedule struct {
Timezone string `json:"timezone" required:"true"`
StartTime time.Time `json:"startTime,omitempty"`
EndTime time.Time `json:"endTime,omitzero"`
Recurrence *Recurrence `json:"recurrence"`
type RepeatType struct {
valuer.String
}
func (s *Schedule) Scan(src interface{}) error {
switch data := src.(type) {
case []byte:
return json.Unmarshal(data, s)
case string:
return json.Unmarshal([]byte(data), s)
case nil:
return nil
default:
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "schedule: (unsupported \"%s\")", reflect.TypeOf(data).String())
var (
RepeatTypeDaily = RepeatType{valuer.NewString("daily")}
RepeatTypeWeekly = RepeatType{valuer.NewString("weekly")}
RepeatTypeMonthly = RepeatType{valuer.NewString("monthly")}
)
// Enum implements jsonschema.Enum; returns the acceptable values for RepeatType.
func (RepeatType) Enum() []any {
return []any{
RepeatTypeDaily,
RepeatTypeWeekly,
RepeatTypeMonthly,
}
}
func (s *Schedule) Value() (driver.Value, error) {
return json.Marshal(s)
type RepeatOn struct {
valuer.String
}
var (
RepeatOnSunday = RepeatOn{valuer.NewString("sunday")}
RepeatOnMonday = RepeatOn{valuer.NewString("monday")}
RepeatOnTuesday = RepeatOn{valuer.NewString("tuesday")}
RepeatOnWednesday = RepeatOn{valuer.NewString("wednesday")}
RepeatOnThursday = RepeatOn{valuer.NewString("thursday")}
RepeatOnFriday = RepeatOn{valuer.NewString("friday")}
RepeatOnSaturday = RepeatOn{valuer.NewString("saturday")}
)
// Enum implements jsonschema.Enum; returns the acceptable values for RepeatOn.
func (RepeatOn) Enum() []any {
return []any{
RepeatOnSunday,
RepeatOnMonday,
RepeatOnTuesday,
RepeatOnWednesday,
RepeatOnThursday,
RepeatOnFriday,
RepeatOnSaturday,
}
}
var RepeatOnAllMap = map[RepeatOn]time.Weekday{
RepeatOnSunday: time.Sunday,
RepeatOnMonday: time.Monday,
RepeatOnTuesday: time.Tuesday,
RepeatOnWednesday: time.Wednesday,
RepeatOnThursday: time.Thursday,
RepeatOnFriday: time.Friday,
RepeatOnSaturday: time.Saturday,
}
type Recurrence struct {
StartTime time.Time `json:"startTime" required:"true"`
EndTime *time.Time `json:"endTime,omitempty"`
Duration valuer.TextDuration `json:"duration" required:"true"`
RepeatType RepeatType `json:"repeatType" required:"true"`
RepeatOn []RepeatOn `json:"repeatOn"`
}
func (r *Recurrence) Scan(src interface{}) error {
switch data := src.(type) {
case []byte:
return json.Unmarshal(data, r)
case string:
return json.Unmarshal([]byte(data), r)
case nil:
return nil
default:
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "recurrence: (unsupported \"%s\")", reflect.TypeOf(data).String())
}
}
func (r *Recurrence) Value() (driver.Value, error) {
return json.Marshal(r)
}
func (s Schedule) MarshalJSON() ([]byte, error) {
@@ -66,13 +124,13 @@ func (s Schedule) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Timezone string `json:"timezone"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime,omitzero"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
Recurrence *Recurrence `json:"recurrence,omitempty"`
}{
Timezone: s.Timezone,
StartTime: startTime,
EndTime: endTime,
StartTime: startTime.Format(time.RFC3339),
EndTime: endTime.Format(time.RFC3339),
Recurrence: recurrence,
})
}

View File

@@ -0,0 +1,34 @@
package ruletypes
import (
"database/sql/driver"
"encoding/json"
"reflect"
"time"
"github.com/SigNoz/signoz/pkg/errors"
)
type Schedule struct {
Timezone string `json:"timezone" required:"true"`
StartTime time.Time `json:"startTime,omitempty"`
EndTime time.Time `json:"endTime,omitempty"`
Recurrence *Recurrence `json:"recurrence"`
}
func (s *Schedule) Scan(src interface{}) error {
switch data := src.(type) {
case []byte:
return json.Unmarshal(data, s)
case string:
return json.Unmarshal([]byte(data), s)
case nil:
return nil
default:
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "schedule: (unsupported \"%s\")", reflect.TypeOf(data).String())
}
}
func (s *Schedule) Value() (driver.Value, error) {
return json.Marshal(s)
}

View File

@@ -1,14 +1,18 @@
import os
import platform
import shutil
import subprocess
import time
from dataclasses import dataclass
from http import HTTPStatus
from os import path
from pathlib import Path
import docker
import docker.errors
import pytest
import requests
from testcontainers.core.container import DockerContainer, Network
from testcontainers.core.image import DockerImage
from fixtures import reuse, types
from fixtures.logger import setup_logger
@@ -16,6 +20,77 @@ from fixtures.logger import setup_logger
logger = setup_logger(__name__)
@dataclass
class SigNozImageBuild:
process: subprocess.Popen
command: list[str]
cache_path: Path | None = None
next_cache_path: Path | None = None
def start_signoz_image_build(pytestconfig: pytest.Config, dockerfile_path: str, arch: str, zeus_url: str) -> SigNozImageBuild:
root = pytestconfig.rootpath.parent
command = [
"docker",
"buildx",
"build",
"--load",
"--progress",
"plain",
"--tag",
"signoz:integration",
"--file",
dockerfile_path,
"--build-arg",
f"TARGETARCH={arch}",
"--build-arg",
f"ZEUSURL={zeus_url}",
str(root),
]
cache_path = None
next_cache_path = None
if build_cache_dir := os.environ.get("SIGNOZ_INTEGRATION_BUILD_CACHE_DIR"):
cache_path = Path(build_cache_dir)
next_cache_path = Path(f"{build_cache_dir}-next")
cache_path.parent.mkdir(parents=True, exist_ok=True)
shutil.rmtree(next_cache_path, ignore_errors=True)
if cache_path.exists():
command.extend(["--cache-from", f"type=local,src={cache_path}"])
command.extend(["--cache-to", f"type=local,dest={next_cache_path},mode=max,ignore-error=true"])
logger.info("Building SigNoz integration image with %s", " ".join(command))
return SigNozImageBuild(
process=subprocess.Popen(command, cwd=root),
command=command,
cache_path=cache_path,
next_cache_path=next_cache_path,
)
def wait_for_signoz_image_build(build: SigNozImageBuild) -> None:
returncode = build.process.wait()
if returncode != 0:
raise subprocess.CalledProcessError(returncode, build.command)
if build.cache_path and build.next_cache_path and build.next_cache_path.exists():
shutil.rmtree(build.cache_path, ignore_errors=True)
shutil.move(build.next_cache_path, build.cache_path)
def stop_signoz_image_build(build: SigNozImageBuild) -> None:
if build.process.poll() is not None:
return
build.process.terminate()
try:
build.process.wait(timeout=10)
except subprocess.TimeoutExpired:
build.process.kill()
build.process.wait()
def create_signoz(
network: Network,
zeus: types.TestContainerDocker,
@@ -33,9 +108,6 @@ def create_signoz(
"""
def create() -> types.SigNoz:
# Run the migrations for clickhouse
request.getfixturevalue("migrator")
# Get the no-web flag
with_web = pytestconfig.getoption("--with-web")
@@ -48,19 +120,15 @@ def create_signoz(
if with_web:
dockerfile_path = "cmd/enterprise/Dockerfile.with-web.integration"
# Docker build context is the repo root — one up from pytest's
# rootdir (tests/).
self = DockerImage(
path=str(pytestconfig.rootpath.parent),
dockerfile_path=dockerfile_path,
tag="signoz:integration",
buildargs={
"TARGETARCH": arch,
"ZEUSURL": zeus.container_configs["8080"].base(),
},
)
self.build()
image_build = start_signoz_image_build(pytestconfig, dockerfile_path, arch, zeus.container_configs["8080"].base())
try:
# The SigNoz image does not depend on ClickHouse migrations, so
# build it while the migrator container runs.
request.getfixturevalue("migrator")
wait_for_signoz_image_build(image_build)
except Exception: # pylint: disable=broad-exception-caught
stop_signoz_image_build(image_build)
raise
env = (
{