Compare commits

..

2 Commits

Author SHA1 Message Date
Ashwin Bhatkal
914e87158b test: add teardown bits 2026-05-12 21:11:28 +05:30
Ashwin Bhatkal
b98359a785 test: new playwright project to seed data 2026-05-12 20:59:47 +05:30
55 changed files with 8297 additions and 1534 deletions

View File

@@ -4802,8 +4802,6 @@ components:
type: string
kind:
$ref: '#/components/schemas/RuletypesMaintenanceKind'
labelExpression:
type: string
name:
type: string
schedule:
@@ -4831,8 +4829,6 @@ components:
type: array
description:
type: string
labelExpression:
type: string
name:
type: string
schedule:
@@ -4897,6 +4893,10 @@ components:
properties:
duration:
type: string
endTime:
format: date-time
nullable: true
type: string
repeatOn:
items:
$ref: '#/components/schemas/RuletypesRepeatOn'
@@ -4904,7 +4904,11 @@ components:
type: array
repeatType:
$ref: '#/components/schemas/RuletypesRepeatType'
startTime:
format: date-time
type: string
required:
- startTime
- duration
- repeatType
type: object
@@ -5068,7 +5072,6 @@ components:
type: string
required:
- timezone
- startTime
type: object
RuletypesScheduleType:
enum:

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

@@ -7042,10 +7042,6 @@ export interface RuletypesPlannedMaintenanceDTO {
*/
id: string;
kind: RuletypesMaintenanceKindDTO;
/**
* @type string
*/
labelExpression?: string;
/**
* @type string
*/
@@ -7073,10 +7069,6 @@ export interface RuletypesPostablePlannedMaintenanceDTO {
* @type string
*/
description?: string;
/**
* @type string
*/
labelExpression?: string;
/**
* @type string
*/
@@ -7150,12 +7142,23 @@ export interface RuletypesRecurrenceDTO {
* @type string
*/
duration: string;
/**
* @type string
* @format date-time
* @nullable true
*/
endTime?: Date | null;
/**
* @type array
* @nullable true
*/
repeatOn?: RuletypesRepeatOnDTO[] | null;
repeatType: RuletypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: Date;
}
export interface RuletypesRenotifyDTO {
@@ -7337,7 +7340,7 @@ export interface RuletypesScheduleDTO {
* @type string
* @format date-time
*/
startTime: Date;
startTime?: Date;
/**
* @type string
*/

View File

@@ -81,20 +81,6 @@
}
}
.alert-rule-scope {
margin-bottom: 12px;
.ant-radio-wrapper {
color: var(--l1-foreground);
}
}
.alert-rule-all-warning {
font-size: 12px;
font-weight: 400;
color: var(--l2-foreground);
}
.formItemWithBullet {
margin-bottom: 0;
}

View File

@@ -17,7 +17,6 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { USER_ROLES } from 'types/roles';
import { DeepPartial } from 'utils/types';
import 'dayjs/locale/en';
@@ -25,7 +24,7 @@ import { PlannedDowntimeDeleteModal } from './PlannedDowntimeDeleteModal';
import { PlannedDowntimeForm } from './PlannedDowntimeForm';
import { PlannedDowntimeList } from './PlannedDowntimeList';
import {
defaultInitialValues,
defautlInitialValues,
deleteDowntimeHandler,
} from './PlannedDowntimeutils';
@@ -49,8 +48,8 @@ export function PlannedDowntime(): JSX.Element {
const urlQuery = useUrlQuery();
const [initialValues, setInitialValues] =
useState<DeepPartial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>>(
defaultInitialValues,
useState<Partial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>>(
defautlInitialValues,
);
const downtimeSchedules = useListDowntimeSchedules();
@@ -149,7 +148,7 @@ export function PlannedDowntime(): JSX.Element {
<Button
type="primary"
onClick={(): void => {
setInitialValues({ ...defaultInitialValues, editMode: false });
setInitialValues({ ...defautlInitialValues, editMode: false });
setIsOpen(true);
setEditMode(false);
form.resetFields();

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Check, Info } from '@signozhq/icons';
import { Check } from '@signozhq/icons';
import {
Button,
DatePicker,
@@ -8,15 +8,12 @@ import {
FormInstance,
Input,
Modal,
Radio,
Select,
SelectProps,
Spin,
Tooltip,
} from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { DefaultOptionType } from 'antd/es/select';
import { DatePickerProps } from 'antd/es/date-picker';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
createDowntimeSchedule,
@@ -42,7 +39,6 @@ import { defaultTo, isEmpty } from 'lodash-es';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ALL_TIME_ZONES } from 'utils/timeZoneUtil';
import { DeepPartial } from 'utils/types';
import 'dayjs/locale/en';
@@ -68,23 +64,20 @@ const TIME_FORMAT = DATE_TIME_FORMATS.TIME;
const DATE_FORMAT = DATE_TIME_FORMATS.ORDINAL_DATE;
const ORDINAL_FORMAT = DATE_TIME_FORMATS.ORDINAL_ONLY;
type AlertRuleScope = 'all' | 'specific';
interface PlannedDowntimeFormData {
name: string;
startTime: dayjs.Dayjs | string;
endTime: dayjs.Dayjs | string;
recurrence?: RuletypesRecurrenceDTO | null;
alertRuleScope: AlertRuleScope;
alertRules: DefaultOptionType[];
recurrenceSelect?: RuletypesRecurrenceDTO;
timezone?: string;
labelExpression?: string;
}
const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
interface PlannedDowntimeFormProps {
initialValues: DeepPartial<
initialValues: Partial<
RuletypesPlannedMaintenanceDTO & {
editMode: boolean;
}
@@ -96,7 +89,7 @@ interface PlannedDowntimeFormProps {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
refetchAllSchedules: () => void;
isEditMode: boolean;
form: FormInstance;
form: FormInstance<any>;
}
export function PlannedDowntimeForm(
@@ -133,12 +126,6 @@ export function PlannedDowntimeForm(
recurrenceOptions.doesNotRepeat.value,
);
const [alertRuleScope, setAlertRuleScope] = useState<AlertRuleScope>(
initialValues.id && (initialValues.alertIds || []).length === 0
? 'all'
: 'specific',
);
const timezoneInitialValue = !isEmpty(initialValues.schedule?.timezone)
? (initialValues.schedule?.timezone as string)
: undefined;
@@ -146,28 +133,26 @@ export function PlannedDowntimeForm(
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const datePickerFooter: DatePickerProps['renderExtraFooter'] = (mode) =>
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 shouldKeepLocalTime = !isEditMode;
const data: RuletypesPostablePlannedMaintenanceDTO = {
alertIds:
values.alertRuleScope === 'all'
? []
: (values.alertRules
.map((alert) => alert.value)
.filter((alert) => alert !== undefined) as string[]),
alertIds: values.alertRules
.map((alert) => alert.value)
.filter((alert) => alert !== undefined) as string[],
name: values.name,
labelExpression: values.labelExpression,
schedule: {
startTime: new Date(
handleTimeConversion(
values.startTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
),
timezone: values.timezone as string,
@@ -177,6 +162,7 @@ export function PlannedDowntimeForm(
values.endTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
)
: undefined,
@@ -217,24 +203,38 @@ export function PlannedDowntimeForm(
],
);
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}`
duration: values.recurrence?.duration
? `${values.recurrence?.duration}${durationUnit}`
: undefined,
repeatOn: recurrence.repeatOn?.length ? recurrence.repeatOn : undefined,
repeatType: recurrence.repeatType,
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,
};
const payloadValues = {
...values,
recurrence: recurrenceData as RuletypesRecurrenceDTO | undefined,
};
await saveHandler(payloadValues);
await saveHanlder(payloadValues);
};
const formValidationRules = [
@@ -275,20 +275,19 @@ export function PlannedDowntimeForm(
};
const formatedInitialValues = useMemo(() => {
const initialAlertIds = initialValues.alertIds || [];
const isExistingSchedule = Boolean(initialValues.id);
const formData: PlannedDowntimeFormData = {
name: defaultTo(initialValues.name, ''),
alertRuleScope:
isExistingSchedule && initialAlertIds.length === 0 ? 'all' : 'specific',
alertRules: getAlertOptionsFromIds(initialAlertIds, alertOptions),
alertRules: getAlertOptionsFromIds(
initialValues.alertIds || [],
alertOptions,
),
endTime: getEndTime(initialValues) ? dayjs(getEndTime(initialValues)) : '',
startTime: initialValues.schedule?.startTime
? dayjs(initialValues.schedule?.startTime)
: '',
recurrence: {
...initialValues.schedule?.recurrence,
repeatType: (!isScheduleRecurring(initialValues.schedule)
repeatType: (!isScheduleRecurring(initialValues?.schedule)
? recurrenceOptions.doesNotRepeat.value
: initialValues.schedule?.recurrence
?.repeatType) as RuletypesRecurrenceDTO['repeatType'],
@@ -298,14 +297,12 @@ export function PlannedDowntimeForm(
),
} as RuletypesRecurrenceDTO,
timezone: initialValues.schedule?.timezone as string,
labelExpression: initialValues.labelExpression || '',
};
return formData;
}, [initialValues, alertOptions]);
useEffect(() => {
setSelectedTags(formatedInitialValues.alertRules);
setAlertRuleScope(formatedInitialValues.alertRuleScope);
form.setFieldsValue({ ...formatedInitialValues });
}, [form, formatedInitialValues, initialValues]);
@@ -320,6 +317,7 @@ export function PlannedDowntimeForm(
const getTimezoneFormattedTime = (
time: string | dayjs.Dayjs,
timeZone?: string,
isEditMode?: boolean,
format?: string,
): string => {
if (!time) {
@@ -328,11 +326,20 @@ export function PlannedDowntimeForm(
if (!timeZone) {
return dayjs(time).format(format);
}
return dayjs(time).tz(timeZone).format(format);
return dayjs(time).tz(timeZone, isEditMode).format(format);
};
const startTimeText = useMemo((): string => {
let 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 '';
}
@@ -342,6 +349,7 @@ export function PlannedDowntimeForm(
startTime,
timezoneInitialValue,
formData?.timezone,
!isEditMode,
);
}
const daysOfWeek = formData?.recurrence?.repeatOn;
@@ -349,16 +357,21 @@ export function PlannedDowntimeForm(
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,
);
@@ -375,10 +388,21 @@ export function PlannedDowntimeForm(
default:
return `Scheduled for ${formattedStartDate} starting at ${formattedStartTime}.`;
}
}, [formData, recurrenceType, timezoneInitialValue]);
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
const endTimeText = useMemo((): string => {
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 '';
}
@@ -388,21 +412,25 @@ export function PlannedDowntimeForm(
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, timezoneInitialValue]);
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
return (
<Modal
@@ -425,9 +453,6 @@ export function PlannedDowntimeForm(
onFinish={onFinish}
onValuesChange={(): void => {
setRecurrenceType(form.getFieldValue('recurrence')?.repeatType as string);
setAlertRuleScope(
form.getFieldValue('alertRuleScope') as AlertRuleScope,
);
setFormData(form.getFieldsValue());
}}
autoComplete="off"
@@ -440,7 +465,7 @@ export function PlannedDowntimeForm(
name="startTime"
rules={formValidationRules}
className={!isEmpty(startTimeText) ? 'formItemWithBullet' : ''}
getValueProps={(value) => ({
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
>
@@ -521,7 +546,7 @@ export function PlannedDowntimeForm(
},
]}
className={!isEmpty(endTimeText) ? 'formItemWithBullet' : ''}
getValueProps={(value) => ({
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
>
@@ -539,99 +564,50 @@ export function PlannedDowntimeForm(
<div className="scheduleTimeInfoText">{endTimeText}</div>
)}
<div>
<Typography style={{ marginBottom: 8 }}>Silence Alerts</Typography>
<Form.Item
name="alertRuleScope"
initialValue="specific"
className="alert-rule-scope"
>
<Radio.Group>
<Radio value="all">All alert rules</Radio>
<Radio value="specific">Specific alert rules</Radio>
</Radio.Group>
</Form.Item>
{alertRuleScope === 'specific' && (
<>
<Form.Item noStyle shouldUpdate>
<AlertRuleTags
closable
selectedTags={selectedTags}
handleClose={handleClose}
/>
</Form.Item>
<Form.Item
name={alertRuleFormName}
rules={[
{
validator: async (
_rule,
value: DefaultOptionType[] | undefined,
): Promise<void> => {
if (!value || value.length === 0) {
throw new Error(
'Select at least one alert rule, or choose "All alert rules" to silence everything.',
);
}
},
},
]}
>
<Select
placeholder="Search for alerts rules or groups..."
mode="multiple"
status={isError ? 'error' : undefined}
loading={isLoading}
tagRender={noTagRenderer}
onChange={handleChange}
showSearch
options={alertOptions}
filterOption={(input, option): boolean =>
(option?.label as string)?.toLowerCase()?.includes(input.toLowerCase())
}
notFoundContent={
isLoading ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No alert available.</span>
)
}
>
{alertOptions?.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
</>
)}
{alertRuleScope === 'all' && (
<Typography
className="alert-rule-info alert-rule-all-warning"
style={{ marginBottom: 16 }}
>
All alerts will be silenced for the duration of this maintenance window.
<div className="alert-rule-form">
<Typography style={{ marginBottom: 8 }}>Silence Alerts</Typography>
<Typography style={{ marginBottom: 8 }} className="alert-rule-info">
(Leave empty to silence all alerts)
</Typography>
)}
</div>
<Form.Item noStyle shouldUpdate>
<AlertRuleTags
closable
selectedTags={selectedTags}
handleClose={handleClose}
/>
</Form.Item>
<Form.Item name={alertRuleFormName}>
<Select
placeholder="Search for alerts rules or groups..."
mode="multiple"
status={isError ? 'error' : undefined}
loading={isLoading}
tagRender={noTagRenderer}
onChange={handleChange}
showSearch
options={alertOptions}
filterOption={(input, option): boolean =>
(option?.label as string)?.toLowerCase()?.includes(input.toLowerCase())
}
notFoundContent={
isLoading ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No alert available.</span>
)
}
>
{alertOptions?.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
</div>
<Form.Item
label={
<span>
Label Expression&nbsp;
<Tooltip title='Filter by alert labels. Examples: env == "prod", region == "us-east-1" && severity == "critical"'>
<Info size={13} />
</Tooltip>
</span>
}
name="labelExpression"
>
<Input.TextArea
placeholder='e.g. env == "prod" && region == "us-east-1"'
autoSize={{ minRows: 2, maxRows: 4 }}
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<ModalButtonWrapper>
<Button

View File

@@ -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';
@@ -18,9 +18,8 @@ import { defaultTo } from 'lodash-es';
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 { DeepPartial } from 'utils/types';
import { showErrorNotification } from '../../utils/error';
import {
formatDateTime,
getAlertOptionsFromIds,
@@ -28,6 +27,7 @@ import {
getEndTime,
recurrenceInfo,
} from './PlannedDowntimeutils';
import './PlannedDowntime.styles.scss';
const { Panel } = Collapse;
@@ -136,7 +136,7 @@ export function CollapseListContent({
created_at?: string;
created_by_name?: string;
created_by_email?: string;
timeframe: [string | undefined, string | undefined];
timeframe: [string | undefined | null, string | undefined | null];
repeats?: RuletypesRecurrenceDTO | null;
updated_at?: string;
updated_by_name?: string;
@@ -192,12 +192,7 @@ export function CollapseListContent({
),
)}
{renderItems('Timezone', <Typography>{timezone || '-'}</Typography>)}
{renderItems(
'Repeats',
<Typography>
{recurrenceInfo(timeframe[0], timeframe[1], repeats)}
</Typography>,
)}
{renderItems('Repeats', <Typography>{recurrenceInfo(repeats)}</Typography>)}
{renderItems(
'Alerts silenced',
alertOptions?.length ? (
@@ -207,7 +202,7 @@ export function CollapseListContent({
selectedTags={alertOptions}
/>
) : (
<Tag className="all-alerts-tag">All alert rules</Tag>
'-'
),
)}
</Flex>
@@ -217,7 +212,7 @@ export function CollapseListContent({
export function CustomCollapseList(
props: DowntimeSchedulesTableData & {
setInitialValues: React.Dispatch<
React.SetStateAction<DeepPartial<RuletypesPlannedMaintenanceDTO>>
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: string, name: string) => void;
@@ -288,7 +283,9 @@ export function CustomCollapseList(
schedule?.startTime?.toString(),
typeof endTime === 'string' ? endTime : endTime?.toString(),
]}
repeats={schedule?.recurrence}
repeats={
schedule?.recurrence as RuletypesRecurrenceDTO | null | undefined
}
updated_at={updatedAt ? dayjs(updatedAt).toISOString() : ''}
updated_by_name={defaultTo(updatedBy, '')}
alertOptions={alertOptions}
@@ -323,7 +320,7 @@ export function PlannedDowntimeList({
>;
alertOptions: DefaultOptionType[];
setInitialValues: React.Dispatch<
React.SetStateAction<DeepPartial<RuletypesPlannedMaintenanceDTO>>
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: string, name: string) => void;

View File

@@ -2,7 +2,7 @@ import { UseMutateAsyncFunction } from 'react-query';
import type { NotificationInstance } from 'antd/es/notification/interface';
import type { DefaultOptionType } from 'antd/es/select';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
import type {
DeleteDowntimeScheduleByIDPathParameters,
RenderErrorResponseDTO,
RuletypesPlannedMaintenanceDTO,
@@ -14,7 +14,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { isEmpty, isEqual } from 'lodash-es';
import APIError from 'types/api/error';
import { DeepPartial } from 'utils/types';
type DateTimeString = string | null | undefined;
@@ -61,15 +60,13 @@ export const getAlertOptionsFromIds = (
);
export const recurrenceInfo = (
startTime?: string,
endTime?: string,
recurrence?: RuletypesRecurrenceDTO | null,
): string => {
if (!recurrence) {
return 'No';
}
const { duration, repeatOn, repeatType } = recurrence;
const { startTime, duration, repeatOn, repeatType, endTime } = recurrence;
const formattedStartTime = startTime
? formatDateTime(dayjs(startTime).toISOString())
@@ -83,7 +80,7 @@ export const recurrenceInfo = (
return `Repeats - ${repeatType} ${weeklyRepeatString} from ${formattedStartTime} ${formattedEndTime} ${durationString}`;
};
export const defaultInitialValues: DeepPartial<
export const defautlInitialValues: Partial<
RuletypesPlannedMaintenanceDTO & { editMode: boolean }
> = {
name: '',
@@ -213,17 +210,39 @@ 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,
}: DeepPartial<
}: Partial<
RuletypesPlannedMaintenanceDTO & {
editMode: boolean;
}
>): string | dayjs.Dayjs =>
schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '';
>): 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?: DeepPartial<RuletypesPlannedMaintenanceDTO['schedule']> | null,
schedule?: RuletypesPlannedMaintenanceDTO['schedule'] | null,
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);
function convertUtcOffsetToTimezoneOffset(offsetMinutes: number): string {
@@ -253,6 +272,7 @@ 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);
@@ -260,5 +280,5 @@ export function handleTimeConversion(
const formattedTime = formatWithTimezone(initialTime, timezone);
return timezoneChanged
? formattedTime
: dayjs(dateValue).tz(timezone).format();
: dayjs(dateValue).tz(timezone, shouldKeepLocalTime).format();
}

View File

@@ -8,7 +8,7 @@ import {
} from 'api/generated/services/sigNoz.schemas';
export const buildSchedule = (
schedule: RuletypesScheduleDTO,
schedule: Partial<RuletypesScheduleDTO>,
): RuletypesScheduleDTO => ({
timezone: schedule?.timezone ?? '',
startTime: schedule?.startTime,
@@ -17,13 +17,16 @@ export const buildSchedule = (
});
export const createMockDowntime = (
overrides: Partial<RuletypesPlannedMaintenanceDTO> &
Pick<RuletypesPlannedMaintenanceDTO, 'schedule'>,
overrides: Partial<RuletypesPlannedMaintenanceDTO>,
): RuletypesPlannedMaintenanceDTO => ({
id: overrides.id ?? '0',
name: overrides.name ?? '',
description: overrides.description ?? '',
schedule: overrides.schedule,
schedule: buildSchedule({
timezone: 'UTC',
startTime: new Date('2024-01-01'),
...overrides.schedule,
}),
alertIds: overrides.alertIds ?? [],
createdAt: overrides.createdAt,
createdBy: overrides.createdBy ?? '',

View File

@@ -19,6 +19,7 @@ export type ServerError = 500;
export type SuccessStatusCode = Created | Success | SuccessNoContent;
export type ErrorStatusCode =
| Forbidden
| Forbidden
| Unauthorized
| NotFound

View File

@@ -1,22 +0,0 @@
type Builtin =
| string
| number
| boolean
| bigint
| symbol
| null
| undefined
| Function // eslint-disable-line
| Date
| RegExp
| Error;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;

View File

@@ -24,7 +24,6 @@ import (
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/provider/mem"
promTypes "github.com/prometheus/alertmanager/types"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
@@ -365,7 +364,7 @@ route:
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := promTypes.NewMarker(prometheus.NewRegistry())
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
@@ -638,7 +637,7 @@ route:
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := promTypes.NewMarker(prometheus.NewRegistry())
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
@@ -897,7 +896,7 @@ route:
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := promTypes.NewMarker(prometheus.NewRegistry())
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
@@ -1159,7 +1158,7 @@ func newAlert(labels model.LabelSet) *alertmanagertypes.Alert {
func TestDispatcherRace(t *testing.T) {
logger := promslog.NewNopLogger()
marker := promTypes.NewMarker(prometheus.NewRegistry())
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
@@ -1195,7 +1194,7 @@ route:
route := dispatch.NewRoute(conf.Route, nil)
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
marker := promTypes.NewMarker(prometheus.NewRegistry())
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
@@ -1265,7 +1264,7 @@ route:
func TestDispatcher_DoMaintenance(t *testing.T) {
r := prometheus.NewRegistry()
marker := promTypes.NewMarker(r)
marker := alertmanagertypes.NewMarker(r)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Minute, 0, nil, promslog.NewNopLogger(), prometheus.NewRegistry(), nil)
if err != nil {
@@ -1371,7 +1370,7 @@ route:
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := promTypes.NewMarker(prometheus.NewRegistry())
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)

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, lset) {
return true
}
}
return false
}
// MutedBy returns the IDs of all active maintenance windows currently
// suppressing the alert identified by lset. It is used to populate the
// `mutedBy` field on the v2 API alert response so that maintenance-suppressed
// alerts surface as `state=suppressed` in GetAlerts responses.
func (m *MaintenanceMuter) MutedBy(ctx context.Context, lset model.LabelSet) []string {
ruleID := string(lset[ruletypes.AlertRuleIDLabel])
if ruleID == "" {
return nil
}
var ids []string
now := time.Now()
for _, mw := range m.getMaintenances(ctx) {
if mw.ShouldSkip(ruleID, now, lset) {
ids = append(ids, mw.ID.String())
}
}
return ids
}
func (m *MaintenanceMuter) getMaintenances(ctx context.Context) []*alertmanagertypes.PlannedMaintenance {
m.mu.RLock()
if time.Now().Before(m.cacheExpiry) {
cached := m.cached
m.mu.RUnlock()
return cached
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
// Double-check after acquiring write lock.
if time.Now().Before(m.cacheExpiry) {
return m.cached
}
mws, err := m.maintenanceStore.ListPlannedMaintenance(ctx, m.orgID)
if err != nil {
m.logger.ErrorContext(ctx, "failed to list planned maintenance windows; alerts will not be suppressed", slog.String("org_id", m.orgID))
return m.cached // return stale (potentially empty) cache on error
}
m.cached = mws
m.cacheExpiry = time.Now().Add(maintenanceCacheTTL)
return m.cached
}

View File

@@ -1,246 +0,0 @@
package alertmanagerserver
import (
"context"
"errors"
"log/slog"
"sort"
"sync"
"testing"
"time"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// fakeMaintenanceStore implements alertmanagertypes.MaintenanceStore with an in-memory list.
// It counts ListPlannedMaintenance calls so tests can assert caching behavior.
type fakeMaintenanceStore struct {
mu sync.Mutex
items []*alertmanagertypes.PlannedMaintenance
err error
calls int
}
func (s *fakeMaintenanceStore) ListPlannedMaintenance(_ context.Context, _ string) ([]*alertmanagertypes.PlannedMaintenance, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.calls++
if s.err != nil {
return nil, s.err
}
return s.items, nil
}
func (s *fakeMaintenanceStore) callCount() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.calls
}
func (s *fakeMaintenanceStore) setError(err error) {
s.mu.Lock()
defer s.mu.Unlock()
s.err = err
}
// Remaining MaintenanceStore methods — unused by MaintenanceMuter.
func (s *fakeMaintenanceStore) CreatePlannedMaintenance(context.Context, *alertmanagertypes.PostablePlannedMaintenance) (*alertmanagertypes.PlannedMaintenance, error) {
return nil, nil
}
func (s *fakeMaintenanceStore) DeletePlannedMaintenance(context.Context, valuer.UUID) error {
return nil
}
func (s *fakeMaintenanceStore) GetPlannedMaintenanceByID(context.Context, valuer.UUID) (*alertmanagertypes.PlannedMaintenance, error) {
return nil, nil
}
func (s *fakeMaintenanceStore) UpdatePlannedMaintenance(context.Context, *alertmanagertypes.PostablePlannedMaintenance, valuer.UUID) error {
return nil
}
func newMuter(t *testing.T, items ...*alertmanagertypes.PlannedMaintenance) (*MaintenanceMuter, *fakeMaintenanceStore) {
t.Helper()
store := &fakeMaintenanceStore{items: items}
muter := NewMaintenanceMuter(store, "org-1", slog.New(slog.DiscardHandler))
return muter, store
}
// 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{model.LabelName(ruletypes.AlertRuleIDLabel): model.LabelValue(ruleID)}
}
func TestMutes_EmptyRuleIDLabel(t *testing.T) {
muter, store := newMuter(t, activeFixed())
assert.False(t, muter.Mutes(context.Background(), model.LabelSet{}))
// Short-circuit: no store lookup needed when the label is missing.
assert.Equal(t, 0, store.callCount())
}
func TestMutes_NoMaintenanceWindows(t *testing.T) {
muter, _ := newMuter(t)
assert.False(t, muter.Mutes(context.Background(), labelsFor("rule-1")))
}
func TestMutes_MatchingRule(t *testing.T) {
mw := activeFixed("rule-1", "rule-2")
muter, _ := newMuter(t, mw)
assert.True(t, muter.Mutes(context.Background(), labelsFor("rule-1")))
assert.True(t, muter.Mutes(context.Background(), labelsFor("rule-2")))
}
func TestMutes_NonMatchingRule(t *testing.T) {
muter, _ := newMuter(t, activeFixed("rule-1"))
assert.False(t, muter.Mutes(context.Background(), labelsFor("rule-other")))
}
func TestMutes_EmptyRuleIDsMatchesAllRules(t *testing.T) {
// A maintenance with no RuleIDs is treated as scoping every rule.
muter, _ := newMuter(t, activeFixed())
assert.True(t, muter.Mutes(context.Background(), labelsFor("any-rule")))
}
func TestMutes_FutureWindowDoesNotMute(t *testing.T) {
muter, _ := newMuter(t, futureFixed("rule-1"))
assert.False(t, muter.Mutes(context.Background(), labelsFor("rule-1")))
}
func TestMutes_AnyOfMultipleWindowsMatches(t *testing.T) {
muter, _ := newMuter(t,
futureFixed("rule-1"),
activeFixed("rule-1"),
)
assert.True(t, muter.Mutes(context.Background(), labelsFor("rule-1")))
}
func TestMutedBy_EmptyRuleIDLabel(t *testing.T) {
muter, store := newMuter(t, activeFixed())
assert.Nil(t, muter.MutedBy(context.Background(), model.LabelSet{}))
assert.Equal(t, 0, store.callCount())
}
func TestMutedBy_NoMatches(t *testing.T) {
muter, _ := newMuter(t, activeFixed("rule-1"), futureFixed("rule-1"))
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")
muter, _ := newMuter(t, mw1, mw2, mw3, mw4)
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) {
muter, store := newMuter(t, activeFixed("rule-1"))
ctx := context.Background()
for i := 0; i < 5; i++ {
require.True(t, muter.Mutes(ctx, labelsFor("rule-1")))
}
assert.Equal(t, 1, store.callCount(), "cached results should suppress further store lookups within TTL")
}
func TestCache_StoreErrorReturnsStaleCache(t *testing.T) {
mw := activeFixed("rule-1")
muter, store := newMuter(t, mw)
ctx := context.Background()
// First call populates the cache from a working store.
require.True(t, muter.Mutes(ctx, labelsFor("rule-1")))
require.Equal(t, 1, store.callCount())
// 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.
store.setError(errors.New("boom"))
assert.True(t, muter.Mutes(ctx, labelsFor("rule-1")),
"on store error, muter should keep using the last known cache to avoid losing suppression")
assert.Equal(t, 2, store.callCount())
}
func TestCache_ExpiredCacheRefetchesUpdatedData(t *testing.T) {
mw := activeFixed("rule-1")
muter, store := newMuter(t, mw)
ctx := context.Background()
require.True(t, muter.Mutes(ctx, labelsFor("rule-1")))
require.Equal(t, 1, store.callCount())
// Drop the maintenance window from the store and expire the cache.
store.mu.Lock()
store.items = nil
store.mu.Unlock()
muter.mu.Lock()
muter.cacheExpiry = time.Time{}
muter.mu.Unlock()
assert.False(t, muter.Mutes(ctx, labelsFor("rule-1")))
assert.Equal(t, 2, store.callCount())
}
func TestMutes_IsConcurrencySafe(t *testing.T) {
muter, store := newMuter(t, activeFixed("rule-1"))
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.
assert.Equal(t, 1, store.callCount())
}

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

@@ -7,10 +7,6 @@ import (
"testing"
"time"
sqlmock "github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes/alertmanagertypestest"
"github.com/prometheus/alertmanager/dispatch"
@@ -94,8 +90,7 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
stateStore := alertmanagertypestest.NewStateStore()
registry := prometheus.NewRegistry()
logger := slog.New(slog.DiscardHandler)
maintenanceStore := sqlalertmanagerstore.NewMaintenanceStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual))
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)

View File

@@ -10,11 +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/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"
@@ -27,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)
}
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")
@@ -46,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")
@@ -94,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")
@@ -142,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")
@@ -247,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

@@ -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,16 +4,18 @@ 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"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
@@ -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{}),
}

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"
)
@@ -116,29 +115,13 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rules/{id}/mute", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.MuteRule), handler.OpenAPIDef{
ID: "MuteRule",
Tags: []string{"rules"},
Summary: "Mute alert rule",
Description: "This endpoint mutes an alert rule until the provided endTime by creating a fixed-window planned maintenance scoped to that rule. Unmute by deleting the returned downtime schedule.",
Request: new(alertmanagertypes.PostableMuteRule),
RequestContentType: "application/json",
Response: new(alertmanagertypes.PlannedMaintenance),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListDowntimeSchedules), handler.OpenAPIDef{
ID: "ListDowntimeSchedules",
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),
@@ -151,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},
@@ -165,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},
@@ -181,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

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

View File

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

@@ -10,7 +10,6 @@ type Handler interface {
DeleteRuleByID(http.ResponseWriter, *http.Request)
PatchRuleByID(http.ResponseWriter, *http.Request)
TestRule(http.ResponseWriter, *http.Request)
MuteRule(http.ResponseWriter, *http.Request)
ListDowntimeSchedules(http.ResponseWriter, *http.Request)
GetDowntimeScheduleByID(http.ResponseWriter, *http.Request)

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"
@@ -7,8 +7,8 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"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"
)
@@ -16,12 +16,12 @@ type maintenance struct {
sqlstore sqlstore.SQLStore
}
func NewMaintenanceStore(store sqlstore.SQLStore) alertmanagertypes.MaintenanceStore {
func NewMaintenanceStore(store sqlstore.SQLStore) ruletypes.MaintenanceStore {
return &maintenance{sqlstore: store}
}
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().
@@ -33,7 +33,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 {
gettablePlannedMaintenance = append(gettablePlannedMaintenance, gettableMaintenancesRule.ToPlannedMaintenance())
}
@@ -41,8 +41,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().
@@ -57,13 +57,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(),
},
@@ -75,21 +75,20 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
CreatedBy: claims.Email,
UpdatedBy: claims.Email,
},
Name: maintenance.Name,
Description: maintenance.Description,
Schedule: maintenance.Schedule,
OrgID: claims.OrgID,
LabelExpression: maintenance.LabelExpression,
Name: maintenance.Name,
Description: maintenance.Description,
Schedule: maintenance.Schedule,
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(),
},
@@ -114,6 +113,7 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
NewInsert().
Model(&maintenanceRules).
Exec(ctx)
if err != nil {
return err
}
@@ -125,17 +125,16 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
return nil, err
}
return &alertmanagertypes.PlannedMaintenance{
ID: storablePlannedMaintenance.ID,
Name: storablePlannedMaintenance.Name,
Description: storablePlannedMaintenance.Description,
Schedule: storablePlannedMaintenance.Schedule,
RuleIDs: maintenance.AlertIds,
LabelExpression: maintenance.LabelExpression,
CreatedAt: storablePlannedMaintenance.CreatedAt,
CreatedBy: storablePlannedMaintenance.CreatedBy,
UpdatedAt: storablePlannedMaintenance.UpdatedAt,
UpdatedBy: storablePlannedMaintenance.UpdatedBy,
return &ruletypes.PlannedMaintenance{
ID: storablePlannedMaintenance.ID,
Name: storablePlannedMaintenance.Name,
Description: storablePlannedMaintenance.Description,
Schedule: storablePlannedMaintenance.Schedule,
RuleIDs: maintenance.AlertIds,
CreatedAt: storablePlannedMaintenance.CreatedAt,
CreatedBy: storablePlannedMaintenance.CreatedBy,
UpdatedAt: storablePlannedMaintenance.UpdatedAt,
UpdatedBy: storablePlannedMaintenance.UpdatedBy,
}, nil
}
@@ -143,7 +142,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 {
@@ -153,7 +152,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
@@ -164,7 +163,7 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
return err
}
storablePlannedMaintenance := alertmanagertypes.StorablePlannedMaintenance{
storablePlannedMaintenance := ruletypes.StorablePlannedMaintenance{
Identifiable: types.Identifiable{
ID: id,
},
@@ -176,21 +175,20 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
CreatedBy: existing.CreatedBy,
UpdatedBy: claims.Email,
},
Name: maintenance.Name,
Description: maintenance.Description,
Schedule: maintenance.Schedule,
OrgID: claims.OrgID,
LabelExpression: maintenance.LabelExpression,
Name: maintenance.Name,
Description: maintenance.Description,
Schedule: maintenance.Schedule,
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(),
},
@@ -213,9 +211,10 @@ 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)
if err != nil {
return err
}
@@ -232,6 +231,7 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
}
return nil
})
if err != nil {
return err

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"
@@ -185,53 +184,6 @@ func (handler *handler) TestRule(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, ruletypes.GettableTestRule{AlertCount: alertCount, Message: "notification sent"})
}
// MuteRule mutes an alert rule until the provided endTime by creating a
// fixed-window planned maintenance scoped to that rule. The created
// PlannedMaintenance is returned so the client can later unmute by deleting
// it via DELETE /api/v1/downtime_schedules/{id}.
func (handler *handler) MuteRule(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
return
}
rule, err := handler.ruler.GetRule(ctx, id)
if err != nil {
render.Error(rw, err)
return
}
payload := new(ruletypes.PostableMuteRule)
if err := binding.JSON.BindBody(req.Body, payload); err != nil {
render.Error(rw, err)
return
}
now := time.Now()
if err := payload.Validate(now); err != nil {
render.Error(rw, err)
return
}
schedule := payload.ToPostablePlannedMaintenance(id.StringValue(), rule.AlertName, now)
if err := schedule.Validate(); err != nil {
render.Error(rw, err)
return
}
created, err := handler.ruler.MaintenanceStore().CreatePlannedMaintenance(ctx, schedule)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, created)
}
func (handler *handler) ListDowntimeSchedules(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -242,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
@@ -255,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 {
@@ -266,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)
@@ -301,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
@@ -331,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)
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
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"
@@ -40,8 +39,7 @@ func TestNewHandlers(t *testing.T) {
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstore), sharder)
notificationManager := nfmanagertest.NewMock()
require.NoError(t, err)
maintenanceStore := sqlalertmanagerstore.NewMaintenanceStore(sqlstore)
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"
@@ -41,8 +40,7 @@ func TestNewModules(t *testing.T) {
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstore), sharder)
notificationManager := nfmanagertest.NewMock()
require.NoError(t, err)
maintenanceStore := sqlalertmanagerstore.NewMaintenanceStore(sqlstore)
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

@@ -200,7 +200,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore),
sqlmigration.NewAddSpanMapperFactory(sqlstore, sqlschema),
sqlmigration.NewAddLLMPricingRulesFactory(sqlstore, sqlschema),
sqlmigration.NewAddLabelExpressionToPlannedMaintenanceFactory(sqlstore, sqlschema),
)
}
@@ -227,14 +226,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,7 +5,6 @@ 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/flagger"
@@ -60,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)
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"
@@ -370,14 +369,12 @@ func New(
return nil, err
}
maintenanceStore := sqlalertmanagerstore.NewMaintenanceStore(sqlstore)
// 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,97 +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 addLabelExpressionToPlannedMaintenance struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddLabelExpressionToPlannedMaintenanceFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("add_label_expr_to_planned"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addLabelExpressionToPlannedMaintenance{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
},
)
}
func (migration *addLabelExpressionToPlannedMaintenance) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addLabelExpressionToPlannedMaintenance) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
table, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("planned_maintenance"))
if err != nil {
return err
}
column := &sqlschema.Column{
Name: sqlschema.ColumnName("label_expression"),
DataType: sqlschema.DataTypeText,
Nullable: true,
}
sqls := migration.sqlschema.Operator().AddColumn(table, nil, column, nil)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *addLabelExpressionToPlannedMaintenance) Down(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
table, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("planned_maintenance"))
if err != nil {
return err
}
column := &sqlschema.Column{
Name: sqlschema.ColumnName("label_expression"),
DataType: sqlschema.DataTypeText,
Nullable: true,
}
sqls := migration.sqlschema.Operator().DropColumn(table, column)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
return tx.Commit()
}

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

@@ -0,0 +1,12 @@
package alertmanagertypes
import (
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/client_golang/prometheus"
)
type MemMarker = types.MemMarker
func NewMarker(r prometheus.Registerer) *MemMarker {
return types.NewMarker(r)
}

View File

@@ -1,58 +1,19 @@
package alertmanagertypes
package ruletypes
import (
"context"
"encoding/json"
"time"
"github.com/expr-lang/expr"
"github.com/prometheus/common/model"
"github.com/uptrace/bun"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
var ErrCodeInvalidPlannedMaintenancePayload = errors.MustNewCode("invalid_planned_maintenance_payload")
var ErrCodeInvalidMutePayload = errors.MustNewCode("invalid_mute_payload")
// PostableMuteRule is the input payload for muting an alert rule. A mute is a
// shortcut for creating a fixed (non-recurring) planned maintenance scoped to a
// single rule, covering [now, EndTime].
type PostableMuteRule struct {
EndTime time.Time `json:"endTime" required:"true"`
Description string `json:"description"`
}
func (p *PostableMuteRule) Validate(now time.Time) error {
if p.EndTime.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidMutePayload, "missing endTime in the payload")
}
if !p.EndTime.After(now) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidMutePayload, "endTime must be in the future")
}
return nil
}
// ToPostablePlannedMaintenance builds the underlying planned maintenance
// payload for muting the given rule until p.EndTime.
func (p *PostableMuteRule) ToPostablePlannedMaintenance(ruleID string, ruleName string, now time.Time) *PostablePlannedMaintenance {
name := "Mute: " + ruleName
if ruleName == "" {
name = "Mute for rule " + ruleID
}
return &PostablePlannedMaintenance{
Name: name,
Description: p.Description,
Schedule: &Schedule{
Timezone: "UTC",
StartTime: now.UTC(),
EndTime: p.EndTime.UTC(),
},
AlertIds: []string{ruleID},
}
}
var (
ErrCodeInvalidPlannedMaintenancePayload = errors.MustNewCode("invalid_planned_maintenance_payload")
)
type MaintenanceStatus struct {
valuer.String
@@ -95,37 +56,34 @@ type StorablePlannedMaintenance struct {
types.Identifiable
types.TimeAuditable
types.UserAuditable
Name string `bun:"name,type:text,notnull"`
Description string `bun:"description,type:text"`
Schedule *Schedule `bun:"schedule,type:text,notnull"`
OrgID string `bun:"org_id,type:text"`
LabelExpression string `bun:"label_expression,type:text"`
Name string `bun:"name,type:text,notnull"`
Description string `bun:"description,type:text"`
Schedule *Schedule `bun:"schedule,type:text,notnull"`
OrgID string `bun:"org_id,type:text"`
}
type PlannedMaintenance struct {
ID valuer.UUID `json:"id" required:"true"`
Name string `json:"name" required:"true"`
Description string `json:"description"`
Schedule *Schedule `json:"schedule" required:"true"`
RuleIDs []string `json:"alertIds"`
LabelExpression string `json:"labelExpression,omitempty"`
CreatedAt time.Time `json:"createdAt"`
CreatedBy string `json:"createdBy"`
UpdatedAt time.Time `json:"updatedAt"`
UpdatedBy string `json:"updatedBy"`
Status MaintenanceStatus `json:"status" required:"true"`
Kind MaintenanceKind `json:"kind" required:"true"`
ID valuer.UUID `json:"id" required:"true"`
Name string `json:"name" required:"true"`
Description string `json:"description"`
Schedule *Schedule `json:"schedule" required:"true"`
RuleIDs []string `json:"alertIds"`
CreatedAt time.Time `json:"createdAt"`
CreatedBy string `json:"createdBy"`
UpdatedAt time.Time `json:"updatedAt"`
UpdatedBy string `json:"updatedBy"`
Status MaintenanceStatus `json:"status" required:"true"`
Kind MaintenanceKind `json:"kind" required:"true"`
}
// PostablePlannedMaintenance is the input payload for creating or updating a
// planned maintenance. Server-owned fields (id, timestamps, audit users,
// derived status / kind) are deliberately not accepted from the client.
type PostablePlannedMaintenance struct {
Name string `json:"name" required:"true"`
Description string `json:"description"`
Schedule *Schedule `json:"schedule" required:"true"`
AlertIds []string `json:"alertIds"`
LabelExpression string `json:"labelExpression"`
Name string `json:"name" required:"true"`
Description string `json:"description"`
Schedule *Schedule `json:"schedule" required:"true"`
AlertIds []string `json:"alertIds"`
}
func (p *PostablePlannedMaintenance) Validate() error {
@@ -142,11 +100,11 @@ func (p *PostablePlannedMaintenance) Validate() error {
if _, err := time.LoadLocation(p.Schedule.Timezone); err != nil {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "invalid timezone in the payload")
}
if p.Schedule.StartTime.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing start time in the payload")
}
if !p.Schedule.EndTime.IsZero() && p.Schedule.StartTime.After(p.Schedule.EndTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
if !p.Schedule.StartTime.IsZero() && !p.Schedule.EndTime.IsZero() {
if p.Schedule.StartTime.After(p.Schedule.EndTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
}
}
if p.Schedule.Recurrence != nil {
@@ -156,10 +114,8 @@ func (p *PostablePlannedMaintenance) Validate() error {
if p.Schedule.Recurrence.Duration.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing duration in the payload")
}
}
if p.LabelExpression != "" {
if _, err := expr.Compile(p.LabelExpression, expr.AllowUndefinedVariables(), expr.AsBool()); err != nil {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "invalid label expression: %v", err)
if p.Schedule.Recurrence.EndTime != nil && p.Schedule.Recurrence.EndTime.Before(p.Schedule.Recurrence.StartTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "end time cannot be before start time")
}
}
return nil
@@ -177,7 +133,7 @@ type PlannedMaintenanceWithRules struct {
Rules []*StorablePlannedMaintenanceRule `bun:"rel:has-many,join:id=planned_maintenance_id"`
}
func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time, lset model.LabelSet) bool {
func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
// Check if the alert ID is in the maintenance window
found := false
if len(m.RuleIDs) > 0 {
@@ -197,87 +153,60 @@ func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time, lset model
return false
}
if !m.isScheduleActive(now) {
return false
}
// lset is empty when called from IsActive (no instance labels available);
// skip expression filtering in that case.
if m.LabelExpression != "" && len(lset) != 0 {
if !evalLabelExpression(m.LabelExpression, lset) {
return false
}
}
return true
}
// isScheduleActive reports whether now falls inside the maintenance window's schedule.
func (m *PlannedMaintenance) isScheduleActive(now time.Time) bool {
// If alert is found, we check if it should be skipped based on the schedule
loc, err := time.LoadLocation(m.Schedule.Timezone)
if err != nil {
return false
}
// TODO(jatinderjit): `In(loc)` conversions seem redundant
currentTime := now.In(loc)
startTime := m.Schedule.StartTime.In(loc)
endTime := m.Schedule.EndTime.In(loc)
// Maintenance window hasn't yet started
if currentTime.Before(startTime) {
return false
}
// Maintenance window has ended
if !endTime.IsZero() && currentTime.After(endTime) {
return false
}
// Fixed schedule (startTime <= currentTime <= endTime)
if m.Schedule.Recurrence == nil {
return true
// fixed schedule
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
startTime := m.Schedule.StartTime.In(loc)
endTime := m.Schedule.EndTime.In(loc)
if currentTime.Equal(startTime) || currentTime.Equal(endTime) ||
(currentTime.After(startTime) && currentTime.Before(endTime)) {
return true
}
}
// recurring schedule
switch m.Schedule.Recurrence.RepeatType {
case RepeatTypeDaily:
return m.checkDaily(currentTime, m.Schedule.Recurrence, loc)
case RepeatTypeWeekly:
return m.checkWeekly(currentTime, m.Schedule.Recurrence, loc)
case RepeatTypeMonthly:
return m.checkMonthly(currentTime, m.Schedule.Recurrence, loc)
if m.Schedule.Recurrence != nil {
start := m.Schedule.Recurrence.StartTime
// Make sure the recurrence has started
if currentTime.Before(start.In(loc)) {
return false
}
// Check if recurrence has expired
if m.Schedule.Recurrence.EndTime != nil {
endTime := *m.Schedule.Recurrence.EndTime
if !endTime.IsZero() && currentTime.After(endTime.In(loc)) {
return false
}
}
switch m.Schedule.Recurrence.RepeatType {
case RepeatTypeDaily:
return m.checkDaily(currentTime, m.Schedule.Recurrence, loc)
case RepeatTypeWeekly:
return m.checkWeekly(currentTime, m.Schedule.Recurrence, loc)
case RepeatTypeMonthly:
return m.checkMonthly(currentTime, m.Schedule.Recurrence, loc)
}
}
return false
}
// evalLabelExpression compiles and runs the expression against the provided labels.
// Returns false on any error (safety-first: don't suppress on a bad expression).
func evalLabelExpression(expression string, lset model.LabelSet) bool {
env := make(map[string]interface{}, len(lset))
for k, v := range lset {
env[string(k)] = string(v)
}
program, err := expr.Compile(expression, expr.Env(env), expr.AllowUndefinedVariables())
if err != nil {
return false
}
output, err := expr.Run(program, env)
if err != nil {
return false
}
result, ok := output.(bool)
return ok && result
}
// checkDaily rebases the recurrence start to today (or yesterday if needed)
// and returns true if currentTime is within [candidate, candidate+Duration].
func (m *PlannedMaintenance) checkDaily(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
candidate := time.Date(
currentTime.Year(), currentTime.Month(), currentTime.Day(),
m.Schedule.StartTime.Hour(), m.Schedule.StartTime.Minute(), 0, 0,
rec.StartTime.Hour(), rec.StartTime.Minute(), 0, 0,
loc,
)
if candidate.After(currentTime) {
@@ -305,7 +234,7 @@ func (m *PlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence,
// Build a candidate occurrence by rebasing today's date to the allowed weekday.
candidate := time.Date(
currentTime.Year(), currentTime.Month(), currentTime.Day(),
m.Schedule.StartTime.Hour(), m.Schedule.StartTime.Minute(), 0, 0,
rec.StartTime.Hour(), rec.StartTime.Minute(), 0, 0,
loc,
).AddDate(0, 0, delta)
// If the candidate is in the future, subtract 7 days.
@@ -322,8 +251,7 @@ func (m *PlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence,
// checkMonthly rebases the candidate occurrence using the recurrence's day-of-month.
// If the candidate for the current month is in the future, it uses the previous month.
func (m *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
startTime := m.Schedule.StartTime
refDay := startTime.Day()
refDay := rec.StartTime.Day()
year, month, _ := currentTime.Date()
lastDay := time.Date(year, month+1, 0, 0, 0, 0, 0, loc).Day()
day := refDay
@@ -331,7 +259,7 @@ func (m *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence
day = lastDay
}
candidate := time.Date(year, month, day,
startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(),
rec.StartTime.Hour(), rec.StartTime.Minute(), rec.StartTime.Second(), rec.StartTime.Nanosecond(),
loc,
)
if candidate.After(currentTime) {
@@ -341,12 +269,12 @@ func (m *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence
lastDayPrev := time.Date(y, m+1, 0, 0, 0, 0, 0, loc).Day()
if refDay > lastDayPrev {
candidate = time.Date(y, m, lastDayPrev,
startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(),
rec.StartTime.Hour(), rec.StartTime.Minute(), rec.StartTime.Second(), rec.StartTime.Nanosecond(),
loc,
)
} else {
candidate = time.Date(y, m, refDay,
startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(),
rec.StartTime.Hour(), rec.StartTime.Minute(), rec.StartTime.Second(), rec.StartTime.Nanosecond(),
loc,
)
}
@@ -359,7 +287,7 @@ func (m *PlannedMaintenance) IsActive(now time.Time) bool {
if len(m.RuleIDs) > 0 {
ruleID = (m.RuleIDs)[0]
}
return m.ShouldSkip(ruleID, now, nil)
return m.ShouldSkip(ruleID, now)
}
func (m *PlannedMaintenance) IsUpcoming() bool {
@@ -368,7 +296,14 @@ func (m *PlannedMaintenance) IsUpcoming() bool {
return false
}
now := time.Now().In(loc)
return now.Before(m.Schedule.StartTime)
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
return now.Before(m.Schedule.StartTime)
}
if m.Schedule.Recurrence != nil {
return now.Before(m.Schedule.Recurrence.StartTime)
}
return false
}
func (m *PlannedMaintenance) IsRecurring() bool {
@@ -385,16 +320,16 @@ func (m *PlannedMaintenance) Validate() error {
if m.Schedule.Timezone == "" {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing timezone in the payload")
}
_, err := time.LoadLocation(m.Schedule.Timezone)
if err != nil {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "invalid timezone in the payload")
}
if m.Schedule.StartTime.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing start time in the payload")
}
if !m.Schedule.EndTime.IsZero() && m.Schedule.StartTime.After(m.Schedule.EndTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
if m.Schedule.StartTime.After(m.Schedule.EndTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
}
}
if m.Schedule.Recurrence != nil {
@@ -404,6 +339,9 @@ func (m *PlannedMaintenance) Validate() error {
if m.Schedule.Recurrence.Duration.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing duration in the payload")
}
if m.Schedule.Recurrence.EndTime != nil && m.Schedule.Recurrence.EndTime.Before(m.Schedule.Recurrence.StartTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "end time cannot be before start time")
}
}
return nil
}
@@ -427,31 +365,29 @@ func (m PlannedMaintenance) MarshalJSON() ([]byte, error) {
}
return json.Marshal(struct {
ID valuer.UUID `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
Schedule *Schedule `json:"schedule" db:"schedule"`
AlertIds []string `json:"alertIds" db:"alert_ids"`
LabelExpression string `json:"labelExpression,omitempty" db:"label_expression"`
CreatedAt time.Time `json:"createdAt" db:"created_at"`
CreatedBy string `json:"createdBy" db:"created_by"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
UpdatedBy string `json:"updatedBy" db:"updated_by"`
Status MaintenanceStatus `json:"status"`
Kind MaintenanceKind `json:"kind"`
ID valuer.UUID `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
Schedule *Schedule `json:"schedule" db:"schedule"`
AlertIds []string `json:"alertIds" db:"alert_ids"`
CreatedAt time.Time `json:"createdAt" db:"created_at"`
CreatedBy string `json:"createdBy" db:"created_by"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
UpdatedBy string `json:"updatedBy" db:"updated_by"`
Status MaintenanceStatus `json:"status"`
Kind MaintenanceKind `json:"kind"`
}{
ID: m.ID,
Name: m.Name,
Description: m.Description,
Schedule: m.Schedule,
AlertIds: m.RuleIDs,
LabelExpression: m.LabelExpression,
CreatedAt: m.CreatedAt,
CreatedBy: m.CreatedBy,
UpdatedAt: m.UpdatedAt,
UpdatedBy: m.UpdatedBy,
Status: status,
Kind: kind,
ID: m.ID,
Name: m.Name,
Description: m.Description,
Schedule: m.Schedule,
AlertIds: m.RuleIDs,
CreatedAt: m.CreatedAt,
CreatedBy: m.CreatedBy,
UpdatedAt: m.UpdatedAt,
UpdatedBy: m.UpdatedBy,
Status: status,
Kind: kind,
})
}
@@ -464,16 +400,15 @@ func (m *PlannedMaintenanceWithRules) ToPlannedMaintenance() *PlannedMaintenance
}
return &PlannedMaintenance{
ID: m.ID,
Name: m.Name,
Description: m.Description,
Schedule: m.Schedule,
RuleIDs: ruleIDs,
LabelExpression: m.LabelExpression,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
CreatedBy: m.CreatedBy,
UpdatedBy: m.UpdatedBy,
ID: m.ID,
Name: m.Name,
Description: m.Description,
Schedule: m.Schedule,
RuleIDs: ruleIDs,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
CreatedBy: m.CreatedBy,
UpdatedBy: m.UpdatedBy,
}
}

View File

@@ -1,14 +1,19 @@
package alertmanagertypes
package ruletypes
import (
"testing"
"time"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/prometheus/common/model"
)
// Helper function to create a time pointer.
func timePtr(t time.Time) *time.Time {
return &t
}
func TestShouldSkipMaintenance(t *testing.T) {
cases := []struct {
name string
maintenance *PlannedMaintenance
@@ -19,9 +24,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "only-on-saturday",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "Europe/London",
StartTime: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
Timezone: "Europe/London",
Recurrence: &Recurrence{
StartTime: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("24h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday, RepeatOnTuesday, RepeatOnWednesday, RepeatOnThursday, RepeatOnFriday, RepeatOnSunday},
@@ -36,10 +41,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-across-midnight-previous-day",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("4h"), // Until Tuesday 02:00
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Duration: valuer.MustParseTextDuration("4h"), // Until Tuesday 02:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday}, // Only Monday
},
@@ -53,10 +58,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-across-midnight-previous-day",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("4h"), // Until Tuesday 02:00
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Duration: valuer.MustParseTextDuration("4h"), // Until Tuesday 02:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday}, // Only Monday
},
@@ -70,10 +75,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-across-midnight-previous-day",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("52h"), // Until Thursday 02:00
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Duration: valuer.MustParseTextDuration("52h"), // Until Thursday 02:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday}, // Only Monday
},
@@ -87,10 +92,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-across-midnight-previous-day-not-in-repeaton",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 2, 22, 0, 0, 0, time.UTC), // Tuesday 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("4h"), // Until Wednesday 02:00
StartTime: time.Date(2024, 4, 2, 22, 0, 0, 0, time.UTC), // Tuesday 22:00
Duration: valuer.MustParseTextDuration("4h"), // Until Wednesday 02:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnTuesday}, // Only Tuesday
},
@@ -104,10 +109,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "daily-maintenance-across-midnight",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 23, 0, 0, 0, time.UTC), // 23:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"), // Until 01:00 next day
StartTime: time.Date(2024, 1, 1, 23, 0, 0, 0, time.UTC), // 23:00
Duration: valuer.MustParseTextDuration("2h"), // Until 01:00 next day
RepeatType: RepeatTypeDaily,
},
},
@@ -120,9 +125,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "at-start-time-boundary",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -136,9 +141,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "at-end-time-boundary",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -152,9 +157,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-multi-day-duration",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 28, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 28, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("72h"), // 3 days
RepeatType: RepeatTypeMonthly,
},
@@ -168,9 +173,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-multi-day-duration",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 28, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 28, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("72h"), // 3 days
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnSunday},
@@ -185,9 +190,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-crosses-to-next-month",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 30, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 30, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("48h"), // 2 days, crosses to Feb 1
RepeatType: RepeatTypeMonthly,
},
@@ -201,9 +206,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "timezone-offset-test",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "America/New_York", // UTC-5 or UTC-4 depending on DST
StartTime: time.Date(2024, 1, 1, 22, 0, 0, 0, time.FixedZone("America/New_York", -5*3600)),
Timezone: "America/New_York", // UTC-5 or UTC-4 depending on DST
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 22, 0, 0, 0, time.FixedZone("America/New_York", -5*3600)),
Duration: valuer.MustParseTextDuration("4h"),
RepeatType: RepeatTypeDaily,
},
@@ -217,9 +222,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "daily-maintenance-time-outside-window",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -233,10 +238,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring-maintenance-with-past-end-date",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
EndTime: time.Date(2024, 1, 10, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
EndTime: timePtr(time.Date(2024, 1, 10, 12, 0, 0, 0, time.UTC)),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -250,10 +255,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-spans-month-end",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 3, 31, 22, 0, 0, 0, time.UTC), // March 31, 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("6h"), // Until April 1, 04:00
StartTime: time.Date(2024, 3, 31, 22, 0, 0, 0, time.UTC), // March 31, 22:00
Duration: valuer.MustParseTextDuration("6h"), // Until April 1, 04:00
RepeatType: RepeatTypeMonthly,
},
},
@@ -266,9 +271,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-empty-repeaton",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{}, // Empty - should apply to all days
@@ -283,9 +288,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-february-fewer-days",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
@@ -298,9 +303,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "daily-maintenance-crosses-midnight",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 23, 30, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 23, 30, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("1h"), // Crosses to 00:30 next day
RepeatType: RepeatTypeDaily,
},
@@ -313,9 +318,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-crosses-month-end",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
@@ -328,9 +333,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 30, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 30, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("48h"), // 2 days duration
RepeatType: RepeatTypeMonthly,
},
@@ -343,10 +348,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-maintenance-crosses-midnight",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 23, 0, 0, 0, time.UTC), // Monday 23:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"), // Until Tuesday 01:00
StartTime: time.Date(2024, 4, 1, 23, 0, 0, 0, time.UTC), // Monday 23:00
Duration: valuer.MustParseTextDuration("2h"), // Until Tuesday 01:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday}, // Only Monday
},
@@ -359,9 +364,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
@@ -374,9 +379,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "daily-maintenance-crosses-midnight",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 22, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 22, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("4h"), // Until 02:00 next day
RepeatType: RepeatTypeDaily,
},
@@ -389,9 +394,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-hours",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
@@ -440,9 +445,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring maintenance, repeat sunday, saturday, weekly for 24 hours, in Us/Eastern timezone",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "US/Eastern",
StartTime: time.Date(2025, 3, 29, 20, 0, 0, 0, time.FixedZone("US/Eastern", -4*3600)),
Timezone: "US/Eastern",
Recurrence: &Recurrence{
StartTime: time.Date(2025, 3, 29, 20, 0, 0, 0, time.FixedZone("US/Eastern", -4*3600)),
Duration: valuer.MustParseTextDuration("24h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnSunday, RepeatOnSaturday},
@@ -456,9 +461,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -471,9 +476,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -486,182 +491,146 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
},
},
ts: time.Date(2024, 4, 1, 12, 10, 0, 0, time.UTC),
ts: time.Date(2024, 04, 1, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 4, 15, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 4, 14, 12, 10, 0, 0, time.UTC), // 14th 04 is sunday
skip: false,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 4, 16, 12, 10, 0, 0, time.UTC), // 16th 04 is tuesday
skip: false,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 5, 6, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 5, 6, 14, 0, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 4, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 4, 4, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 4, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 4, 4, 14, 10, 0, 0, time.UTC),
skip: false,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 4, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 5, 4, 12, 10, 0, 0, time.UTC),
skip: true,
},
// The recurrence should govern, when set. Not the fixed range.
{
name: "recurring-daily-with-fixed-times-outside-daily-window",
maintenance: &PlannedMaintenance{
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), // daily at 14:00
EndTime: time.Date(2026, 4, 30, 18, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"), // until 16:00
RepeatType: RepeatTypeDaily,
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
// 2026-04-15 11:00 is inside the fixed range but outside the daily 14:00-16:00 window.
ts: time.Date(2026, 4, 15, 11, 0, 0, 0, time.UTC),
ts: time.Date(2024, 04, 15, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 04, 14, 12, 10, 0, 0, time.UTC), // 14th 04 is sunday
skip: false,
},
{
name: "recurring-daily-with-fixed-times-inside-daily-window",
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2026, 4, 1, 14, 0, 0, 0, time.UTC),
EndTime: time.Date(2026, 4, 30, 18, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
// 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),
ts: time.Date(2024, 04, 16, 12, 10, 0, 0, time.UTC), // 16th 04 is tuesday
skip: false,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 05, 06, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 05, 06, 14, 00, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 04, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 04, 04, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 04, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 04, 04, 14, 10, 0, 0, time.UTC),
skip: false,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 04, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 05, 04, 12, 10, 0, 0, time.UTC),
skip: true,
},
}
for idx, c := range cases {
result := c.maintenance.ShouldSkip(c.name, c.ts, model.LabelSet{})
result := c.maintenance.ShouldSkip(c.name, c.ts)
if result != c.skip {
t.Errorf("skip %v, got %v, case:%d - %s", c.skip, result, idx, c.name)
}

View File

@@ -1,4 +1,4 @@
package alertmanagertypes
package ruletypes
import (
"database/sql/driver"
@@ -67,6 +67,8 @@ var RepeatOnAllMap = map[RepeatOn]time.Weekday{
}
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"`
@@ -103,16 +105,33 @@ func (s Schedule) MarshalJSON() ([]byte, error) {
endTime = time.Date(s.EndTime.Year(), s.EndTime.Month(), s.EndTime.Day(), s.EndTime.Hour(), s.EndTime.Minute(), s.EndTime.Second(), s.EndTime.Nanosecond(), loc)
}
var recurrence *Recurrence
if s.Recurrence != nil {
recStartTime := time.Date(s.Recurrence.StartTime.Year(), s.Recurrence.StartTime.Month(), s.Recurrence.StartTime.Day(), s.Recurrence.StartTime.Hour(), s.Recurrence.StartTime.Minute(), s.Recurrence.StartTime.Second(), s.Recurrence.StartTime.Nanosecond(), loc)
var recEndTime *time.Time
if s.Recurrence.EndTime != nil {
end := time.Date(s.Recurrence.EndTime.Year(), s.Recurrence.EndTime.Month(), s.Recurrence.EndTime.Day(), s.Recurrence.EndTime.Hour(), s.Recurrence.EndTime.Minute(), s.Recurrence.EndTime.Second(), s.Recurrence.EndTime.Nanosecond(), loc)
recEndTime = &end
}
recurrence = &Recurrence{
StartTime: recStartTime,
EndTime: recEndTime,
Duration: s.Recurrence.Duration,
RepeatType: s.Recurrence.RepeatType,
RepeatOn: s.Recurrence.RepeatOn,
}
}
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,
Recurrence: s.Recurrence,
StartTime: startTime.Format(time.RFC3339),
EndTime: endTime.Format(time.RFC3339),
Recurrence: recurrence,
})
}
@@ -147,11 +166,34 @@ func (s *Schedule) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
// TODO(jatinderjit): if endTime.IsZero() then we should not set the endTime
s.EndTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), endTime.Hour(), endTime.Minute(), endTime.Second(), endTime.Nanosecond(), loc)
}
s.Timezone = aux.Timezone
s.Recurrence = aux.Recurrence
if aux.Recurrence != nil {
recStartTime, err := time.Parse(time.RFC3339, aux.Recurrence.StartTime.Format(time.RFC3339))
if err != nil {
return err
}
var recEndTime *time.Time
if aux.Recurrence.EndTime != nil {
end, err := time.Parse(time.RFC3339, aux.Recurrence.EndTime.Format(time.RFC3339))
if err != nil {
return err
}
endConverted := time.Date(end.Year(), end.Month(), end.Day(), end.Hour(), end.Minute(), end.Second(), end.Nanosecond(), loc)
recEndTime = &endConverted
}
s.Recurrence = &Recurrence{
StartTime: time.Date(recStartTime.Year(), recStartTime.Month(), recStartTime.Day(), recStartTime.Hour(), recStartTime.Minute(), recStartTime.Second(), recStartTime.Nanosecond(), loc),
EndTime: recEndTime,
Duration: aux.Recurrence.Duration,
RepeatType: aux.Recurrence.RepeatType,
RepeatOn: aux.Recurrence.RepeatOn,
}
}
return nil
}

View File

@@ -1,4 +1,4 @@
package alertmanagertypes
package ruletypes
import (
"database/sql/driver"
@@ -11,8 +11,8 @@ import (
type Schedule struct {
Timezone string `json:"timezone" required:"true"`
StartTime time.Time `json:"startTime" required:"true"`
EndTime time.Time `json:"endTime,omitzero"`
StartTime time.Time `json:"startTime,omitempty"`
EndTime time.Time `json:"endTime,omitempty"`
Recurrence *Recurrence `json:"recurrence"`
}

View File

@@ -27,6 +27,7 @@ pytest_plugins = [
"fixtures.seeder",
"fixtures.serviceaccount",
"fixtures.role",
"fixtures.seed_golden_dataset",
]

View File

@@ -0,0 +1,13 @@
import { expect, test as setup } from '@playwright/test';
const seederUrl = process.env.SIGNOZ_E2E_SEEDER_URL ?? '';
setup('refresh golden dataset', async ({ request }) => {
expect(seederUrl, 'SIGNOZ_E2E_SEEDER_URL not set').not.toBe('');
const response = await request.post(`${seederUrl}/seed/golden`, {
timeout: 120_000,
});
expect(response.ok()).toBeTruthy();
// eslint-disable-next-line no-console
console.log(`[setup] refreshed golden dataset: ${await response.text()}`);
});

View File

@@ -0,0 +1,14 @@
import { expect, test as teardown } from '@playwright/test';
const seederUrl = process.env.SIGNOZ_E2E_SEEDER_URL ?? '';
teardown('clear seeded telemetry', async ({ request }) => {
expect(seederUrl, 'SIGNOZ_E2E_SEEDER_URL not set').not.toBe('');
for (const signal of ['metrics', 'traces', 'logs'] as const) {
const response = await request.delete(
`${seederUrl}/telemetry/${signal}`,
{ timeout: 60_000 },
);
expect(response.ok()).toBeTruthy();
}
});

View File

@@ -2,6 +2,7 @@ import os
from pathlib import Path
import pytest
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
@@ -38,6 +39,13 @@ def test_teardown(
signoz: types.SigNoz, # pylint: disable=unused-argument
create_user_admin: types.Operation, # pylint: disable=unused-argument
apply_license: types.Operation, # pylint: disable=unused-argument
seeder: types.TestContainerDocker, # pylint: disable=unused-argument
seeder: types.TestContainerDocker,
) -> None:
"""Fixture dependencies trigger container teardown via --teardown."""
"""Truncate seeded telemetry; containers come down via fixture
dependency under `--teardown`."""
base = seeder.host_configs["8080"].base().rstrip("/")
for signal in ("metrics", "traces", "logs"):
try:
requests.delete(f"{base}/telemetry/{signal}", timeout=30).raise_for_status()
except Exception as e: # pylint: disable=broad-exception-caught
print(f"seeder DELETE /telemetry/{signal} failed: {e}")

View File

@@ -50,12 +50,36 @@ export default defineConfig({
viewport: { width: 1280, height: 720 },
},
// Browser projects. No project-level auth — specs opt in via the
// authedPage fixture in tests/e2e/fixtures/auth.ts, which logs a user
// in on first use and caches the resulting storageState per worker.
// `setup` runs `bootstrap/global.setup.ts` once before any browser
// project — refreshes the golden dataset so chart-data assertions
// land inside default panel time windows. Per
// https://playwright.dev/docs/test-global-setup-teardown#option-1-project-dependencies.
projects: [
{ name: 'chromium', use: devices['Desktop Chrome'] },
{ name: 'firefox', use: devices['Desktop Firefox'] },
{ name: 'webkit', use: devices['Desktop Safari'] },
{
name: 'setup',
testDir: './bootstrap',
testMatch: /global\.setup\.ts/,
teardown: 'teardown',
},
{
name: 'teardown',
testDir: './bootstrap',
testMatch: /global\.teardown\.ts/,
},
{
name: 'chromium',
use: devices['Desktop Chrome'],
dependencies: ['setup'],
},
{
name: 'firefox',
use: devices['Desktop Firefox'],
dependencies: ['setup'],
},
{
name: 'webkit',
use: devices['Desktop Safari'],
dependencies: ['setup'],
},
],
});

View File

@@ -18,6 +18,6 @@
"outDir": "./dist",
"rootDir": "."
},
"include": ["tests/**/*.ts", "helpers/**/*.ts", "fixtures/**/*.ts", "playwright.config.ts"],
"include": ["tests/**/*.ts", "helpers/**/*.ts", "fixtures/**/*.ts", "bootstrap/**/*.ts", "playwright.config.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,192 @@
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Database query timed out","severity":"ERROR","minutes_ago":270,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Slow response detected","severity":"WARN","minutes_ago":240,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Handled request","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Cache miss","severity":"WARN","minutes_ago":180,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Upstream call failed","severity":"ERROR","minutes_ago":120,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Retrying upstream call","severity":"WARN","minutes_ago":30,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Upstream call failed","severity":"ERROR","minutes_ago":300,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Retrying upstream call","severity":"WARN","minutes_ago":270,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Retrying upstream call","severity":"WARN","minutes_ago":120,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Handled request","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[cartservice] Handled request","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
{"body":"[checkoutservice] Retrying upstream call","severity":"WARN","minutes_ago":360,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":270,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Retrying upstream call","severity":"WARN","minutes_ago":210,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Handled request","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Authorization failed","severity":"ERROR","minutes_ago":150,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Cache miss","severity":"WARN","minutes_ago":120,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[checkoutservice] Slow response detected","severity":"WARN","minutes_ago":15,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Connection established","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Handled request","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Handled request","severity":"INFO","minutes_ago":270,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Connection established","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Handled request","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Connection established","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache miss","severity":"WARN","minutes_ago":135,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Handled request","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
{"body":"[frontend] Slow response detected","severity":"WARN","minutes_ago":360,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":270,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Handled request","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Slow response detected","severity":"WARN","minutes_ago":225,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Cache hit","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Handled request","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Cache miss","severity":"WARN","minutes_ago":165,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Authorization failed","severity":"ERROR","minutes_ago":150,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Handled request","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Cache hit","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Slow response detected","severity":"WARN","minutes_ago":270,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Authorization failed","severity":"ERROR","minutes_ago":255,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Authorization failed","severity":"ERROR","minutes_ago":225,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Background job completed","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Authorization failed","severity":"ERROR","minutes_ago":195,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Retrying upstream call","severity":"WARN","minutes_ago":150,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Background job completed","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Authorization failed","severity":"ERROR","minutes_ago":90,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[paymentservice] Upstream call failed","severity":"ERROR","minutes_ago":15,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Connection established","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Cache miss","severity":"WARN","minutes_ago":270,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Connection established","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Handled request","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Connection established","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
{"body":"[shippingservice] Database query timed out","severity":"ERROR","minutes_ago":360,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Handled request","severity":"INFO","minutes_ago":270,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Handled request","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Cache miss","severity":"WARN","minutes_ago":225,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Database query timed out","severity":"ERROR","minutes_ago":135,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Handled request","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Handled request","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
{"body":"[shippingservice] Cache miss","severity":"WARN","minutes_ago":15,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,180 @@
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":360,"duration_ms":380,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":330,"duration_ms":141,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":300,"duration_ms":419,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":270,"duration_ms":421,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":240,"duration_ms":245,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":210,"duration_ms":341,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":180,"duration_ms":403,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":150,"duration_ms":334,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":120,"duration_ms":402,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":90,"duration_ms":140,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":60,"duration_ms":350,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":30,"duration_ms":229,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":360,"duration_ms":421,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":330,"duration_ms":332,"status":"ERROR","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"500"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":300,"duration_ms":70,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":270,"duration_ms":464,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":240,"duration_ms":77,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":210,"duration_ms":187,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":180,"duration_ms":278,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":150,"duration_ms":191,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":120,"duration_ms":433,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":90,"duration_ms":191,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":60,"duration_ms":104,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":30,"duration_ms":189,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":360,"duration_ms":303,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":330,"duration_ms":492,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":300,"duration_ms":271,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":270,"duration_ms":114,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":240,"duration_ms":245,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":210,"duration_ms":171,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":180,"duration_ms":145,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":150,"duration_ms":190,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":120,"duration_ms":166,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":90,"duration_ms":391,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":60,"duration_ms":361,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":30,"duration_ms":456,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":360,"duration_ms":173,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":330,"duration_ms":127,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":300,"duration_ms":342,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":270,"duration_ms":124,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":240,"duration_ms":499,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":210,"duration_ms":168,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":180,"duration_ms":292,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":150,"duration_ms":417,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":120,"duration_ms":107,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":90,"duration_ms":439,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":60,"duration_ms":441,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":30,"duration_ms":223,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":360,"duration_ms":420,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":330,"duration_ms":244,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":300,"duration_ms":485,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":270,"duration_ms":189,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":240,"duration_ms":134,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":210,"duration_ms":420,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":180,"duration_ms":456,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":150,"duration_ms":485,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":120,"duration_ms":80,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":90,"duration_ms":291,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":60,"duration_ms":158,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":30,"duration_ms":56,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":360,"duration_ms":378,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":330,"duration_ms":82,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":300,"duration_ms":102,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":270,"duration_ms":249,"status":"ERROR","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"500"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":240,"duration_ms":215,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":210,"duration_ms":234,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":180,"duration_ms":301,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":150,"duration_ms":284,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":120,"duration_ms":290,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":90,"duration_ms":212,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":60,"duration_ms":400,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":30,"duration_ms":339,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":360,"duration_ms":205,"status":"ERROR","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"500"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":330,"duration_ms":77,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":300,"duration_ms":217,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":270,"duration_ms":348,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":240,"duration_ms":351,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":210,"duration_ms":288,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":180,"duration_ms":83,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":150,"duration_ms":264,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":120,"duration_ms":111,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":90,"duration_ms":349,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":60,"duration_ms":464,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":30,"duration_ms":201,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":360,"duration_ms":333,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":330,"duration_ms":227,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":300,"duration_ms":487,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":270,"duration_ms":478,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":240,"duration_ms":137,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":210,"duration_ms":215,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":180,"duration_ms":445,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":150,"duration_ms":59,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":120,"duration_ms":254,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":90,"duration_ms":197,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":60,"duration_ms":52,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /","kind":"SERVER","minutes_ago":30,"duration_ms":221,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":360,"duration_ms":72,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":330,"duration_ms":335,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":300,"duration_ms":292,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":270,"duration_ms":286,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":240,"duration_ms":444,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":210,"duration_ms":183,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":180,"duration_ms":123,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":150,"duration_ms":337,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":120,"duration_ms":373,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":90,"duration_ms":248,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":60,"duration_ms":459,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /product","kind":"SERVER","minutes_ago":30,"duration_ms":90,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":360,"duration_ms":304,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":330,"duration_ms":427,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":300,"duration_ms":130,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":270,"duration_ms":152,"status":"ERROR","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"500"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":240,"duration_ms":163,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":210,"duration_ms":73,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":180,"duration_ms":177,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":150,"duration_ms":80,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":120,"duration_ms":440,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":90,"duration_ms":450,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":60,"duration_ms":481,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":30,"duration_ms":219,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":360,"duration_ms":381,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":330,"duration_ms":307,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":300,"duration_ms":351,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":270,"duration_ms":384,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":240,"duration_ms":273,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":210,"duration_ms":499,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":180,"duration_ms":80,"status":"ERROR","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"500"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":150,"duration_ms":186,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":120,"duration_ms":423,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":90,"duration_ms":121,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":60,"duration_ms":451,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":30,"duration_ms":402,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":360,"duration_ms":189,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":330,"duration_ms":229,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":300,"duration_ms":59,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":270,"duration_ms":300,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":240,"duration_ms":439,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":210,"duration_ms":478,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":180,"duration_ms":104,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":150,"duration_ms":336,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":120,"duration_ms":335,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":90,"duration_ms":430,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":60,"duration_ms":116,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":30,"duration_ms":75,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":360,"duration_ms":314,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":330,"duration_ms":303,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":300,"duration_ms":174,"status":"ERROR","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"500"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":270,"duration_ms":238,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":240,"duration_ms":494,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":210,"duration_ms":394,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":180,"duration_ms":71,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":150,"duration_ms":222,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":120,"duration_ms":386,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":90,"duration_ms":227,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":60,"duration_ms":54,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":30,"duration_ms":456,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":360,"duration_ms":317,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":330,"duration_ms":111,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":300,"duration_ms":478,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":270,"duration_ms":75,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":240,"duration_ms":413,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":210,"duration_ms":217,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":180,"duration_ms":160,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":150,"duration_ms":170,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":120,"duration_ms":415,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":90,"duration_ms":448,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":60,"duration_ms":340,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":30,"duration_ms":390,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":360,"duration_ms":96,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":330,"duration_ms":414,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":300,"duration_ms":182,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":270,"duration_ms":116,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":240,"duration_ms":489,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":210,"duration_ms":130,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":180,"duration_ms":394,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":150,"duration_ms":59,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":120,"duration_ms":159,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":90,"duration_ms":432,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":60,"duration_ms":87,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":30,"duration_ms":59,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}

389
tests/fixtures/seed_golden_dataset.py vendored Normal file
View File

@@ -0,0 +1,389 @@
"""Golden dataset fixture — seeds OTel-demo-shaped metrics, traces, and
logs into ClickHouse via the seeder on every test_setup invocation.
Timestamps are rebased to `now` so panels with default time windows
always find data. To refresh the dataset shape on disk, run
`uv run python -m fixtures.seed_golden_dataset regenerate`.
"""
from __future__ import annotations
import datetime
import json
import logging
import os
import random
from collections.abc import Iterator
from pathlib import Path
import pytest
import requests
from fixtures import types
logger = logging.getLogger(__name__)
_GOLDEN_DIR = Path(__file__).resolve().parent / "golden"
METRICS_PATH = _GOLDEN_DIR / "otel-demo-metrics-golden.jsonl"
TRACES_PATH = _GOLDEN_DIR / "otel-demo-traces-golden.jsonl"
LOGS_PATH = _GOLDEN_DIR / "otel-demo-logs-golden.jsonl"
# ─── Generator ───────────────────────────────────────────────────────────
_SERVICES = [
"adservice",
"cartservice",
"checkoutservice",
"currencyservice",
"frontend",
"paymentservice",
"productcatalogservice",
"shippingservice",
]
_OPERATIONS = {
"adservice": ["/ads/get", "/ads/list"],
"cartservice": ["/cart/add", "/cart/get", "/cart/empty"],
"checkoutservice": ["/checkout"],
"currencyservice": ["/currency/convert"],
"frontend": ["/", "/product", "/checkout"],
"paymentservice": ["/payment/charge"],
"productcatalogservice": ["/products/list", "/products/get"],
"shippingservice": ["/shipping/quote", "/shipping/ship"],
}
_DB_SERVICES = {"cartservice", "productcatalogservice"}
_ENV = "production"
_BUCKET_MINUTES = 5
_WINDOW_HOURS = 6
def _generate_metrics() -> list[dict]:
rng = random.Random(20260511)
samples: list[dict] = []
n_buckets = (_WINDOW_HOURS * 60) // _BUCKET_MINUTES
base_counter = 1000
for service in _SERVICES:
for operation in _OPERATIONS[service]:
for status in ("STATUS_CODE_OK", "STATUS_CODE_ERROR"):
weight = 9 if status == "STATUS_CODE_OK" else 1
counter = base_counter
latency_sum = 0
for i in range(n_buckets):
minutes_ago = (_WINDOW_HOURS * 60) - (i + 1) * _BUCKET_MINUTES
bucket_calls = int(
weight
* (50 + 20 * (1 + i % 12 / 12.0) + rng.randint(0, 10))
)
counter += bucket_calls
latency_sum += bucket_calls * rng.randint(100_000, 500_000)
resource_attrs = {
"service.name": service,
"deployment.environment": _ENV,
"k8s.namespace.name": f"signoz-{service}",
}
point_attrs = {
"operation": operation,
"status_code": status,
"span_kind": "SPAN_KIND_SERVER",
}
for name, value in (
("signoz_calls_total", counter),
("signoz_latency_count", counter),
("signoz_latency_sum", latency_sum),
):
samples.append(
{
"metric_name": name,
"minutes_ago": minutes_ago,
"value": value,
"resource_attributes": resource_attrs,
"attributes": point_attrs,
"is_monotonic": True,
}
)
if service in _DB_SERVICES:
db_counter = 0
for i in range(n_buckets):
minutes_ago = (_WINDOW_HOURS * 60) - (i + 1) * _BUCKET_MINUTES
db_counter += 20 + rng.randint(0, 15)
samples.append(
{
"metric_name": "signoz_db_latency_count",
"minutes_ago": minutes_ago,
"value": db_counter,
"resource_attributes": {
"service.name": service,
"deployment.environment": _ENV,
"k8s.namespace.name": f"signoz-{service}",
},
"attributes": {
"db.system": "postgresql"
if service == "cartservice"
else "mongodb",
},
"is_monotonic": True,
}
)
return samples
def _generate_traces() -> list[dict]:
rng = random.Random(20260512)
samples: list[dict] = []
n_buckets = 12
for service in _SERVICES:
for operation in _OPERATIONS[service]:
for i in range(n_buckets):
minutes_ago = int(
(_WINDOW_HOURS * 60) - i * (_WINDOW_HOURS * 60 / n_buckets)
)
http_status = "500" if rng.random() < 0.05 else "200"
samples.append(
{
"name": f"{service} {operation}",
"kind": "SERVER",
"minutes_ago": minutes_ago,
"duration_ms": rng.randint(50, 500),
"status": "ERROR" if http_status == "500" else "OK",
"resource_attributes": {
"service.name": service,
"deployment.environment": _ENV,
"k8s.namespace.name": f"signoz-{service}",
},
"attributes": {
"http.method": "GET"
if "get" in operation.lower() or operation == "/"
else "POST",
"http.route": operation,
"http.status_code": http_status,
},
}
)
return samples
_LOG_SEVERITIES = [("INFO", 0.85), ("WARN", 0.10), ("ERROR", 0.05)]
_LOG_BODIES = {
"INFO": ["Handled request", "Cache hit", "Connection established"],
"WARN": ["Slow response detected", "Cache miss", "Retrying upstream call"],
"ERROR": ["Upstream call failed", "Database query timed out", "Auth failed"],
}
def _generate_logs() -> list[dict]:
rng = random.Random(20260512)
samples: list[dict] = []
n_buckets = 24
for service in _SERVICES:
for i in range(n_buckets):
minutes_ago = int(
(_WINDOW_HOURS * 60) - i * (_WINDOW_HOURS * 60 / n_buckets)
)
r = rng.random()
cumulative = 0.0
severity = "INFO"
for name, weight in _LOG_SEVERITIES:
cumulative += weight
if r < cumulative:
severity = name
break
samples.append(
{
"body": f"[{service}] {rng.choice(_LOG_BODIES[severity])}",
"severity": severity,
"minutes_ago": minutes_ago,
"resource_attributes": {
"service.name": service,
"deployment.environment": _ENV,
"k8s.namespace.name": f"signoz-{service}",
},
"attributes": {"logger.name": f"{service}.app"},
}
)
return samples
def _write_jsonl(path: Path, samples: list[dict]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w") as f:
for s in samples:
f.write(json.dumps(s, separators=(",", ":")))
f.write("\n")
def regenerate() -> dict[str, int]:
metrics = _generate_metrics()
traces = _generate_traces()
logs = _generate_logs()
_write_jsonl(METRICS_PATH, metrics)
_write_jsonl(TRACES_PATH, traces)
_write_jsonl(LOGS_PATH, logs)
return {"metrics": len(metrics), "traces": len(traces), "logs": len(logs)}
# ─── Loader ──────────────────────────────────────────────────────────────
_KIND_TO_INT = {
"UNSPECIFIED": 0,
"INTERNAL": 1,
"SERVER": 2,
"CLIENT": 3,
"PRODUCER": 4,
"CONSUMER": 5,
}
_STATUS_TO_INT = {"UNSET": 0, "OK": 1, "ERROR": 2}
def _read_jsonl(path: Path) -> Iterator[dict]:
with path.open() as f:
for line in f:
line = line.strip()
if line:
yield json.loads(line)
def _iso_minus_minutes(now: datetime.datetime, minutes: float) -> str:
ts = now - datetime.timedelta(minutes=minutes)
return (
ts.replace(tzinfo=datetime.timezone.utc)
.isoformat()
.replace("+00:00", "Z")
)
def _rebased_metric(sample: dict, now: datetime.datetime) -> dict:
out = {k: v for k, v in sample.items() if k != "minutes_ago"}
out["timestamp"] = _iso_minus_minutes(now, sample["minutes_ago"])
return out
def _rebased_trace(sample: dict, now: datetime.datetime) -> dict:
return {
"timestamp": _iso_minus_minutes(now, sample["minutes_ago"]),
"duration": f"PT{sample['duration_ms'] / 1000:.3f}S",
"trace_id": sample.get("trace_id") or os.urandom(16).hex(),
"span_id": sample.get("span_id") or os.urandom(8).hex(),
"name": sample["name"],
"kind": _KIND_TO_INT.get(str(sample.get("kind", "SERVER")).upper(), 2),
"status_code": _STATUS_TO_INT.get(
str(sample.get("status", "UNSET")).upper(), 0
),
"resources": sample.get("resource_attributes", {}),
"attributes": sample.get("attributes", {}),
}
def _rebased_log(sample: dict, now: datetime.datetime) -> dict:
return {
"timestamp": _iso_minus_minutes(now, sample["minutes_ago"]),
"body": sample["body"],
"severity_text": str(sample.get("severity", "INFO")).upper(),
"resources": sample.get("resource_attributes", {}),
"attributes": sample.get("attributes", {}),
}
def _post_batches(
url: str, rows: Iterator[dict], batch_size: int, timeout: int
) -> int:
batch: list[dict] = []
total = 0
for row in rows:
batch.append(row)
if len(batch) >= batch_size:
response = requests.post(url, json=batch, timeout=timeout)
response.raise_for_status()
total += len(batch)
batch = []
if batch:
response = requests.post(url, json=batch, timeout=timeout)
response.raise_for_status()
total += len(batch)
return total
def seed(
seeder_base_url: str,
*,
batch_size: int = 500,
timeout: int = 60,
clear_first: bool = True,
) -> dict[str, int]:
"""Wipe each signal table (via DELETE /telemetry/<signal>) and replay
the golden dataset with timestamps rebased to `now`. Each call leaves
the stack in the exact state the JSONL files describe — chart-data
assertions are reproducible across sessions regardless of how many
earlier sessions seeded."""
for path in (METRICS_PATH, TRACES_PATH, LOGS_PATH):
if not path.exists():
raise FileNotFoundError(
f"golden dataset missing at {path} — run "
"`uv run python -m fixtures.seed_golden_dataset regenerate`"
)
now = datetime.datetime.now(datetime.timezone.utc).replace(
microsecond=0, tzinfo=None
)
base = seeder_base_url.rstrip("/")
if clear_first:
for signal in ("metrics", "traces", "logs"):
requests.delete(f"{base}/telemetry/{signal}", timeout=timeout).raise_for_status()
counts = {
"metrics": _post_batches(
base + "/telemetry/metrics",
(_rebased_metric(s, now) for s in _read_jsonl(METRICS_PATH)),
batch_size,
timeout,
),
"traces": _post_batches(
base + "/telemetry/traces",
(_rebased_trace(s, now) for s in _read_jsonl(TRACES_PATH)),
batch_size,
timeout,
),
"logs": _post_batches(
base + "/telemetry/logs",
(_rebased_log(s, now) for s in _read_jsonl(LOGS_PATH)),
batch_size,
timeout,
),
}
logger.info("seeded through %s: %s", base, counts)
return counts
# ─── Fixture ─────────────────────────────────────────────────────────────
@pytest.fixture(name="golden_dataset", scope="package")
def golden_dataset(seeder: types.TestContainerDocker) -> dict[str, int]:
"""Seed metrics + traces + logs into the running stack via the
seeder. Runs unconditionally on every test_setup invocation so the
rebased timestamps always anchor against `now`."""
return seed(seeder.host_configs["8080"].base())
if __name__ == "__main__":
import sys
logging.basicConfig(level=logging.INFO)
if len(sys.argv) < 2:
sys.stderr.write(
"usage: seed_golden_dataset.py seed <seeder-base-url> | regenerate\n"
)
sys.exit(2)
cmd = sys.argv[1]
if cmd == "regenerate":
print(f"wrote {regenerate()}")
elif cmd == "seed":
if len(sys.argv) != 3:
sys.stderr.write(
"usage: seed_golden_dataset.py seed <seeder-base-url>\n"
)
sys.exit(2)
print(f"seeded {seed(sys.argv[2])}")
else:
sys.stderr.write(f"unknown command: {cmd}\n")
sys.exit(2)

View File

@@ -1,5 +1,12 @@
"""HTTP seeder — wraps fixtures.{traces,logs,metrics} so Playwright specs
can POST per-test telemetry (tagged `seeder=true`) and DELETE to clear."""
"""HTTP seeder — single entrypoint for e2e/integration telemetry.
POST /telemetry/{metrics,logs,traces} insert into ClickHouse via
fixtures.{metrics,logs,traces}. DELETE truncates the signal tables.
Parallel-safe: every seeded row is tagged `seeder=true`. Tests share
the seeded baseline; per-test mutations live in their own dashboards.
Only test_teardown should call DELETE — workers must finish first.
"""
import os
from collections.abc import AsyncIterator
@@ -10,11 +17,7 @@ import clickhouse_connect
from fastapi import FastAPI, HTTPException, Response, status
from fixtures.logger import setup_logger
from fixtures.logs import (
Logs,
insert_logs_to_clickhouse,
truncate_logs_tables,
)
from fixtures.logs import Logs, insert_logs_to_clickhouse, truncate_logs_tables
from fixtures.metrics import (
Metrics,
insert_metrics_to_clickhouse,
@@ -39,7 +42,9 @@ SEEDER_MARKER = {"seeder": "true"}
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
conn = clickhouse_connect.get_client(host=CH_HOST, port=CH_PORT, username=CH_USER, password=CH_PASSWORD)
conn = clickhouse_connect.get_client(
host=CH_HOST, port=CH_PORT, username=CH_USER, password=CH_PASSWORD
)
app.state.ch = conn
try:
yield
@@ -64,12 +69,19 @@ def _tag(item: dict[str, Any]) -> dict[str, Any]:
return {**item, "resources": resources}
# Metrics payload carries label dicts at the top level, not a `resources`
# key — tagging goes on the `resource_attrs` wrapper that Metrics.from_dict
# unpacks. Same effect, different key.
def _tag_metrics(item: dict[str, Any]) -> dict[str, Any]:
resource_attrs = {**(item.get("resource_attrs") or {}), **SEEDER_MARKER}
return {**item, "resource_attrs": resource_attrs}
# Accept OTLP-style `resource_attributes` / `attributes` or legacy
# `resource_attrs` / `labels` interchangeably.
resource_attrs = {
**(item.get("resource_attrs") or {}),
**(item.get("resource_attributes") or {}),
**SEEDER_MARKER,
}
labels = {**(item.get("labels") or {}), **(item.get("attributes") or {})}
out = {**item, "resource_attrs": resource_attrs, "labels": labels}
out.pop("resource_attributes", None)
out.pop("attributes", None)
return out
@app.post("/telemetry/traces", status_code=status.HTTP_201_CREATED)
@@ -145,3 +157,17 @@ def delete_metrics() -> Response:
except Exception as e:
logger.exception("truncate failed")
raise HTTPException(status_code=500, detail=str(e)) from e
@app.post("/seed/golden", status_code=status.HTTP_200_OK)
def seed_golden() -> dict[str, int]:
"""Re-seed the golden dataset with timestamps rebased to `now`.
Called by Playwright globalSetup before every test session so chart
assertions land within default panel time windows."""
from fixtures import seed_golden_dataset # local import: fast cold-start
try:
return seed_golden_dataset.seed("http://localhost:8080")
except Exception as e:
logger.exception("golden seed failed")
raise HTTPException(500, str(e)) from e