diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 49ab21484b0..12bee7c6447 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -208,6 +208,9 @@ "isPaused": { "type": "boolean" }, + "keepFiringFor": { + "$ref": "#/definitions/Duration" + }, "labels": { "additionalProperties": { "type": "string" @@ -468,6 +471,10 @@ "health": { "type": "string" }, + "keepFiringFor": { + "format": "double", + "type": "number" + }, "labels": { "$ref": "#/definitions/Labels" }, @@ -3012,9 +3019,22 @@ "Interval": { "$ref": "#/definitions/Duration" }, + "Labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Limit": { + "format": "int64", + "type": "integer" + }, "Name": { "type": "string" }, + "QueryOffset": { + "type": "string" + }, "Rules": { "items": { "$ref": "#/definitions/PrometheusRule" @@ -3117,6 +3137,10 @@ "example": false, "type": "boolean" }, + "keep_firing_for": { + "format": "duration", + "type": "string" + }, "labels": { "additionalProperties": { "type": "string" @@ -4952,7 +4976,6 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert", "type": "object" diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go index f0202486e46..6e9f0f2bd4c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -192,9 +192,12 @@ type PrometheusNamespace struct { // swagger:model type PrometheusRuleGroup struct { - Name string `yaml:"name"` - Interval model.Duration `yaml:"interval"` - Rules []PrometheusRule `yaml:"rules"` + Name string `yaml:"name"` + Interval model.Duration `yaml:"interval"` + QueryOffset *model.Duration `yaml:"query_offset,omitempty"` + Limit int `yaml:"limit,omitempty"` + Rules []PrometheusRule `yaml:"rules"` + Labels map[string]string `yaml:"labels,omitempty"` } // swagger:model diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 3c9cc03dbec..5968f49fdbc 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -208,6 +208,9 @@ "isPaused": { "type": "boolean" }, + "keepFiringFor": { + "$ref": "#/definitions/Duration" + }, "labels": { "additionalProperties": { "type": "string" @@ -468,6 +471,10 @@ "health": { "type": "string" }, + "keepFiringFor": { + "format": "double", + "type": "number" + }, "labels": { "$ref": "#/definitions/Labels" }, @@ -3012,9 +3019,22 @@ "Interval": { "$ref": "#/definitions/Duration" }, + "Labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Limit": { + "format": "int64", + "type": "integer" + }, "Name": { "type": "string" }, + "QueryOffset": { + "type": "string" + }, "Rules": { "items": { "$ref": "#/definitions/PrometheusRule" @@ -3117,6 +3137,10 @@ "example": false, "type": "boolean" }, + "keep_firing_for": { + "format": "duration", + "type": "string" + }, "labels": { "additionalProperties": { "type": "string" diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 15d8a288303..706a40d67fd 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -4433,6 +4433,9 @@ "isPaused": { "type": "boolean" }, + "keepFiringFor": { + "$ref": "#/definitions/Duration" + }, "labels": { "type": "object", "additionalProperties": { @@ -4703,6 +4706,10 @@ "health": { "type": "string" }, + "keepFiringFor": { + "type": "number", + "format": "double" + }, "labels": { "$ref": "#/definitions/Labels" }, @@ -7238,9 +7245,22 @@ "Interval": { "$ref": "#/definitions/Duration" }, + "Labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Limit": { + "type": "integer", + "format": "int64" + }, "Name": { "type": "string" }, + "QueryOffset": { + "type": "string" + }, "Rules": { "type": "array", "items": { @@ -7354,6 +7374,10 @@ "type": "boolean", "example": false }, + "keep_firing_for": { + "type": "string", + "format": "duration" + }, "labels": { "type": "object", "additionalProperties": { diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index c86126bfb90..12e97106461 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -199,7 +199,7 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom var err error isRecordingRule := rule.Record != "" - query, err = p.createQuery(rule.Expr, isRecordingRule) + query, err = p.createQuery(rule.Expr, isRecordingRule, promGroup) if err != nil { return models.AlertRule{}, err } @@ -265,8 +265,16 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom // // This is needed to ensure that we keep the Prometheus behaviour, where any returned result // is considered alerting, and only when the query returns no data is the alert treated as normal. -func (p *Converter) createQuery(expr string, isRecordingRule bool) ([]models.AlertQuery, error) { - queryNode, err := createQueryNode(p.cfg.DatasourceUID, p.cfg.DatasourceType, expr, *p.cfg.FromTimeRange, *p.cfg.EvaluationOffset) +func (p *Converter) createQuery(expr string, isRecordingRule bool, promGroup PrometheusRuleGroup) ([]models.AlertQuery, error) { + // If evaluation offset is set on the group level, use that, otherwise use the global evaluation offset. + var evaluationOffset time.Duration + if promGroup.QueryOffset != nil { + evaluationOffset = time.Duration(*promGroup.QueryOffset) + } else { + evaluationOffset = *p.cfg.EvaluationOffset + } + + queryNode, err := createQueryNode(p.cfg.DatasourceUID, p.cfg.DatasourceType, expr, *p.cfg.FromTimeRange, evaluationOffset) if err != nil { return nil, err } diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index 7e0149ac14a..4e92a090b19 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -36,8 +36,9 @@ func TestPrometheusRulesToGrafana(t *testing.T) { orgID: 1, namespace: "some-namespace-uid", promGroup: PrometheusRuleGroup{ - Name: "test-group-1", - Interval: prommodel.Duration(10 * time.Second), + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + QueryOffset: util.Pointer(prommodel.Duration(1 * time.Minute)), Rules: []PrometheusRule{ { Alert: "alert-1", @@ -124,16 +125,13 @@ func TestPrometheusRulesToGrafana(t *testing.T) { expectError: false, }, { - name: "rule group with query_offset is not supported", + name: "query_offset must be >= 0", orgID: 1, namespace: "namespaceUID", promGroup: PrometheusRuleGroup{ - Name: "test-group-1", - Interval: prommodel.Duration(10 * time.Second), - QueryOffset: func() *prommodel.Duration { - d := prommodel.Duration(30 * time.Second) - return &d - }(), + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + QueryOffset: util.Pointer(prommodel.Duration(-1)), Rules: []PrometheusRule{ { Alert: "alert-1", @@ -142,7 +140,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) { }, }, expectError: true, - errorMsg: "query_offset is not supported", + errorMsg: "query_offset must be >= 0", }, { name: "rule group with limit is not supported", @@ -275,8 +273,13 @@ func TestPrometheusRulesToGrafana(t *testing.T) { if tc.config.EvaluationOffset != nil { evalOffset = *tc.config.EvaluationOffset } + if tc.promGroup.QueryOffset != nil { + // group-level offset takes precedence + evalOffset = time.Duration(*tc.promGroup.QueryOffset) + } + require.Equal(t, models.Duration(evalOffset), grafanaRule.Data[0].RelativeTimeRange.To) - require.Equal(t, models.Duration(evalOffset+10*time.Minute), grafanaRule.Data[0].RelativeTimeRange.From) + require.Equal(t, models.Duration(10*time.Minute+evalOffset), grafanaRule.Data[0].RelativeTimeRange.From) originalRuleDefinition, err := yaml.Marshal(promRule) require.NoError(t, err) diff --git a/pkg/services/ngalert/prom/models.go b/pkg/services/ngalert/prom/models.go index 21ea45061dc..0ef0a301885 100644 --- a/pkg/services/ngalert/prom/models.go +++ b/pkg/services/ngalert/prom/models.go @@ -25,14 +25,14 @@ type PrometheusRuleGroup struct { } func (g *PrometheusRuleGroup) Validate() error { - if g.QueryOffset != nil { - return ErrPrometheusRuleGroupValidationFailed.Errorf("query_offset is not supported") - } - if g.Limit != 0 { return ErrPrometheusRuleGroupValidationFailed.Errorf("limit is not supported") } + if g.QueryOffset != nil && *g.QueryOffset < prommodel.Duration(0) { + return ErrPrometheusRuleGroupValidationFailed.Errorf("query_offset must be >= 0") + } + for _, rule := range g.Rules { if err := rule.Validate(); err != nil { return err diff --git a/pkg/services/ngalert/prom/models_test.go b/pkg/services/ngalert/prom/models_test.go index b2bdb734b44..90be51c140c 100644 --- a/pkg/services/ngalert/prom/models_test.go +++ b/pkg/services/ngalert/prom/models_test.go @@ -26,6 +26,7 @@ func TestPrometheusRuleGroup_Validate(t *testing.T) { Labels: map[string]string{ "label-1": "value-1", }, + QueryOffset: util.Pointer(prommodel.Duration(time.Duration(1) * time.Second)), Rules: []PrometheusRule{ { Alert: "test_alert", @@ -36,14 +37,14 @@ func TestPrometheusRuleGroup_Validate(t *testing.T) { expectError: false, }, { - name: "invalid group with query_offset", + name: "invalid group with negative query_offset", group: PrometheusRuleGroup{ Name: "test_group", Interval: prommodel.Duration(60), - QueryOffset: util.Pointer(prommodel.Duration(10)), + QueryOffset: util.Pointer(prommodel.Duration(-1)), }, expectError: true, - errorMsg: "query_offset is not supported", + errorMsg: "query_offset must be >= 0", }, { name: "invalid group with limit", diff --git a/pkg/services/ngalert/prom/query.go b/pkg/services/ngalert/prom/query.go index 74aed34f83f..8e28b0ac3e7 100644 --- a/pkg/services/ngalert/prom/query.go +++ b/pkg/services/ngalert/prom/query.go @@ -43,7 +43,7 @@ func createQueryNode(datasourceUID, datasourceType, expr string, fromTimeRange, RefID: queryRefID, RelativeTimeRange: models.RelativeTimeRange{ From: models.Duration(fromTimeRange + evaluationOffset), - To: models.Duration(0 + evaluationOffset), + To: models.Duration(evaluationOffset), }, }, nil } diff --git a/public/api-merged.json b/public/api-merged.json index 6beecc94824..3ca4e15b213 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -12654,6 +12654,9 @@ "isPaused": { "type": "boolean" }, + "keepFiringFor": { + "$ref": "#/definitions/Duration" + }, "labels": { "type": "object", "additionalProperties": { @@ -12924,6 +12927,10 @@ "health": { "type": "string" }, + "keepFiringFor": { + "type": "number", + "format": "double" + }, "labels": { "$ref": "#/definitions/Labels" }, @@ -18688,9 +18695,22 @@ "Interval": { "$ref": "#/definitions/Duration" }, + "Labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Limit": { + "type": "integer", + "format": "int64" + }, "Name": { "type": "string" }, + "QueryOffset": { + "type": "string" + }, "Rules": { "type": "array", "items": { @@ -18804,6 +18824,10 @@ "type": "boolean", "example": false }, + "keep_firing_for": { + "type": "string", + "format": "duration" + }, "labels": { "type": "object", "additionalProperties": { @@ -22869,7 +22893,6 @@ } }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "type": "array", "items": { "type": "object", diff --git a/public/openapi3.json b/public/openapi3.json index 9440b5efd7d..d89f792df71 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -2715,6 +2715,9 @@ "isPaused": { "type": "boolean" }, + "keepFiringFor": { + "$ref": "#/components/schemas/Duration" + }, "labels": { "additionalProperties": { "type": "string" @@ -2975,6 +2978,10 @@ "health": { "type": "string" }, + "keepFiringFor": { + "format": "double", + "type": "number" + }, "labels": { "$ref": "#/components/schemas/Labels" }, @@ -8750,9 +8757,22 @@ "Interval": { "$ref": "#/components/schemas/Duration" }, + "Labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Limit": { + "format": "int64", + "type": "integer" + }, "Name": { "type": "string" }, + "QueryOffset": { + "type": "string" + }, "Rules": { "items": { "$ref": "#/components/schemas/PrometheusRule" @@ -8855,6 +8875,10 @@ "example": false, "type": "boolean" }, + "keep_firing_for": { + "format": "duration", + "type": "string" + }, "labels": { "additionalProperties": { "type": "string" @@ -12930,7 +12954,6 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/components/schemas/gettableAlert" },