[release-12.0.2] Alerting: Fix group-level labels and query_offset in the import API (#106392)

Alerting: Fix group-level labels and query_offset in the import API (#106379)

What is this feature?

Fixes a bug when group-level query_offset and labels parameters are ignored and not saved

Why do we need this feature?

In the import API Prometheus YAML rule definitions are supported:

groups:
  - name: group-1
    interval: 1m
    query_offset: 10m
    labels:
      severity: "warning"
    rules:
      - alert: Alert 0 > 0
        expr: vector(0) > 0

But applying group-level labels and query_offset is broken and they are not saved right now because during the conversion of the API model to PrometheusRuleGroup they aren't saved to the new structure.

(cherry picked from commit f7a52bc04e)
pull/106401/head
Alexander Akhmetov 3 weeks ago committed by GitHub
parent d6b7bb6759
commit c53915cb87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      pkg/services/ngalert/api/api_convert_prometheus.go
  2. 13
      pkg/services/ngalert/api/api_convert_prometheus_test.go
  3. 26
      pkg/services/ngalert/api/tooling/api.json
  4. 26
      pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go
  5. 26
      pkg/services/ngalert/api/tooling/post.json
  6. 26
      pkg/services/ngalert/api/tooling/spec.json
  7. 7
      pkg/services/ngalert/prom/convert.go
  8. 6
      pkg/services/ngalert/prom/convert_test.go
  9. 67
      pkg/tests/api/alerting/api_convert_prometheus_test.go
  10. 26
      public/api-merged.json
  11. 26
      public/openapi3.json

@ -459,9 +459,12 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
} }
} }
group := prom.PrometheusRuleGroup{ group := prom.PrometheusRuleGroup{
Name: promGroup.Name, Name: promGroup.Name,
Interval: promGroup.Interval, Interval: promGroup.Interval,
Rules: rules, Rules: rules,
QueryOffset: promGroup.QueryOffset,
Limit: promGroup.Limit,
Labels: promGroup.Labels,
} }
converter, err := prom.NewConverter( converter, err := prom.NewConverter(

@ -1012,12 +1012,17 @@ func TestRouteConvertPrometheusPostRuleGroups(t *testing.T) {
Name: "TestGroup1", Name: "TestGroup1",
Interval: prommodel.Duration(1 * time.Minute), Interval: prommodel.Duration(1 * time.Minute),
Rules: []apimodels.PrometheusRule{promAlertRule}, Rules: []apimodels.PrometheusRule{promAlertRule},
Labels: map[string]string{
"group_label": "group_value",
},
} }
queryOffset := prommodel.Duration(5 * time.Minute)
promGroup2 := apimodels.PrometheusRuleGroup{ promGroup2 := apimodels.PrometheusRuleGroup{
Name: "TestGroup2", Name: "TestGroup2",
Interval: prommodel.Duration(1 * time.Minute), Interval: prommodel.Duration(1 * time.Minute),
Rules: []apimodels.PrometheusRule{promAlertRule}, Rules: []apimodels.PrometheusRule{promAlertRule},
QueryOffset: &queryOffset,
} }
promGroup3 := apimodels.PrometheusRuleGroup{ promGroup3 := apimodels.PrometheusRuleGroup{
@ -1053,10 +1058,12 @@ func TestRouteConvertPrometheusPostRuleGroups(t *testing.T) {
require.Equal(t, "TestAlert", rule.Title) require.Equal(t, "TestAlert", rule.Title)
require.Equal(t, "critical", rule.Labels["severity"]) require.Equal(t, "critical", rule.Labels["severity"])
require.Equal(t, 5*time.Minute, rule.For) require.Equal(t, 5*time.Minute, rule.For)
require.Equal(t, "group_value", rule.Labels["group_label"])
case "TestGroup2": case "TestGroup2":
require.Equal(t, "TestAlert", rule.Title) require.Equal(t, "TestAlert", rule.Title)
require.Equal(t, "critical", rule.Labels["severity"]) require.Equal(t, "critical", rule.Labels["severity"])
require.Equal(t, 5*time.Minute, rule.For) require.Equal(t, 5*time.Minute, rule.For)
require.Equal(t, models.Duration(queryOffset), rule.Data[0].RelativeTimeRange.To)
case "TestGroup3": case "TestGroup3":
switch rule.Title { switch rule.Title {
case "TestAlert": case "TestAlert":

@ -3030,31 +3030,31 @@
}, },
"PrometheusRule": { "PrometheusRule": {
"properties": { "properties": {
"Alert": { "alert": {
"type": "string" "type": "string"
}, },
"Annotations": { "annotations": {
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"type": "object" "type": "object"
}, },
"Expr": { "expr": {
"type": "string" "type": "string"
}, },
"For": { "for": {
"type": "string" "type": "string"
}, },
"KeepFiringFor": { "keep_firing_for": {
"type": "string" "type": "string"
}, },
"Labels": { "labels": {
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"type": "object" "type": "object"
}, },
"Record": { "record": {
"type": "string" "type": "string"
} }
}, },
@ -3062,26 +3062,26 @@
}, },
"PrometheusRuleGroup": { "PrometheusRuleGroup": {
"properties": { "properties": {
"Interval": { "interval": {
"$ref": "#/definitions/Duration" "$ref": "#/definitions/Duration"
}, },
"Labels": { "labels": {
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"type": "object" "type": "object"
}, },
"Limit": { "limit": {
"format": "int64", "format": "int64",
"type": "integer" "type": "integer"
}, },
"Name": { "name": {
"type": "string" "type": "string"
}, },
"QueryOffset": { "query_offset": {
"type": "string" "type": "string"
}, },
"Rules": { "rules": {
"items": { "items": {
"$ref": "#/definitions/PrometheusRule" "$ref": "#/definitions/PrometheusRule"
}, },

@ -222,23 +222,23 @@ type PrometheusNamespace struct {
// swagger:model // swagger:model
type PrometheusRuleGroup struct { type PrometheusRuleGroup struct {
Name string `yaml:"name"` Name string `yaml:"name" json:"name"`
Interval model.Duration `yaml:"interval"` Interval model.Duration `yaml:"interval" json:"interval"`
QueryOffset *model.Duration `yaml:"query_offset,omitempty"` QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"`
Limit int `yaml:"limit,omitempty"` Limit int `yaml:"limit,omitempty" json:"limit,omitempty"`
Rules []PrometheusRule `yaml:"rules"` Rules []PrometheusRule `yaml:"rules" json:"rules"`
Labels map[string]string `yaml:"labels,omitempty"` Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
} }
// swagger:model // swagger:model
type PrometheusRule struct { type PrometheusRule struct {
Alert string `yaml:"alert,omitempty"` Alert string `yaml:"alert,omitempty" json:"alert,omitempty"`
Expr string `yaml:"expr"` Expr string `yaml:"expr" json:"expr"`
For *model.Duration `yaml:"for,omitempty"` For *model.Duration `yaml:"for,omitempty" json:"for,omitempty"`
KeepFiringFor *model.Duration `yaml:"keep_firing_for,omitempty"` KeepFiringFor *model.Duration `yaml:"keep_firing_for,omitempty" json:"keep_firing_for,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"` Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
Annotations map[string]string `yaml:"annotations,omitempty"` Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"`
Record string `yaml:"record,omitempty"` Record string `yaml:"record,omitempty" json:"record,omitempty"`
} }
// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusCortexDeleteRuleGroup RouteConvertPrometheusGetRuleGroup RouteConvertPrometheusCortexGetRuleGroup // swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusCortexDeleteRuleGroup RouteConvertPrometheusGetRuleGroup RouteConvertPrometheusCortexGetRuleGroup

@ -3030,31 +3030,31 @@
}, },
"PrometheusRule": { "PrometheusRule": {
"properties": { "properties": {
"Alert": { "alert": {
"type": "string" "type": "string"
}, },
"Annotations": { "annotations": {
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"type": "object" "type": "object"
}, },
"Expr": { "expr": {
"type": "string" "type": "string"
}, },
"For": { "for": {
"type": "string" "type": "string"
}, },
"KeepFiringFor": { "keep_firing_for": {
"type": "string" "type": "string"
}, },
"Labels": { "labels": {
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"type": "object" "type": "object"
}, },
"Record": { "record": {
"type": "string" "type": "string"
} }
}, },
@ -3062,26 +3062,26 @@
}, },
"PrometheusRuleGroup": { "PrometheusRuleGroup": {
"properties": { "properties": {
"Interval": { "interval": {
"$ref": "#/definitions/Duration" "$ref": "#/definitions/Duration"
}, },
"Labels": { "labels": {
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"type": "object" "type": "object"
}, },
"Limit": { "limit": {
"format": "int64", "format": "int64",
"type": "integer" "type": "integer"
}, },
"Name": { "name": {
"type": "string" "type": "string"
}, },
"QueryOffset": { "query_offset": {
"type": "string" "type": "string"
}, },
"Rules": { "rules": {
"items": { "items": {
"$ref": "#/definitions/PrometheusRule" "$ref": "#/definitions/PrometheusRule"
}, },

@ -7131,31 +7131,31 @@
"PrometheusRule": { "PrometheusRule": {
"type": "object", "type": "object",
"properties": { "properties": {
"Alert": { "alert": {
"type": "string" "type": "string"
}, },
"Annotations": { "annotations": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} }
}, },
"Expr": { "expr": {
"type": "string" "type": "string"
}, },
"For": { "for": {
"type": "string" "type": "string"
}, },
"KeepFiringFor": { "keep_firing_for": {
"type": "string" "type": "string"
}, },
"Labels": { "labels": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} }
}, },
"Record": { "record": {
"type": "string" "type": "string"
} }
} }
@ -7163,26 +7163,26 @@
"PrometheusRuleGroup": { "PrometheusRuleGroup": {
"type": "object", "type": "object",
"properties": { "properties": {
"Interval": { "interval": {
"$ref": "#/definitions/Duration" "$ref": "#/definitions/Duration"
}, },
"Labels": { "labels": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} }
}, },
"Limit": { "limit": {
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"Name": { "name": {
"type": "string" "type": "string"
}, },
"QueryOffset": { "query_offset": {
"type": "string" "type": "string"
}, },
"Rules": { "rules": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/PrometheusRule" "$ref": "#/definitions/PrometheusRule"

@ -235,6 +235,13 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom
maps.Copy(labels, promGroup.Labels) maps.Copy(labels, promGroup.Labels)
maps.Copy(labels, rule.Labels) maps.Copy(labels, rule.Labels)
// Save the merged group-level + rule-level labels to the original rule,
// to ensure that they are saved to the original YAML rule definition.
if rule.Labels == nil {
rule.Labels = make(map[string]string)
}
maps.Copy(rule.Labels, labels)
originalRuleDefinition, err := yaml.Marshal(rule) originalRuleDefinition, err := yaml.Marshal(rule)
if err != nil { if err != nil {
return models.AlertRule{}, fmt.Errorf("failed to marshal original rule definition: %w", err) return models.AlertRule{}, fmt.Errorf("failed to marshal original rule definition: %w", err)

@ -346,6 +346,12 @@ func TestPrometheusRulesToGrafana(t *testing.T) {
require.Equal(t, models.OkErrState, grafanaRule.ExecErrState) require.Equal(t, models.OkErrState, grafanaRule.ExecErrState)
require.Equal(t, models.OK, grafanaRule.NoDataState) require.Equal(t, models.OK, grafanaRule.NoDataState)
// Update the rule with the group-level labels,
// to test that they are saved to the rule definition.
mergedLabels := make(map[string]string)
maps.Copy(mergedLabels, tc.promGroup.Labels)
maps.Copy(mergedLabels, promRule.Labels)
promRule.Labels = mergedLabels
originalRuleDefinition, err := yaml.Marshal(promRule) originalRuleDefinition, err := yaml.Marshal(promRule)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, string(originalRuleDefinition), grafanaRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition) require.Equal(t, string(originalRuleDefinition), grafanaRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition)

@ -2,6 +2,7 @@ package alerting
import ( import (
"encoding/json" "encoding/json"
"maps"
"net/http" "net/http"
"testing" "testing"
"time" "time"
@ -1195,3 +1196,69 @@ func TestIntegrationConvertPrometheusEndpoints_Delete(t *testing.T) {
runTest(t, true) runTest(t, true)
}) })
} }
func TestIntegrationConvertPrometheusEndpoints_GroupLabels(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableRecordingRules: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
testGroup := apimodels.PrometheusRuleGroup{
Name: "test-group-with-labels",
Interval: prommodel.Duration(60 * time.Second),
Labels: map[string]string{
"group_label": "value-1",
},
Rules: []apimodels.PrometheusRule{
{
Alert: "TestAlert",
Expr: "up == 0",
For: util.Pointer(prommodel.Duration(2 * time.Minute)),
Labels: map[string]string{
"rule_label": "value-2",
},
Annotations: map[string]string{
"annotation-1": "annotation-value",
},
},
},
}
namespace := "test-namespace-1"
namespaceUID := util.GenerateShortUID()
apiClient.CreateFolder(t, namespaceUID, namespace)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, testGroup, nil)
expectedLabels := make(map[string]string)
maps.Copy(expectedLabels, testGroup.Labels)
maps.Copy(expectedLabels, testGroup.Rules[0].Labels)
// Verify the Import API returns the expected merged format
group := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace, testGroup.Name, nil)
testGroup.Labels = nil
testGroup.Rules[0].Labels = expectedLabels
require.Equal(t, testGroup, group)
ruleGroup, _, _ := apiClient.GetRulesGroupWithStatus(t, namespaceUID, testGroup.Name)
require.Len(t, ruleGroup.Rules, 1)
rule := ruleGroup.Rules[0]
require.Equal(t, expectedLabels, rule.Labels)
}

@ -18705,31 +18705,31 @@
"PrometheusRule": { "PrometheusRule": {
"type": "object", "type": "object",
"properties": { "properties": {
"Alert": { "alert": {
"type": "string" "type": "string"
}, },
"Annotations": { "annotations": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} }
}, },
"Expr": { "expr": {
"type": "string" "type": "string"
}, },
"For": { "for": {
"type": "string" "type": "string"
}, },
"KeepFiringFor": { "keep_firing_for": {
"type": "string" "type": "string"
}, },
"Labels": { "labels": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} }
}, },
"Record": { "record": {
"type": "string" "type": "string"
} }
} }
@ -18737,26 +18737,26 @@
"PrometheusRuleGroup": { "PrometheusRuleGroup": {
"type": "object", "type": "object",
"properties": { "properties": {
"Interval": { "interval": {
"$ref": "#/definitions/Duration" "$ref": "#/definitions/Duration"
}, },
"Labels": { "labels": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} }
}, },
"Limit": { "limit": {
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"Name": { "name": {
"type": "string" "type": "string"
}, },
"QueryOffset": { "query_offset": {
"type": "string" "type": "string"
}, },
"Rules": { "rules": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/PrometheusRule" "$ref": "#/definitions/PrometheusRule"

@ -8766,31 +8766,31 @@
}, },
"PrometheusRule": { "PrometheusRule": {
"properties": { "properties": {
"Alert": { "alert": {
"type": "string" "type": "string"
}, },
"Annotations": { "annotations": {
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"type": "object" "type": "object"
}, },
"Expr": { "expr": {
"type": "string" "type": "string"
}, },
"For": { "for": {
"type": "string" "type": "string"
}, },
"KeepFiringFor": { "keep_firing_for": {
"type": "string" "type": "string"
}, },
"Labels": { "labels": {
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"type": "object" "type": "object"
}, },
"Record": { "record": {
"type": "string" "type": "string"
} }
}, },
@ -8798,26 +8798,26 @@
}, },
"PrometheusRuleGroup": { "PrometheusRuleGroup": {
"properties": { "properties": {
"Interval": { "interval": {
"$ref": "#/components/schemas/Duration" "$ref": "#/components/schemas/Duration"
}, },
"Labels": { "labels": {
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"type": "object" "type": "object"
}, },
"Limit": { "limit": {
"format": "int64", "format": "int64",
"type": "integer" "type": "integer"
}, },
"Name": { "name": {
"type": "string" "type": "string"
}, },
"QueryOffset": { "query_offset": {
"type": "string" "type": "string"
}, },
"Rules": { "rules": {
"items": { "items": {
"$ref": "#/components/schemas/PrometheusRule" "$ref": "#/components/schemas/PrometheusRule"
}, },

Loading…
Cancel
Save