mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-12 21:20:30 +01:00
Compare commits
2 Commits
feat/maint
...
mute-rules
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58c2e8366e | ||
|
|
17f0c33fe0 |
@@ -81,6 +81,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
FormInstance,
|
||||
Input,
|
||||
Modal,
|
||||
Radio,
|
||||
Select,
|
||||
SelectProps,
|
||||
Spin,
|
||||
@@ -67,11 +68,14 @@ 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[];
|
||||
timezone?: string;
|
||||
labelExpression?: string;
|
||||
@@ -129,6 +133,12 @@ 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;
|
||||
@@ -144,9 +154,12 @@ export function PlannedDowntimeForm(
|
||||
const saveHandler = useCallback(
|
||||
async (values: PlannedDowntimeFormData) => {
|
||||
const data: RuletypesPostablePlannedMaintenanceDTO = {
|
||||
alertIds: values.alertRules
|
||||
.map((alert) => alert.value)
|
||||
.filter((alert) => alert !== undefined) as string[],
|
||||
alertIds:
|
||||
values.alertRuleScope === 'all'
|
||||
? []
|
||||
: (values.alertRules
|
||||
.map((alert) => alert.value)
|
||||
.filter((alert) => alert !== undefined) as string[]),
|
||||
name: values.name,
|
||||
labelExpression: values.labelExpression,
|
||||
schedule: {
|
||||
@@ -262,12 +275,13 @@ export function PlannedDowntimeForm(
|
||||
};
|
||||
|
||||
const formatedInitialValues = useMemo(() => {
|
||||
const initialAlertIds = initialValues.alertIds || [];
|
||||
const isExistingSchedule = Boolean(initialValues.id);
|
||||
const formData: PlannedDowntimeFormData = {
|
||||
name: defaultTo(initialValues.name, ''),
|
||||
alertRules: getAlertOptionsFromIds(
|
||||
initialValues.alertIds || [],
|
||||
alertOptions,
|
||||
),
|
||||
alertRuleScope:
|
||||
isExistingSchedule && initialAlertIds.length === 0 ? 'all' : 'specific',
|
||||
alertRules: getAlertOptionsFromIds(initialAlertIds, alertOptions),
|
||||
endTime: getEndTime(initialValues) ? dayjs(getEndTime(initialValues)) : '',
|
||||
startTime: initialValues.schedule?.startTime
|
||||
? dayjs(initialValues.schedule?.startTime)
|
||||
@@ -291,6 +305,7 @@ export function PlannedDowntimeForm(
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTags(formatedInitialValues.alertRules);
|
||||
setAlertRuleScope(formatedInitialValues.alertRuleScope);
|
||||
form.setFieldsValue({ ...formatedInitialValues });
|
||||
}, [form, formatedInitialValues, initialValues]);
|
||||
|
||||
@@ -410,6 +425,9 @@ 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"
|
||||
@@ -521,49 +539,82 @@ export function PlannedDowntimeForm(
|
||||
<div className="scheduleTimeInfoText">{endTimeText}</div>
|
||||
)}
|
||||
<div>
|
||||
<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}
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
{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 }}
|
||||
>
|
||||
{alertOptions?.map((option) => (
|
||||
<Select.Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
All alerts will be silenced for the duration of this maintenance window.
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<Form.Item
|
||||
label={
|
||||
|
||||
@@ -207,7 +207,7 @@ export function CollapseListContent({
|
||||
selectedTags={alertOptions}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
<Tag className="all-alerts-tag">All alert rules</Tag>
|
||||
),
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -116,6 +116,22 @@ 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"},
|
||||
|
||||
@@ -10,6 +10,7 @@ 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)
|
||||
|
||||
@@ -185,6 +185,53 @@ 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()
|
||||
|
||||
@@ -15,6 +15,44 @@ import (
|
||||
)
|
||||
|
||||
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},
|
||||
}
|
||||
}
|
||||
|
||||
type MaintenanceStatus struct {
|
||||
valuer.String
|
||||
|
||||
Reference in New Issue
Block a user