mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-13 18:10:32 +01:00
Compare commits
1 Commits
hotfix/spa
...
feat/rules
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b961819b |
@@ -7551,35 +7551,28 @@ components:
|
||||
type: string
|
||||
disabled:
|
||||
type: boolean
|
||||
evalWindow:
|
||||
type: string
|
||||
evaluation:
|
||||
$ref: '#/components/schemas/RuletypesEvaluationEnvelope'
|
||||
frequency:
|
||||
type: string
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
notificationSettings:
|
||||
$ref: '#/components/schemas/RuletypesNotificationSettings'
|
||||
preferredChannels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
ruleType:
|
||||
$ref: '#/components/schemas/RuletypesRuleType'
|
||||
schemaVersion:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
version:
|
||||
enum:
|
||||
- v2alpha1
|
||||
type: string
|
||||
required:
|
||||
- alert
|
||||
- alertType
|
||||
- ruleType
|
||||
- condition
|
||||
- schemaVersion
|
||||
- evaluation
|
||||
- notificationSettings
|
||||
type: object
|
||||
RuletypesQueryType:
|
||||
enum:
|
||||
@@ -7629,12 +7622,8 @@ components:
|
||||
type: string
|
||||
disabled:
|
||||
type: boolean
|
||||
evalWindow:
|
||||
type: string
|
||||
evaluation:
|
||||
$ref: '#/components/schemas/RuletypesEvaluationEnvelope'
|
||||
frequency:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
labels:
|
||||
@@ -7643,15 +7632,11 @@ components:
|
||||
type: object
|
||||
notificationSettings:
|
||||
$ref: '#/components/schemas/RuletypesNotificationSettings'
|
||||
preferredChannels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
ruleType:
|
||||
$ref: '#/components/schemas/RuletypesRuleType'
|
||||
schemaVersion:
|
||||
type: string
|
||||
source:
|
||||
enum:
|
||||
- v2alpha1
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
@@ -7660,8 +7645,6 @@ components:
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- state
|
||||
@@ -7669,6 +7652,9 @@ components:
|
||||
- alertType
|
||||
- ruleType
|
||||
- condition
|
||||
- schemaVersion
|
||||
- evaluation
|
||||
- notificationSettings
|
||||
type: object
|
||||
RuletypesRuleCondition:
|
||||
properties:
|
||||
@@ -7681,10 +7667,6 @@ components:
|
||||
type: string
|
||||
compositeQuery:
|
||||
$ref: '#/components/schemas/RuletypesAlertCompositeQuery'
|
||||
matchType:
|
||||
$ref: '#/components/schemas/RuletypesMatchType'
|
||||
op:
|
||||
$ref: '#/components/schemas/RuletypesCompareOperator'
|
||||
requireMinPoints:
|
||||
type: boolean
|
||||
requiredNumPoints:
|
||||
@@ -7693,16 +7675,11 @@ components:
|
||||
$ref: '#/components/schemas/RuletypesSeasonality'
|
||||
selectedQueryName:
|
||||
type: string
|
||||
target:
|
||||
format: double
|
||||
nullable: true
|
||||
type: number
|
||||
targetUnit:
|
||||
type: string
|
||||
thresholds:
|
||||
$ref: '#/components/schemas/RuletypesRuleThresholdData'
|
||||
required:
|
||||
- compositeQuery
|
||||
- thresholds
|
||||
type: object
|
||||
RuletypesRuleThresholdData:
|
||||
discriminator:
|
||||
|
||||
@@ -8644,6 +8644,9 @@ export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
export enum RuletypesPostableRuleDTOSchemaVersion {
|
||||
v2alpha1 = 'v2alpha1',
|
||||
}
|
||||
export enum RuletypesSeasonalityDTO {
|
||||
hourly = 'hourly',
|
||||
daily = 'daily',
|
||||
@@ -8678,8 +8681,6 @@ export interface RuletypesRuleConditionDTO {
|
||||
*/
|
||||
algorithm?: string;
|
||||
compositeQuery: RuletypesAlertCompositeQueryDTO;
|
||||
matchType?: RuletypesMatchTypeDTO;
|
||||
op?: RuletypesCompareOperatorDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -8693,16 +8694,7 @@ export interface RuletypesRuleConditionDTO {
|
||||
* @type string
|
||||
*/
|
||||
selectedQueryName?: string;
|
||||
/**
|
||||
* @type number,null
|
||||
* @format double
|
||||
*/
|
||||
target?: number | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
targetUnit?: string;
|
||||
thresholds?: RuletypesRuleThresholdDataDTO;
|
||||
thresholds: RuletypesRuleThresholdDataDTO;
|
||||
}
|
||||
|
||||
export enum RuletypesRuleTypeDTO {
|
||||
@@ -8729,43 +8721,27 @@ export interface RuletypesPostableRuleDTO {
|
||||
* @type boolean
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
evalWindow?: string;
|
||||
evaluation?: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frequency?: string;
|
||||
evaluation: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
labels?: RuletypesPostableRuleDTOLabels;
|
||||
notificationSettings?: RuletypesNotificationSettingsDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
preferredChannels?: string[];
|
||||
notificationSettings: RuletypesNotificationSettingsDTO;
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
/**
|
||||
* @enum v2alpha1
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
version?: string;
|
||||
schemaVersion: RuletypesPostableRuleDTOSchemaVersion;
|
||||
}
|
||||
|
||||
export type RuletypesRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
export enum RuletypesRuleDTOSchemaVersion {
|
||||
v2alpha1 = 'v2alpha1',
|
||||
}
|
||||
export interface RuletypesRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -8794,15 +8770,7 @@ export interface RuletypesRuleDTO {
|
||||
* @type boolean
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
evalWindow?: string;
|
||||
evaluation?: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frequency?: string;
|
||||
evaluation: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -8811,20 +8779,13 @@ export interface RuletypesRuleDTO {
|
||||
* @type object
|
||||
*/
|
||||
labels?: RuletypesRuleDTOLabels;
|
||||
notificationSettings?: RuletypesNotificationSettingsDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
preferredChannels?: string[];
|
||||
notificationSettings: RuletypesNotificationSettingsDTO;
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
/**
|
||||
* @enum v2alpha1
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
schemaVersion: RuletypesRuleDTOSchemaVersion;
|
||||
state: RuletypesAlertStateDTO;
|
||||
/**
|
||||
* @type string
|
||||
@@ -8835,10 +8796,6 @@ export interface RuletypesRuleDTO {
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export enum RuletypesThresholdKindDTO {
|
||||
|
||||
@@ -71,12 +71,13 @@ function ActionsMenu({
|
||||
createRule({
|
||||
...rule,
|
||||
alert: `${rule.alert} - Copy`,
|
||||
} as RuletypesPostableRuleDTO).then(async (response) => {
|
||||
} as unknown as RuletypesPostableRuleDTO).then(async (response) => {
|
||||
await invalidateListRules(queryClient);
|
||||
const newRule = response.data;
|
||||
if (newRule) {
|
||||
onEdit(newRule as AlertRule);
|
||||
}
|
||||
return newRule;
|
||||
}),
|
||||
{
|
||||
loading: 'Cloning alert...',
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
RuletypesAlertStateDTO,
|
||||
RuletypesAlertTypeDTO,
|
||||
RuletypesEvaluationRollingDTOKind,
|
||||
RuletypesPanelTypeDTO,
|
||||
RuletypesQueryTypeDTO,
|
||||
RuletypesRuleDTOSchemaVersion,
|
||||
RuletypesRuleTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
@@ -16,6 +18,10 @@ const baseCondition: RuletypesRuleConditionDTO = {
|
||||
panelType: RuletypesPanelTypeDTO.graph,
|
||||
queries: null,
|
||||
},
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [{ name: 'critical', target: 90, matchType: '1', op: '1' }],
|
||||
},
|
||||
} as unknown as RuletypesRuleConditionDTO;
|
||||
|
||||
const make = (
|
||||
@@ -34,9 +40,12 @@ const make = (
|
||||
state: RuletypesAlertStateDTO.inactive,
|
||||
labels: { severity: 'info' },
|
||||
annotations: {},
|
||||
source: '',
|
||||
evalWindow: '5m0s',
|
||||
frequency: '1m0s',
|
||||
schemaVersion: RuletypesRuleDTOSchemaVersion.v2alpha1,
|
||||
evaluation: {
|
||||
kind: RuletypesEvaluationRollingDTOKind.rolling,
|
||||
spec: { evalWindow: '5m0s', frequency: '1m0s' },
|
||||
},
|
||||
notificationSettings: {},
|
||||
ruleType: RuletypesRuleTypeDTO.threshold_rule,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -61,6 +61,22 @@ function stripUndefinedLabels(
|
||||
return out;
|
||||
}
|
||||
|
||||
// why: the published DTO carries only the v2alpha1 contract, but the server
|
||||
// still accepts the legacy v1 fields (evalWindow, frequency,
|
||||
// preferredChannels, version, and a free-form schemaVersion) that the v1
|
||||
// editor flows send. LegacyRuleFields keeps those on the wire until
|
||||
// schemaVersion v1 is removed from the backend.
|
||||
type LegacyRuleFields = {
|
||||
evalWindow?: string;
|
||||
frequency?: string;
|
||||
preferredChannels?: string[];
|
||||
version?: string;
|
||||
schemaVersion?: string;
|
||||
description?: string;
|
||||
notificationSettings?: RuletypesPostableRuleDTO['notificationSettings'];
|
||||
evaluation?: RuletypesPostableRuleDTO['evaluation'];
|
||||
};
|
||||
|
||||
// why: local PostableAlertRuleV2/AlertDef diverge from RuletypesPostableRuleDTO
|
||||
// in several spots that match by string value but not by nominal TS type —
|
||||
// condition.{op,matchType}, evaluation.kind, notificationSettings.renotify.alertStates.
|
||||
@@ -69,24 +85,24 @@ function stripUndefinedLabels(
|
||||
export function toPostableRuleDTO(
|
||||
local: PostableAlertRuleV2,
|
||||
): RuletypesPostableRuleDTO {
|
||||
const payload: Record<keyof RuletypesPostableRuleDTO, any> = {
|
||||
const legacy = local as unknown as LegacyRuleFields;
|
||||
const payload: Record<string, unknown> = {
|
||||
alert: local.alert,
|
||||
alertType: toAlertTypeDTO(local.alertType),
|
||||
ruleType: toRuleTypeDTO(local.ruleType),
|
||||
condition: local.condition,
|
||||
annotations: local.annotations,
|
||||
labels: stripUndefinedLabels(local.labels),
|
||||
evalWindow: (local as unknown as RuletypesPostableRuleDTO).evalWindow,
|
||||
frequency: (local as unknown as RuletypesPostableRuleDTO).frequency,
|
||||
preferredChannels: (local as unknown as RuletypesPostableRuleDTO)
|
||||
.preferredChannels,
|
||||
evalWindow: legacy.evalWindow,
|
||||
frequency: legacy.frequency,
|
||||
preferredChannels: legacy.preferredChannels,
|
||||
notificationSettings: local.notificationSettings,
|
||||
evaluation: local.evaluation,
|
||||
schemaVersion: local.schemaVersion,
|
||||
source: local.source,
|
||||
version: local.version,
|
||||
disabled: local.disabled,
|
||||
description: (local as unknown as RuletypesPostableRuleDTO).description,
|
||||
description: legacy.description,
|
||||
};
|
||||
return payload as unknown as RuletypesPostableRuleDTO;
|
||||
}
|
||||
@@ -94,7 +110,8 @@ export function toPostableRuleDTO(
|
||||
export function toPostableRuleDTOFromAlertDef(
|
||||
local: AlertDef,
|
||||
): RuletypesPostableRuleDTO {
|
||||
const payload: Record<keyof RuletypesPostableRuleDTO, any> = {
|
||||
const legacy = local as unknown as LegacyRuleFields;
|
||||
const payload: Record<string, unknown> = {
|
||||
alert: local.alert,
|
||||
alertType: toAlertTypeDTO(local.alertType),
|
||||
ruleType: toRuleTypeDTO(local.ruleType),
|
||||
@@ -104,16 +121,15 @@ export function toPostableRuleDTOFromAlertDef(
|
||||
evalWindow: local.evalWindow,
|
||||
frequency: local.frequency,
|
||||
preferredChannels: local.preferredChannels,
|
||||
notificationSettings: (local as unknown as RuletypesPostableRuleDTO)
|
||||
.notificationSettings,
|
||||
evaluation: (local as unknown as RuletypesPostableRuleDTO).evaluation,
|
||||
schemaVersion: (local as unknown as RuletypesPostableRuleDTO).schemaVersion,
|
||||
notificationSettings: legacy.notificationSettings,
|
||||
evaluation: legacy.evaluation,
|
||||
schemaVersion: legacy.schemaVersion,
|
||||
source: local.source,
|
||||
version: local.version,
|
||||
disabled: local.disabled,
|
||||
description: (local as unknown as RuletypesPostableRuleDTO).description,
|
||||
description: legacy.description,
|
||||
};
|
||||
return payload;
|
||||
return payload as unknown as RuletypesPostableRuleDTO;
|
||||
}
|
||||
|
||||
export function fromRuleDTOToPostableRuleV2(
|
||||
|
||||
@@ -76,9 +76,12 @@ type PostableRule struct {
|
||||
}
|
||||
|
||||
type NotificationSettings struct {
|
||||
GroupBy []string `json:"groupBy,omitempty"`
|
||||
Renotify Renotify `json:"renotify,omitzero"`
|
||||
UsePolicy bool `json:"usePolicy,omitempty"`
|
||||
GroupBy []string `json:"groupBy,omitempty"`
|
||||
// Renotify is a pointer so that an explicitly disabled renotify
|
||||
// ({"enabled": false}) survives the read path; a value struct with
|
||||
// omitzero would drop it from every response.
|
||||
Renotify *Renotify `json:"renotify,omitempty"`
|
||||
UsePolicy bool `json:"usePolicy,omitempty"`
|
||||
// NewGroupEvalDelay is the grace period for new series to be excluded from alerts evaluation
|
||||
NewGroupEvalDelay valuer.TextDuration `json:"newGroupEvalDelay,omitzero"`
|
||||
}
|
||||
@@ -92,7 +95,7 @@ type Renotify struct {
|
||||
func (ns *NotificationSettings) GetAlertManagerNotificationConfig() alertmanagertypes.NotificationConfig {
|
||||
var renotifyInterval time.Duration
|
||||
var noDataRenotifyInterval time.Duration
|
||||
if ns.Renotify.Enabled {
|
||||
if ns.Renotify != nil && ns.Renotify.Enabled {
|
||||
if slices.Contains(ns.Renotify.AlertStates, StateNoData) {
|
||||
noDataRenotifyInterval = ns.Renotify.ReNotifyInterval.Duration()
|
||||
}
|
||||
@@ -204,10 +207,12 @@ func (ns *NotificationSettings) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
// Validate states after unmarshaling
|
||||
for _, state := range ns.Renotify.AlertStates {
|
||||
if state != StateFiring && state != StateNoData {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid alert state: %s", state)
|
||||
if ns.Renotify != nil {
|
||||
for _, state := range ns.Renotify.AlertStates {
|
||||
if state != StateFiring && state != StateNoData {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid alert state: %s", state)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -220,6 +225,12 @@ func (r *PostableRule) processRuleDefaults() {
|
||||
r.SchemaVersion = DefaultSchemaVersion
|
||||
}
|
||||
|
||||
// v5 is the only supported query version; default it so clients
|
||||
// don't have to send it.
|
||||
if r.Version == "" {
|
||||
r.Version = "v5"
|
||||
}
|
||||
|
||||
// v2alpha1 uses the Evaluation envelope for window/frequency;
|
||||
// only default top-level fields for v1.
|
||||
if r.SchemaVersion != SchemaVersionV2Alpha1 {
|
||||
@@ -271,7 +282,7 @@ func (r *PostableRule) processRuleDefaults() {
|
||||
r.RuleCondition.Thresholds = &thresholdData
|
||||
r.Evaluation = &EvaluationEnvelope{RollingEvaluation, RollingWindow{EvalWindow: r.EvalWindow, Frequency: r.Frequency}}
|
||||
r.NotificationSettings = &NotificationSettings{
|
||||
Renotify: Renotify{
|
||||
Renotify: &Renotify{
|
||||
Enabled: true,
|
||||
ReNotifyInterval: valuer.MustParseTextDuration("4h"),
|
||||
AlertStates: []AlertState{StateFiring},
|
||||
@@ -557,7 +568,7 @@ func (r *PostableRule) validateV2Alpha1() []error {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"notificationSettings: field is required for schemaVersion %q", SchemaVersionV2Alpha1))
|
||||
} else {
|
||||
if r.NotificationSettings.Renotify.Enabled && !r.NotificationSettings.Renotify.ReNotifyInterval.IsPositive() {
|
||||
if r.NotificationSettings.Renotify != nil && r.NotificationSettings.Renotify.Enabled && !r.NotificationSettings.Renotify.ReNotifyInterval.IsPositive() {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"notificationSettings.renotify.interval: must be a positive duration when renotify is enabled"))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package ruletypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -1327,3 +1328,105 @@ func TestAnomalyNegationEval(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenotifyRoundTrip ensures notificationSettings.renotify survives the
|
||||
// v2 read path (stored JSON -> GettableRule -> NewRule -> marshal) exactly as
|
||||
// stored, including an explicitly disabled renotify with no interval.
|
||||
func TestRenotifyRoundTrip(t *testing.T) {
|
||||
base := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "1", "op": "1"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": %s
|
||||
}`
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
settings string
|
||||
wantRenotify string
|
||||
}{
|
||||
{
|
||||
name: "absent renotify stays absent",
|
||||
settings: `{"usePolicy": false}`,
|
||||
wantRenotify: "",
|
||||
},
|
||||
{
|
||||
name: "explicitly disabled renotify is echoed",
|
||||
settings: `{"renotify": {"enabled": false}}`,
|
||||
wantRenotify: `{"enabled":false}`,
|
||||
},
|
||||
{
|
||||
name: "enabled renotify with states is echoed",
|
||||
settings: `{"renotify": {"enabled": true, "interval": "30m", "alertStates": ["firing"]}}`,
|
||||
wantRenotify: `{"enabled":true,"interval":"30m","alertStates":["firing"]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
stored := strings.Replace(base, "%s", tc.settings, 1)
|
||||
g := GettableRule{}
|
||||
if err := json.Unmarshal([]byte(stored), &g); err != nil {
|
||||
t.Fatalf("unmarshal stored rule: %v", err)
|
||||
}
|
||||
out, err := json.Marshal(NewRule(&g))
|
||||
if err != nil {
|
||||
t.Fatalf("marshal v2 rule: %v", err)
|
||||
}
|
||||
var resp struct {
|
||||
NotificationSettings struct {
|
||||
Renotify json.RawMessage `json:"renotify"`
|
||||
} `json:"notificationSettings"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &resp); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
got := string(resp.NotificationSettings.Renotify)
|
||||
if got != tc.wantRenotify {
|
||||
t.Errorf("renotify round-trip mismatch: got %q, want %q", got, tc.wantRenotify)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVersionDefaultsToV5 ensures rules posted without a version parse and
|
||||
// validate with version defaulted to v5.
|
||||
func TestVersionDefaultsToV5(t *testing.T) {
|
||||
content := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "1", "op": "1"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": {"usePolicy": false}
|
||||
}`
|
||||
|
||||
rule := PostableRule{}
|
||||
if err := json.Unmarshal([]byte(content), &rule); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if rule.Version != "v5" {
|
||||
t.Errorf("expected version to default to v5, got %q", rule.Version)
|
||||
}
|
||||
if err := rule.Validate(); err != nil {
|
||||
t.Errorf("expected rule without version to validate, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
46
pkg/types/ruletypes/jsonschema.go
Normal file
46
pkg/types/ruletypes/jsonschema.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
var (
|
||||
_ jsonschema.Preparer = (*PostableRule)(nil)
|
||||
_ jsonschema.Preparer = (*RuleCondition)(nil)
|
||||
)
|
||||
|
||||
// PrepareJSONSchema restricts the published rule schema to the v2alpha1
|
||||
// contract: the v1-only fields (evalWindow, frequency, preferredChannels) and
|
||||
// the query version (always "v5", defaulted by the server) are omitted, and
|
||||
// the fields the server requires for schemaVersion v2alpha1 are marked
|
||||
// required. Runtime JSON handling is unchanged and continues to load stored
|
||||
// v1 rules until schemaVersion v1 is removed.
|
||||
//
|
||||
// Rule and GettableRule embed PostableRule, so this also applies to the
|
||||
// published read models through method promotion.
|
||||
func (r *PostableRule) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
for _, name := range []string{"evalWindow", "frequency", "preferredChannels", "version", "source"} {
|
||||
delete(schema.Properties, name)
|
||||
}
|
||||
|
||||
schema.Required = append(schema.Required, "schemaVersion", "evaluation", "notificationSettings")
|
||||
|
||||
if prop, ok := schema.Properties["schemaVersion"]; ok && prop.TypeObject != nil {
|
||||
prop.TypeObject.WithEnum(SchemaVersionV2Alpha1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareJSONSchema removes the v1-only condition fields (target, op,
|
||||
// matchType, targetUnit — expressed per threshold entry in v2alpha1) from the
|
||||
// published schema and marks thresholds required.
|
||||
func (rc *RuleCondition) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
for _, name := range []string{"target", "op", "matchType", "targetUnit"} {
|
||||
delete(schema.Properties, name)
|
||||
}
|
||||
|
||||
schema.Required = append(schema.Required, "thresholds")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -169,12 +169,10 @@ func TestValidate_PostableRule_Common(t *testing.T) {
|
||||
errSubstr: "alert",
|
||||
},
|
||||
|
||||
// only "v5" is allowed
|
||||
// only "v5" is allowed; missing/empty defaults to "v5"
|
||||
{
|
||||
name: "missing version",
|
||||
json: removeField(validV1Builder(), "version"),
|
||||
wantErr: true,
|
||||
errSubstr: "version",
|
||||
name: "missing version defaults to v5",
|
||||
json: removeField(validV1Builder(), "version"),
|
||||
},
|
||||
{
|
||||
name: "wrong version v4",
|
||||
@@ -189,10 +187,8 @@ func TestValidate_PostableRule_Common(t *testing.T) {
|
||||
errSubstr: "version",
|
||||
},
|
||||
{
|
||||
name: "empty version",
|
||||
json: patchJSON(validV1Builder(), `{"version": ""}`),
|
||||
wantErr: true,
|
||||
errSubstr: "version",
|
||||
name: "empty version defaults to v5",
|
||||
json: patchJSON(validV1Builder(), `{"version": ""}`),
|
||||
},
|
||||
|
||||
// alert type, capital case to avoid breaking changes
|
||||
|
||||
Reference in New Issue
Block a user