diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index b47334df8cc..c3974d2c3ac 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -459,9 +459,12 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup( } } group := prom.PrometheusRuleGroup{ - Name: promGroup.Name, - Interval: promGroup.Interval, - Rules: rules, + Name: promGroup.Name, + Interval: promGroup.Interval, + Rules: rules, + QueryOffset: promGroup.QueryOffset, + Limit: promGroup.Limit, + Labels: promGroup.Labels, } converter, err := prom.NewConverter( diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 2b493a3413c..867b3b817e3 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -1012,12 +1012,17 @@ func TestRouteConvertPrometheusPostRuleGroups(t *testing.T) { Name: "TestGroup1", Interval: prommodel.Duration(1 * time.Minute), Rules: []apimodels.PrometheusRule{promAlertRule}, + Labels: map[string]string{ + "group_label": "group_value", + }, } + queryOffset := prommodel.Duration(5 * time.Minute) promGroup2 := apimodels.PrometheusRuleGroup{ - Name: "TestGroup2", - Interval: prommodel.Duration(1 * time.Minute), - Rules: []apimodels.PrometheusRule{promAlertRule}, + Name: "TestGroup2", + Interval: prommodel.Duration(1 * time.Minute), + Rules: []apimodels.PrometheusRule{promAlertRule}, + QueryOffset: &queryOffset, } promGroup3 := apimodels.PrometheusRuleGroup{ @@ -1053,10 +1058,12 @@ func TestRouteConvertPrometheusPostRuleGroups(t *testing.T) { require.Equal(t, "TestAlert", rule.Title) require.Equal(t, "critical", rule.Labels["severity"]) require.Equal(t, 5*time.Minute, rule.For) + require.Equal(t, "group_value", rule.Labels["group_label"]) case "TestGroup2": require.Equal(t, "TestAlert", rule.Title) require.Equal(t, "critical", rule.Labels["severity"]) require.Equal(t, 5*time.Minute, rule.For) + require.Equal(t, models.Duration(queryOffset), rule.Data[0].RelativeTimeRange.To) case "TestGroup3": switch rule.Title { case "TestAlert": diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 696e5686d06..119c3b343cd 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -3030,31 +3030,31 @@ }, "PrometheusRule": { "properties": { - "Alert": { + "alert": { "type": "string" }, - "Annotations": { + "annotations": { "additionalProperties": { "type": "string" }, "type": "object" }, - "Expr": { + "expr": { "type": "string" }, - "For": { + "for": { "type": "string" }, - "KeepFiringFor": { + "keep_firing_for": { "type": "string" }, - "Labels": { + "labels": { "additionalProperties": { "type": "string" }, "type": "object" }, - "Record": { + "record": { "type": "string" } }, @@ -3062,26 +3062,26 @@ }, "PrometheusRuleGroup": { "properties": { - "Interval": { + "interval": { "$ref": "#/definitions/Duration" }, - "Labels": { + "labels": { "additionalProperties": { "type": "string" }, "type": "object" }, - "Limit": { + "limit": { "format": "int64", "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "QueryOffset": { + "query_offset": { "type": "string" }, - "Rules": { + "rules": { "items": { "$ref": "#/definitions/PrometheusRule" }, 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 9758cdf0215..9a628d2ce42 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -222,23 +222,23 @@ type PrometheusNamespace struct { // swagger:model type PrometheusRuleGroup struct { - 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"` + Name string `yaml:"name" json:"name"` + Interval model.Duration `yaml:"interval" json:"interval"` + QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"` + Limit int `yaml:"limit,omitempty" json:"limit,omitempty"` + Rules []PrometheusRule `yaml:"rules" json:"rules"` + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` } // swagger:model type PrometheusRule struct { - Alert string `yaml:"alert,omitempty"` - Expr string `yaml:"expr"` - For *model.Duration `yaml:"for,omitempty"` - KeepFiringFor *model.Duration `yaml:"keep_firing_for,omitempty"` - Labels map[string]string `yaml:"labels,omitempty"` - Annotations map[string]string `yaml:"annotations,omitempty"` - Record string `yaml:"record,omitempty"` + Alert string `yaml:"alert,omitempty" json:"alert,omitempty"` + Expr string `yaml:"expr" json:"expr"` + For *model.Duration `yaml:"for,omitempty" json:"for,omitempty"` + KeepFiringFor *model.Duration `yaml:"keep_firing_for,omitempty" json:"keep_firing_for,omitempty"` + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` + Record string `yaml:"record,omitempty" json:"record,omitempty"` } // swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusCortexDeleteRuleGroup RouteConvertPrometheusGetRuleGroup RouteConvertPrometheusCortexGetRuleGroup diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index a8c9b24f49e..f1e70b91c39 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -3030,31 +3030,31 @@ }, "PrometheusRule": { "properties": { - "Alert": { + "alert": { "type": "string" }, - "Annotations": { + "annotations": { "additionalProperties": { "type": "string" }, "type": "object" }, - "Expr": { + "expr": { "type": "string" }, - "For": { + "for": { "type": "string" }, - "KeepFiringFor": { + "keep_firing_for": { "type": "string" }, - "Labels": { + "labels": { "additionalProperties": { "type": "string" }, "type": "object" }, - "Record": { + "record": { "type": "string" } }, @@ -3062,26 +3062,26 @@ }, "PrometheusRuleGroup": { "properties": { - "Interval": { + "interval": { "$ref": "#/definitions/Duration" }, - "Labels": { + "labels": { "additionalProperties": { "type": "string" }, "type": "object" }, - "Limit": { + "limit": { "format": "int64", "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "QueryOffset": { + "query_offset": { "type": "string" }, - "Rules": { + "rules": { "items": { "$ref": "#/definitions/PrometheusRule" }, diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 1875f799b8b..8564be18e0f 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -7131,31 +7131,31 @@ "PrometheusRule": { "type": "object", "properties": { - "Alert": { + "alert": { "type": "string" }, - "Annotations": { + "annotations": { "type": "object", "additionalProperties": { "type": "string" } }, - "Expr": { + "expr": { "type": "string" }, - "For": { + "for": { "type": "string" }, - "KeepFiringFor": { + "keep_firing_for": { "type": "string" }, - "Labels": { + "labels": { "type": "object", "additionalProperties": { "type": "string" } }, - "Record": { + "record": { "type": "string" } } @@ -7163,26 +7163,26 @@ "PrometheusRuleGroup": { "type": "object", "properties": { - "Interval": { + "interval": { "$ref": "#/definitions/Duration" }, - "Labels": { + "labels": { "type": "object", "additionalProperties": { "type": "string" } }, - "Limit": { + "limit": { "type": "integer", "format": "int64" }, - "Name": { + "name": { "type": "string" }, - "QueryOffset": { + "query_offset": { "type": "string" }, - "Rules": { + "rules": { "type": "array", "items": { "$ref": "#/definitions/PrometheusRule" diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index df0adedbff5..9350f4b718c 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -235,6 +235,13 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom maps.Copy(labels, promGroup.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) if err != nil { return models.AlertRule{}, fmt.Errorf("failed to marshal original rule definition: %w", err) diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index 9fed80776e7..324c20ff208 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -346,6 +346,12 @@ func TestPrometheusRulesToGrafana(t *testing.T) { require.Equal(t, models.OkErrState, grafanaRule.ExecErrState) 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) require.NoError(t, err) require.Equal(t, string(originalRuleDefinition), grafanaRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition) diff --git a/pkg/tests/api/alerting/api_convert_prometheus_test.go b/pkg/tests/api/alerting/api_convert_prometheus_test.go index 9c9cf46e10d..6532c483b84 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_test.go @@ -2,6 +2,7 @@ package alerting import ( "encoding/json" + "maps" "net/http" "testing" "time" @@ -1195,3 +1196,69 @@ func TestIntegrationConvertPrometheusEndpoints_Delete(t *testing.T) { 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) +} diff --git a/public/api-merged.json b/public/api-merged.json index 0d448848112..b589f745464 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -18705,31 +18705,31 @@ "PrometheusRule": { "type": "object", "properties": { - "Alert": { + "alert": { "type": "string" }, - "Annotations": { + "annotations": { "type": "object", "additionalProperties": { "type": "string" } }, - "Expr": { + "expr": { "type": "string" }, - "For": { + "for": { "type": "string" }, - "KeepFiringFor": { + "keep_firing_for": { "type": "string" }, - "Labels": { + "labels": { "type": "object", "additionalProperties": { "type": "string" } }, - "Record": { + "record": { "type": "string" } } @@ -18737,26 +18737,26 @@ "PrometheusRuleGroup": { "type": "object", "properties": { - "Interval": { + "interval": { "$ref": "#/definitions/Duration" }, - "Labels": { + "labels": { "type": "object", "additionalProperties": { "type": "string" } }, - "Limit": { + "limit": { "type": "integer", "format": "int64" }, - "Name": { + "name": { "type": "string" }, - "QueryOffset": { + "query_offset": { "type": "string" }, - "Rules": { + "rules": { "type": "array", "items": { "$ref": "#/definitions/PrometheusRule" diff --git a/public/openapi3.json b/public/openapi3.json index 562042acc6d..c39e180b0bb 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -8766,31 +8766,31 @@ }, "PrometheusRule": { "properties": { - "Alert": { + "alert": { "type": "string" }, - "Annotations": { + "annotations": { "additionalProperties": { "type": "string" }, "type": "object" }, - "Expr": { + "expr": { "type": "string" }, - "For": { + "for": { "type": "string" }, - "KeepFiringFor": { + "keep_firing_for": { "type": "string" }, - "Labels": { + "labels": { "additionalProperties": { "type": "string" }, "type": "object" }, - "Record": { + "record": { "type": "string" } }, @@ -8798,26 +8798,26 @@ }, "PrometheusRuleGroup": { "properties": { - "Interval": { + "interval": { "$ref": "#/components/schemas/Duration" }, - "Labels": { + "labels": { "additionalProperties": { "type": "string" }, "type": "object" }, - "Limit": { + "limit": { "format": "int64", "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "QueryOffset": { + "query_offset": { "type": "string" }, - "Rules": { + "rules": { "items": { "$ref": "#/components/schemas/PrometheusRule" },