From cb7abbaa0f70888bc9c68f642e4cc78a91b90900 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Fri, 12 Sep 2025 18:15:55 -0400 Subject: [PATCH] Alerting: Rename expression elements of Rules APIs (#110914) This renames `data` to `expressions` for clarity in the rules apis. Also makes certain fields that are redundant optional in the case of pure expressions, so that users don't have to specify them when they are not needed (e.g. not datasource queries). --- .../rules/definitions/alerting-manifest.yaml | 42 ++- .../alertrule.rules.alerting.grafana.app.yaml | 29 +- ...ordingrule.rules.alerting.grafana.app.yaml | 13 +- .../rules/kinds/v0alpha1/alertRule_spec.cue | 14 +- .../rules/kinds/v0alpha1/rule_spec.cue | 23 +- .../alerting/v0alpha1/alertrule_spec_gen.go | 76 +++-- .../v0alpha1/recordingrule_spec_gen.go | 62 ++-- .../apis/alerting/v0alpha1/zz_openapi_gen.go | 322 +++++++++--------- .../rules/pkg/apis/alerting_manifest.go | 4 +- .../alertrule/v0alpha1/types.spec.gen.ts | 76 +++-- .../recordingrule/v0alpha1/types.spec.gen.ts | 60 ++-- .../apps/alerting/rules/alertrule/compat.go | 74 ++-- .../alerting/rules/recordingrule/compat.go | 110 +++--- .../rules/alertrule/alertrule_test.go | 52 +-- .../alerting/rules/compat/alertrule_test.go | 30 +- .../rules/compat/recordingrule_test.go | 22 +- .../rules/recordingrule/recordingrule_test.go | 52 +-- .../rules.alerting.grafana.app-v0alpha1.json | 40 +-- .../clients/rules/v0alpha1/endpoints.gen.ts | 26 +- 19 files changed, 610 insertions(+), 517 deletions(-) diff --git a/apps/alerting/rules/definitions/alerting-manifest.yaml b/apps/alerting/rules/definitions/alerting-manifest.yaml index f842608a627..2c7b9374a66 100644 --- a/apps/alerting/rules/definitions/alerting-manifest.yaml +++ b/apps/alerting/rules/definitions/alerting-manifest.yaml @@ -19,14 +19,24 @@ spec: additionalProperties: type: string type: object - data: + execErrState: + default: Error + enum: + - Error + - Ok + - Alerting + - KeepLast + type: string + expressions: additionalProperties: properties: datasourceUID: + description: The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource pattern: ^[a-zA-Z0-9_-]+$ type: string model: {} queryType: + description: The type of query if this is a query expression type: string relativeTimeRange: properties: @@ -41,21 +51,16 @@ spec: - to type: object source: + description: |- + Used to mark the expression to be used as the final source for the rule evaluation + Only one expression in a rule can be marked as the source + For AlertRules, this is the expression that will be evaluated against the alerting condition + For RecordingRules, this is the expression that will be recorded type: boolean required: - - queryType - - datasourceUID - model type: object type: object - execErrState: - default: Error - enum: - - Error - - Ok - - Alerting - - KeepLast - type: string for: allOf: - pattern: ^((([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?|0)$ @@ -151,10 +156,10 @@ spec: type: object required: - title - - data - trigger - noDataState - execErrState + - expressions type: object x-kubernetes-preserve-unknown-fields: true status: @@ -207,14 +212,16 @@ spec: schema: spec: properties: - data: + expressions: additionalProperties: properties: datasourceUID: + description: The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource pattern: ^[a-zA-Z0-9_-]+$ type: string model: {} queryType: + description: The type of query if this is a query expression type: string relativeTimeRange: properties: @@ -229,10 +236,13 @@ spec: - to type: object source: + description: |- + Used to mark the expression to be used as the final source for the rule evaluation + Only one expression in a rule can be marked as the source + For AlertRules, this is the expression that will be evaluated against the alerting condition + For RecordingRules, this is the expression that will be recorded type: boolean required: - - queryType - - datasourceUID - model type: object type: object @@ -263,9 +273,9 @@ spec: type: object required: - title - - data - trigger - metric + - expressions - targetDatasourceUID type: object x-kubernetes-preserve-unknown-fields: true diff --git a/apps/alerting/rules/definitions/alertrule.rules.alerting.grafana.app.yaml b/apps/alerting/rules/definitions/alertrule.rules.alerting.grafana.app.yaml index e7d9ab25aaa..e5fdfc57e5e 100644 --- a/apps/alerting/rules/definitions/alertrule.rules.alerting.grafana.app.yaml +++ b/apps/alerting/rules/definitions/alertrule.rules.alerting.grafana.app.yaml @@ -17,14 +17,24 @@ spec: additionalProperties: type: string type: object - data: + execErrState: + default: Error + enum: + - Error + - Ok + - Alerting + - KeepLast + type: string + expressions: additionalProperties: properties: datasourceUID: + description: The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource pattern: ^[a-zA-Z0-9_-]+$ type: string model: {} queryType: + description: The type of query if this is a query expression type: string relativeTimeRange: properties: @@ -39,21 +49,16 @@ spec: - to type: object source: + description: |- + Used to mark the expression to be used as the final source for the rule evaluation + Only one expression in a rule can be marked as the source + For AlertRules, this is the expression that will be evaluated against the alerting condition + For RecordingRules, this is the expression that will be recorded type: boolean required: - - queryType - - datasourceUID - model type: object type: object - execErrState: - default: Error - enum: - - Error - - Ok - - Alerting - - KeepLast - type: string for: allOf: - pattern: ^((([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?|0)$ @@ -149,10 +154,10 @@ spec: type: object required: - title - - data - trigger - noDataState - execErrState + - expressions type: object x-kubernetes-preserve-unknown-fields: true status: diff --git a/apps/alerting/rules/definitions/recordingrule.rules.alerting.grafana.app.yaml b/apps/alerting/rules/definitions/recordingrule.rules.alerting.grafana.app.yaml index b12bdf781f9..368bc5893c0 100644 --- a/apps/alerting/rules/definitions/recordingrule.rules.alerting.grafana.app.yaml +++ b/apps/alerting/rules/definitions/recordingrule.rules.alerting.grafana.app.yaml @@ -13,14 +13,16 @@ spec: properties: spec: properties: - data: + expressions: additionalProperties: properties: datasourceUID: + description: The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource pattern: ^[a-zA-Z0-9_-]+$ type: string model: {} queryType: + description: The type of query if this is a query expression type: string relativeTimeRange: properties: @@ -35,10 +37,13 @@ spec: - to type: object source: + description: |- + Used to mark the expression to be used as the final source for the rule evaluation + Only one expression in a rule can be marked as the source + For AlertRules, this is the expression that will be evaluated against the alerting condition + For RecordingRules, this is the expression that will be recorded type: boolean required: - - queryType - - datasourceUID - model type: object type: object @@ -69,9 +74,9 @@ spec: type: object required: - title - - data - trigger - metric + - expressions - targetDatasourceUID type: object x-kubernetes-preserve-unknown-fields: true diff --git a/apps/alerting/rules/kinds/v0alpha1/alertRule_spec.cue b/apps/alerting/rules/kinds/v0alpha1/alertRule_spec.cue index 0d074d28e6f..140005fe1d6 100644 --- a/apps/alerting/rules/kinds/v0alpha1/alertRule_spec.cue +++ b/apps/alerting/rules/kinds/v0alpha1/alertRule_spec.cue @@ -10,16 +10,16 @@ ExecErrState: *"Error" | "Ok" | "Alerting" | "KeepLast" // FIXME: the For and KeepFiringFor types should be using the AlertRulePromDuration type, but there seems to be an issue with the generator AlertRuleSpec: #RuleSpec & { - noDataState: NoDataState - execErrState: ExecErrState - "for"?: string & #PromDuration - keepFiringFor?: string & #PromDuration - missingSeriesEvalsToResolve?: int & >=0 - notificationSettings?: #NotificationSettings annotations?: { [string]: TemplateString } - panelRef?: #PanelRef + "for"?: string & #PromDuration + keepFiringFor?: string & #PromDuration + missingSeriesEvalsToResolve?: int & >=0 + noDataState: NoDataState + execErrState: ExecErrState + notificationSettings?: #NotificationSettings + panelRef?: #PanelRef } #PanelRef: { diff --git a/apps/alerting/rules/kinds/v0alpha1/rule_spec.cue b/apps/alerting/rules/kinds/v0alpha1/rule_spec.cue index 11c45d3b8fb..7b8cf55e734 100644 --- a/apps/alerting/rules/kinds/v0alpha1/rule_spec.cue +++ b/apps/alerting/rules/kinds/v0alpha1/rule_spec.cue @@ -12,12 +12,12 @@ TemplateString: string #RuleSpec: { title: string - data: #QueryMap paused?: bool trigger: #IntervalTrigger labels?: { [string]: TemplateString } + expressions: #ExpressionMap ... } @@ -34,15 +34,20 @@ TemplateString: string } // TODO: validate that only one can specify source=true -#QueryMap: { - [string]: #Query +#ExpressionMap: { + [string]: #Expression } // & struct.MinFields(1) This doesn't work in Cue ({ + interval: defaultPromDuration(), +}); + +export type PromDuration = string; + +export const defaultPromDuration = (): PromDuration => (""); + +export type TemplateString = string; + +export const defaultTemplateString = (): TemplateString => (""); + +// TODO(@moustafab): validate regex for time interval ref +export type TimeIntervalRef = string; + +export const defaultTimeIntervalRef = (): TimeIntervalRef => (""); + // TODO: validate that only one can specify source=true // & struct.MinFields(1) This doesn't work in Cue ; +export type ExpressionMap = Record; -export const defaultQueryMap = (): QueryMap => ({}); +export const defaultExpressionMap = (): ExpressionMap => ({}); -// TODO: come up with a better name for this. We have expression type things and data source queries -export interface Query { - // TODO: consider making this optional, with the nil value meaning "__expr__" (i.e. expression query) - queryType: string; +export interface Expression { + // The type of query if this is a query expression + queryType?: string; relativeTimeRange?: RelativeTimeRange; - datasourceUID: DatasourceUID; + // The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource + datasourceUID?: DatasourceUID; model: any; + // Used to mark the expression to be used as the final source for the rule evaluation + // Only one expression in a rule can be marked as the source + // For AlertRules, this is the expression that will be evaluated against the alerting condition + // For RecordingRules, this is the expression that will be recorded source?: boolean; } -export const defaultQuery = (): Query => ({ - queryType: "", - datasourceUID: defaultDatasourceUID(), +export const defaultExpression = (): Expression => ({ model: {}, }); @@ -40,37 +63,17 @@ export type DatasourceUID = string; export const defaultDatasourceUID = (): DatasourceUID => (""); -export interface IntervalTrigger { - interval: PromDuration; -} - -export const defaultIntervalTrigger = (): IntervalTrigger => ({ - interval: defaultPromDuration(), -}); - -export type PromDuration = string; - -export const defaultPromDuration = (): PromDuration => (""); - -// TODO(@moustafab): validate regex for time interval ref -export type TimeIntervalRef = string; - -export const defaultTimeIntervalRef = (): TimeIntervalRef => (""); - -export type TemplateString = string; - -export const defaultTemplateString = (): TemplateString => (""); - export interface Spec { title: string; - data: QueryMap; paused?: boolean; trigger: IntervalTrigger; - noDataState: string; - execErrState: string; + labels?: Record; + annotations?: Record; for?: string; keepFiringFor?: string; missingSeriesEvalsToResolve?: number; + noDataState: string; + execErrState: string; notificationSettings?: { receiver: string; groupBy?: string[]; @@ -80,8 +83,7 @@ export interface Spec { muteTimeIntervals?: TimeIntervalRef[]; activeTimeIntervals?: TimeIntervalRef[]; }; - annotations?: Record; - labels?: Record; + expressions: ExpressionMap; panelRef?: { dashboardUID: string; panelID: number; @@ -90,9 +92,9 @@ export interface Spec { export const defaultSpec = (): Spec => ({ title: "", - data: defaultQueryMap(), trigger: defaultIntervalTrigger(), noDataState: "NoData", execErrState: "Error", + expressions: defaultExpressionMap(), }); diff --git a/apps/alerting/rules/plugin/src/generated/recordingrule/v0alpha1/types.spec.gen.ts b/apps/alerting/rules/plugin/src/generated/recordingrule/v0alpha1/types.spec.gen.ts index f9944c8b4d7..fc8923debe6 100644 --- a/apps/alerting/rules/plugin/src/generated/recordingrule/v0alpha1/types.spec.gen.ts +++ b/apps/alerting/rules/plugin/src/generated/recordingrule/v0alpha1/types.spec.gen.ts @@ -1,24 +1,42 @@ // Code generated - EDITING IS FUTILE. DO NOT EDIT. +export interface IntervalTrigger { + interval: PromDuration; +} + +export const defaultIntervalTrigger = (): IntervalTrigger => ({ + interval: defaultPromDuration(), +}); + +export type PromDuration = string; + +export const defaultPromDuration = (): PromDuration => (""); + +export type TemplateString = string; + +export const defaultTemplateString = (): TemplateString => (""); + // TODO: validate that only one can specify source=true // & struct.MinFields(1) This doesn't work in Cue ; +export type ExpressionMap = Record; -export const defaultQueryMap = (): QueryMap => ({}); +export const defaultExpressionMap = (): ExpressionMap => ({}); -// TODO: come up with a better name for this. We have expression type things and data source queries -export interface Query { - // TODO: consider making this optional, with the nil value meaning "__expr__" (i.e. expression query) - queryType: string; +export interface Expression { + // The type of query if this is a query expression + queryType?: string; relativeTimeRange?: RelativeTimeRange; - datasourceUID: DatasourceUID; + // The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource + datasourceUID?: DatasourceUID; model: any; + // Used to mark the expression to be used as the final source for the rule evaluation + // Only one expression in a rule can be marked as the source + // For AlertRules, this is the expression that will be evaluated against the alerting condition + // For RecordingRules, this is the expression that will be recorded source?: boolean; } -export const defaultQuery = (): Query => ({ - queryType: "", - datasourceUID: defaultDatasourceUID(), +export const defaultExpression = (): Expression => ({ model: {}, }); @@ -40,37 +58,21 @@ export type DatasourceUID = string; export const defaultDatasourceUID = (): DatasourceUID => (""); -export interface IntervalTrigger { - interval: PromDuration; -} - -export const defaultIntervalTrigger = (): IntervalTrigger => ({ - interval: defaultPromDuration(), -}); - -export type PromDuration = string; - -export const defaultPromDuration = (): PromDuration => (""); - -export type TemplateString = string; - -export const defaultTemplateString = (): TemplateString => (""); - export interface Spec { title: string; - data: QueryMap; paused?: boolean; trigger: IntervalTrigger; - metric: string; labels?: Record; + metric: string; + expressions: ExpressionMap; targetDatasourceUID: string; } export const defaultSpec = (): Spec => ({ title: "", - data: defaultQueryMap(), trigger: defaultIntervalTrigger(), metric: "", + expressions: defaultExpressionMap(), targetDatasourceUID: "", }); diff --git a/pkg/registry/apps/alerting/rules/alertrule/compat.go b/pkg/registry/apps/alerting/rules/alertrule/compat.go index fbbb2be73d7..0ea9507c6e0 100644 --- a/pkg/registry/apps/alerting/rules/alertrule/compat.go +++ b/pkg/registry/apps/alerting/rules/alertrule/compat.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,8 +44,8 @@ func convertToK8sResource( Labels: make(map[string]string), }, Spec: model.AlertRuleSpec{ - Title: rule.Title, - Data: make(map[string]model.AlertRuleQuery), + Title: rule.Title, + Expressions: make(model.AlertRuleExpressionMap), Trigger: model.AlertRuleIntervalTrigger{ Interval: model.AlertRulePromDuration(interval.String()), }, @@ -90,19 +91,7 @@ func convertToK8sResource( } for _, query := range rule.Data { - k8sQuery := model.AlertRuleQuery{ - QueryType: query.QueryType, - Model: query.Model, - DatasourceUID: model.AlertRuleDatasourceUID(query.DatasourceUID), - Source: util.Pointer(rule.Condition == query.RefID), - } - if time.Duration(query.RelativeTimeRange.From) > 0 || time.Duration(query.RelativeTimeRange.To) > 0 { - k8sQuery.RelativeTimeRange = &model.AlertRuleRelativeTimeRange{ - From: model.AlertRulePromDurationWMillis(query.RelativeTimeRange.From.String()), - To: model.AlertRulePromDurationWMillis(query.RelativeTimeRange.To.String()), - } - } - k8sRule.Spec.Data[query.RefID] = k8sQuery + k8sRule.Spec.Expressions[query.RefID] = convertToK8sExpression(query, rule) } for _, setting := range rule.NotificationSettings { @@ -158,6 +147,29 @@ func convertToK8sResource( return k8sRule, nil } +func convertToK8sExpression(query ngmodels.AlertQuery, rule *ngmodels.AlertRule) model.AlertRuleExpression { + expression := model.AlertRuleExpression{ + Model: query.Model, + } + if query.QueryType != "" { + expression.QueryType = util.Pointer(query.QueryType) + } + // DatasourceUID is optional and defaults to expr datasource + if !expr.IsDataSource(query.DatasourceUID) { + expression.DatasourceUID = util.Pointer(model.AlertRuleDatasourceUID(query.DatasourceUID)) + } + if time.Duration(query.RelativeTimeRange.From) > 0 || time.Duration(query.RelativeTimeRange.To) > 0 { + expression.RelativeTimeRange = &model.AlertRuleRelativeTimeRange{ + From: model.AlertRulePromDurationWMillis(query.RelativeTimeRange.From.String()), + To: model.AlertRulePromDurationWMillis(query.RelativeTimeRange.To.String()), + } + } + if rule.Condition == query.RefID { + expression.Source = util.Pointer(true) + } + return expression +} + func convertToK8sResources( orgID int64, rules []*ngmodels.AlertRule, @@ -201,7 +213,7 @@ func convertToBaseDomainModel(orgID int64, k8sRule *model.AlertRule) (*ngmodels. UID: k8sRule.Name, Title: k8sRule.Spec.Title, NamespaceUID: k8sRule.Namespace, - Data: make([]ngmodels.AlertQuery, 0, len(k8sRule.Spec.Data)), + Data: make([]ngmodels.AlertQuery, 0, len(k8sRule.Spec.Expressions)), IsPaused: k8sRule.Spec.Paused != nil && *k8sRule.Spec.Paused, Labels: make(map[string]string), Annotations: make(map[string]string), @@ -267,13 +279,13 @@ func convertToBaseDomainModel(orgID int64, k8sRule *model.AlertRule) (*ngmodels. } domainRule.IntervalSeconds = int64(time.Duration(interval).Seconds()) - for refID, query := range k8sRule.Spec.Data { - domainQuery, err := convertToDomainQuery(query, refID) + for refID, expression := range k8sRule.Spec.Expressions { + domainQuery, err := convertToDomainQuery(expression, refID) if err != nil { return nil, err } domainRule.Data = append(domainRule.Data, domainQuery) - if query.Source != nil && *query.Source { + if expression.Source != nil && *expression.Source { if domainRule.Condition != "" { return nil, fmt.Errorf("multiple queries marked as source: %s and %s", domainRule.Condition, refID) } @@ -339,23 +351,29 @@ func convertNotificationSettings(sourceSettings *model.AlertRuleV0alpha1SpecNoti return settings, nil } -func convertToDomainQuery(query model.AlertRuleQuery, refID string) (ngmodels.AlertQuery, error) { - modelJson, err := json.Marshal(query.Model) +func convertToDomainQuery(expression model.AlertRuleExpression, refID string) (ngmodels.AlertQuery, error) { + modelJson, err := json.Marshal(expression.Model) if err != nil { return ngmodels.AlertQuery{}, fmt.Errorf("failed to marshal model: %w", err) } domainQuery := ngmodels.AlertQuery{ - RefID: refID, - QueryType: query.QueryType, - DatasourceUID: string(query.DatasourceUID), - Model: modelJson, + RefID: refID, + Model: modelJson, } - if query.RelativeTimeRange != nil { - from, err := prom_model.ParseDuration(string(query.RelativeTimeRange.From)) + if expression.QueryType != nil { + domainQuery.QueryType = *expression.QueryType + } + if expression.DatasourceUID != nil { + domainQuery.DatasourceUID = string(*expression.DatasourceUID) + } else { + domainQuery.DatasourceUID = expr.DatasourceUID + } + if expression.RelativeTimeRange != nil { + from, err := prom_model.ParseDuration(string(expression.RelativeTimeRange.From)) if err != nil { return ngmodels.AlertQuery{}, fmt.Errorf("failed to parse duration: %w", err) } - to, err := prom_model.ParseDuration(string(query.RelativeTimeRange.To)) + to, err := prom_model.ParseDuration(string(expression.RelativeTimeRange.To)) if err != nil { return ngmodels.AlertQuery{}, fmt.Errorf("failed to parse duration: %w", err) } diff --git a/pkg/registry/apps/alerting/rules/recordingrule/compat.go b/pkg/registry/apps/alerting/rules/recordingrule/compat.go index f4479a41d48..e9ef0c4e295 100644 --- a/pkg/registry/apps/alerting/rules/recordingrule/compat.go +++ b/pkg/registry/apps/alerting/rules/recordingrule/compat.go @@ -9,6 +9,7 @@ import ( model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -42,8 +43,8 @@ func convertToK8sResource( Labels: make(map[string]string), }, Spec: model.RecordingRuleSpec{ - Title: rule.Title, - Data: make(map[string]model.RecordingRuleQuery), + Title: rule.Title, + Expressions: make(model.RecordingRuleExpressionMap), Trigger: model.RecordingRuleIntervalTrigger{ Interval: model.RecordingRulePromDuration(interval.String()), }, @@ -67,21 +68,7 @@ func convertToK8sResource( } for _, query := range rule.Data { - k8sQuery := model.RecordingRuleQuery{ - QueryType: query.QueryType, - Model: query.Model, - DatasourceUID: model.RecordingRuleDatasourceUID(query.DatasourceUID), - } - if time.Duration(query.RelativeTimeRange.From) > 0 || time.Duration(query.RelativeTimeRange.To) > 0 { - k8sQuery.RelativeTimeRange = &model.RecordingRuleRelativeTimeRange{ - From: model.RecordingRulePromDurationWMillis(query.RelativeTimeRange.From.String()), - To: model.RecordingRulePromDurationWMillis(query.RelativeTimeRange.To.String()), - } - } - if rule.Record != nil && rule.Record.From == query.RefID { - k8sQuery.Source = util.Pointer(true) - } - k8sRule.Spec.Data[query.RefID] = k8sQuery + k8sRule.Spec.Expressions[query.RefID] = convertToK8sExpression(query, rule) } meta, err := utils.MetaAccessor(k8sRule) @@ -108,6 +95,29 @@ func convertToK8sResource( return k8sRule, nil } +func convertToK8sExpression(query ngmodels.AlertQuery, rule *ngmodels.AlertRule) model.RecordingRuleExpression { + expression := model.RecordingRuleExpression{ + Model: query.Model, + } + if query.QueryType != "" { + expression.QueryType = util.Pointer(query.QueryType) + } + // DatasourceUID is optional and defaults to expr datasource + if !expr.IsDataSource(query.DatasourceUID) { + expression.DatasourceUID = util.Pointer(model.RecordingRuleDatasourceUID(query.DatasourceUID)) + } + if time.Duration(query.RelativeTimeRange.From) > 0 || time.Duration(query.RelativeTimeRange.To) > 0 { + expression.RelativeTimeRange = &model.RecordingRuleRelativeTimeRange{ + From: model.RecordingRulePromDurationWMillis(query.RelativeTimeRange.From.String()), + To: model.RecordingRulePromDurationWMillis(query.RelativeTimeRange.To.String()), + } + } + if rule.Record != nil && rule.Record.From == query.RefID { + expression.Source = util.Pointer(true) + } + return expression +} + func convertToK8sResources( orgID int64, rules []*ngmodels.AlertRule, @@ -150,7 +160,7 @@ func convertToBaseDomainModel(orgID int64, k8sRule *model.RecordingRule) (*ngmod OrgID: orgID, UID: k8sRule.Name, Title: k8sRule.Spec.Title, - Data: make([]ngmodels.AlertQuery, 0, len(k8sRule.Spec.Data)), + Data: make([]ngmodels.AlertQuery, 0, len(k8sRule.Spec.Expressions)), IsPaused: k8sRule.Spec.Paused != nil && *k8sRule.Spec.Paused, Labels: make(map[string]string), @@ -187,35 +197,13 @@ func convertToBaseDomainModel(orgID int64, k8sRule *model.RecordingRule) (*ngmod for k, v := range k8sRule.Spec.Labels { domainRule.Labels[k] = string(v) } - for refID, query := range k8sRule.Spec.Data { - modelJson, err := json.Marshal(query.Model) + for refID, expression := range k8sRule.Spec.Expressions { + domainQuery, err := convertToDomainQuery(expression, refID) if err != nil { - return nil, fmt.Errorf("failed to marshal model: %w", err) + return nil, err } - domainQuery := ngmodels.AlertQuery{ - RefID: refID, - QueryType: query.QueryType, - DatasourceUID: string(query.DatasourceUID), - Model: modelJson, - } - if query.RelativeTimeRange != nil { - from, err := prom_model.ParseDuration(string(query.RelativeTimeRange.From)) - if err != nil { - return nil, fmt.Errorf("failed to parse duration: %w", err) - } - to, err := prom_model.ParseDuration(string(query.RelativeTimeRange.To)) - if err != nil { - return nil, fmt.Errorf("failed to parse duration: %w", err) - } - domainQuery.RelativeTimeRange = ngmodels.RelativeTimeRange{ - From: ngmodels.Duration(from), - To: ngmodels.Duration(to), - } - } - domainRule.Data = append(domainRule.Data, domainQuery) - - if query.Source != nil && *query.Source { + if expression.Source != nil && *expression.Source { if domainRule.Record.From != "" { return nil, fmt.Errorf("multiple queries marked as source: %s and %s", domainRule.Record.From, refID) } @@ -227,3 +215,37 @@ func convertToBaseDomainModel(orgID int64, k8sRule *model.RecordingRule) (*ngmod } return domainRule, nil } + +func convertToDomainQuery(expression model.RecordingRuleExpression, refID string) (ngmodels.AlertQuery, error) { + modelJson, err := json.Marshal(expression.Model) + if err != nil { + return ngmodels.AlertQuery{}, fmt.Errorf("failed to marshal model: %w", err) + } + domainQuery := ngmodels.AlertQuery{ + RefID: refID, + Model: modelJson, + } + if expression.QueryType != nil { + domainQuery.QueryType = *expression.QueryType + } + if expression.DatasourceUID != nil { + domainQuery.DatasourceUID = string(*expression.DatasourceUID) + } else { + domainQuery.DatasourceUID = expr.DatasourceUID + } + if expression.RelativeTimeRange != nil { + from, err := prom_model.ParseDuration(string(expression.RelativeTimeRange.From)) + if err != nil { + return ngmodels.AlertQuery{}, fmt.Errorf("failed to parse duration: %w", err) + } + to, err := prom_model.ParseDuration(string(expression.RelativeTimeRange.To)) + if err != nil { + return ngmodels.AlertQuery{}, fmt.Errorf("failed to parse duration: %w", err) + } + domainQuery.RelativeTimeRange = ngmodels.RelativeTimeRange{ + From: ngmodels.Duration(from), + To: ngmodels.Duration(to), + } + } + return domainQuery, nil +} diff --git a/pkg/tests/apis/alerting/rules/alertrule/alertrule_test.go b/pkg/tests/apis/alerting/rules/alertrule/alertrule_test.go index 9daedc259ae..5d4a8b67497 100644 --- a/pkg/tests/apis/alerting/rules/alertrule/alertrule_test.go +++ b/pkg/tests/apis/alerting/rules/alertrule/alertrule_test.go @@ -54,10 +54,10 @@ func TestIntegrationResourceIdentifier(t *testing.T) { }, Spec: v0alpha1.AlertRuleSpec{ Title: rule.Title, - Data: map[string]v0alpha1.AlertRuleQuery{ + Expressions: v0alpha1.AlertRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer("query"), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ @@ -154,10 +154,10 @@ func TestIntegrationAccessControl(t *testing.T) { }, Spec: v0alpha1.AlertRuleSpec{ Title: rule.Title, - Data: map[string]v0alpha1.AlertRuleQuery{ + Expressions: v0alpha1.AlertRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ @@ -241,10 +241,10 @@ func TestIntegrationCRUD(t *testing.T) { }, Spec: v0alpha1.AlertRuleSpec{ Title: rule.Title, - Data: map[string]v0alpha1.AlertRuleQuery{ + Expressions: v0alpha1.AlertRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ @@ -295,10 +295,10 @@ func TestIntegrationCRUD(t *testing.T) { }, Spec: v0alpha1.AlertRuleSpec{ Title: rule.Title, - Data: map[string]v0alpha1.AlertRuleQuery{ + Expressions: v0alpha1.AlertRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ @@ -328,8 +328,8 @@ func TestIntegrationCRUD(t *testing.T) { }, }, Spec: v0alpha1.AlertRuleSpec{ - Title: "invalid-rule", - Data: map[string]v0alpha1.AlertRuleQuery{}, // Empty data should fail + Title: "invalid-rule", + Expressions: v0alpha1.AlertRuleExpressionMap{}, // Empty data should fail Trigger: v0alpha1.AlertRuleIntervalTrigger{ Interval: "30", }, @@ -356,10 +356,10 @@ func TestIntegrationCRUD(t *testing.T) { }, Spec: v0alpha1.AlertRuleSpec{ Title: rule.Title, - Data: map[string]v0alpha1.AlertRuleQuery{ + Expressions: v0alpha1.AlertRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ @@ -408,10 +408,10 @@ func TestIntegrationCRUD(t *testing.T) { }, Spec: v0alpha1.AlertRuleSpec{ Title: rule.Title, - Data: map[string]v0alpha1.AlertRuleQuery{ + Expressions: v0alpha1.AlertRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ From: v0alpha1.AlertRulePromDurationWMillis("5m"), @@ -445,10 +445,10 @@ func TestIntegrationCRUD(t *testing.T) { }, Spec: v0alpha1.AlertRuleSpec{ Title: rule.Title, - Data: map[string]v0alpha1.AlertRuleQuery{ + Expressions: v0alpha1.AlertRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ @@ -501,10 +501,10 @@ func TestIntegrationPatch(t *testing.T) { }, Spec: v0alpha1.AlertRuleSpec{ Title: rule.Title, - Data: map[string]v0alpha1.AlertRuleQuery{ + Expressions: v0alpha1.AlertRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ diff --git a/pkg/tests/apis/alerting/rules/compat/alertrule_test.go b/pkg/tests/apis/alerting/rules/compat/alertrule_test.go index d5339bdf7c7..4eb2cc39b52 100644 --- a/pkg/tests/apis/alerting/rules/compat/alertrule_test.go +++ b/pkg/tests/apis/alerting/rules/compat/alertrule_test.go @@ -57,10 +57,10 @@ func TestIntegrationAlertRuleCompatCreateViaK8s(t *testing.T) { }, Spec: v0alpha1.AlertRuleSpec{ Title: rule.Title, - Data: map[string]v0alpha1.AlertRuleQuery{ + Expressions: v0alpha1.AlertRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ @@ -91,9 +91,9 @@ func TestIntegrationAlertRuleCompatCreateViaK8s(t *testing.T) { err := json.Unmarshal(retrievedRule.Data[0].Model, &model) require.NoError(t, err) require.NotNil(t, model) - expectedModel, ok := created.Spec.Data["A"].Model.(map[string]interface{}) + expectedModel, ok := created.Spec.Expressions["A"].Model.(map[string]interface{}) if !ok { - t.Fatalf("Expected model to be a map[string]interface{}, got %T", created.Spec.Data["A"].Model) + t.Fatalf("Expected model to be a map[string]interface{}, got %T", created.Spec.Expressions["A"].Model) } for k, v := range expectedModel { require.EqualValues(t, v, model[k], "Model field %s should match", k) @@ -230,13 +230,13 @@ func TestIntegrationAlertRuleCompatCreateViaProvisioning(t *testing.T) { require.NoError(t, err) require.NotNil(t, retrievedRule) require.Equal(t, r.Title, retrievedRule.Spec.Title) - require.NotNil(t, retrievedRule.Spec.Data[r.Data[0].RefID].Source) - require.True(t, *retrievedRule.Spec.Data[r.Data[0].RefID].Source) + require.NotNil(t, retrievedRule.Spec.Expressions[r.Data[0].RefID].Source) + require.True(t, *retrievedRule.Spec.Expressions[r.Data[0].RefID].Source) require.Equal(t, r.FolderUID, retrievedRule.Annotations["grafana.app/folder"]) require.Equal(t, created.Title, retrievedRule.Labels[v0alpha1.GroupLabelKey]) require.Equal(t, fmt.Sprintf("%d", i), retrievedRule.Labels[v0alpha1.GroupIndexLabelKey]) require.Equal(t, ngmodels.ProvenanceAPI, ngmodels.Provenance(retrievedRule.GetProvenanceStatus())) - require.EqualValues(t, r.Data[0].DatasourceUID, retrievedRule.Spec.Data["A"].DatasourceUID) + require.EqualValues(t, r.Data[0].DatasourceUID, *retrievedRule.Spec.Expressions["A"].DatasourceUID) expectedDuration, err := prom_model.ParseDuration(fmt.Sprintf("%ds", created.Interval)) require.NoError(t, err) require.Equal(t, expectedDuration.String(), string(retrievedRule.Spec.Trigger.Interval)) @@ -244,9 +244,9 @@ func TestIntegrationAlertRuleCompatCreateViaProvisioning(t *testing.T) { err = json.Unmarshal(r.Data[0].Model, &expectedModel) require.NoError(t, err) require.NotNil(t, expectedModel) - retrievedModel, ok := retrievedRule.Spec.Data["A"].Model.(map[string]interface{}) + retrievedModel, ok := retrievedRule.Spec.Expressions["A"].Model.(map[string]interface{}) if !ok { - t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Data["A"].Model) + t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Expressions["A"].Model) } for k, v := range expectedModel { require.EqualValues(t, v, retrievedModel[k], "Model field %s should match", k) @@ -372,13 +372,13 @@ func TestIntegrationAlertRuleCompatCreateViaProvisioningChangeGroupInK8s(t *test require.NoError(t, err) require.NotNil(t, retrievedRule) require.Equal(t, r.Title, retrievedRule.Spec.Title) - require.NotNil(t, retrievedRule.Spec.Data[r.Data[0].RefID].Source) - require.True(t, *retrievedRule.Spec.Data[r.Data[0].RefID].Source) + require.NotNil(t, retrievedRule.Spec.Expressions[r.Data[0].RefID].Source) + require.True(t, *retrievedRule.Spec.Expressions[r.Data[0].RefID].Source) require.Equal(t, r.FolderUID, retrievedRule.Annotations["grafana.app/folder"]) require.Equal(t, created.Title, retrievedRule.Labels[v0alpha1.GroupLabelKey]) require.Equal(t, fmt.Sprintf("%d", i), retrievedRule.Labels[v0alpha1.GroupIndexLabelKey]) require.Equal(t, ngmodels.ProvenanceAPI, ngmodels.Provenance(retrievedRule.GetProvenanceStatus())) - require.EqualValues(t, r.Data[0].DatasourceUID, retrievedRule.Spec.Data["X"].DatasourceUID) + require.EqualValues(t, r.Data[0].DatasourceUID, *retrievedRule.Spec.Expressions["X"].DatasourceUID) expectedDuration, err := prom_model.ParseDuration(fmt.Sprintf("%ds", created.Interval)) require.NoError(t, err) require.Equal(t, expectedDuration.String(), string(retrievedRule.Spec.Trigger.Interval)) @@ -386,9 +386,9 @@ func TestIntegrationAlertRuleCompatCreateViaProvisioningChangeGroupInK8s(t *test err = json.Unmarshal(r.Data[0].Model, &expectedModel) require.NoError(t, err) require.NotNil(t, expectedModel) - retrievedModel, ok := retrievedRule.Spec.Data["X"].Model.(map[string]interface{}) + retrievedModel, ok := retrievedRule.Spec.Expressions["X"].Model.(map[string]interface{}) if !ok { - t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Data["X"].Model) + t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Expressions["X"].Model) } for k, v := range expectedModel { require.EqualValues(t, v, retrievedModel[k], "Model field %s should match", k) diff --git a/pkg/tests/apis/alerting/rules/compat/recordingrule_test.go b/pkg/tests/apis/alerting/rules/compat/recordingrule_test.go index a254901c72f..25f4dbf243b 100644 --- a/pkg/tests/apis/alerting/rules/compat/recordingrule_test.go +++ b/pkg/tests/apis/alerting/rules/compat/recordingrule_test.go @@ -64,10 +64,10 @@ func TestIntegrationRecordingRuleCompatCreateViaK8s(t *testing.T) { Spec: v0alpha1.RecordingRuleSpec{ Title: rule.Title, Metric: rule.Record.Metric, - Data: map[string]v0alpha1.RecordingRuleQuery{ + Expressions: v0alpha1.RecordingRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ @@ -98,9 +98,9 @@ func TestIntegrationRecordingRuleCompatCreateViaK8s(t *testing.T) { err := json.Unmarshal(retrievedRule.Data[0].Model, &model) require.NoError(t, err) require.NotNil(t, model) - expectedModel, ok := created.Spec.Data["A"].Model.(map[string]interface{}) + expectedModel, ok := created.Spec.Expressions["A"].Model.(map[string]interface{}) if !ok { - t.Fatalf("Expected model to be a map[string]interface{}, got %T", created.Spec.Data["A"].Model) + t.Fatalf("Expected model to be a map[string]interface{}, got %T", created.Spec.Expressions["A"].Model) } for k, v := range expectedModel { require.EqualValues(t, v, model[k], "Model field %s should match", k) @@ -247,7 +247,7 @@ func TestIntegrationRecordingRuleCompatCreateViaProvisioning(t *testing.T) { require.Equal(t, created.Title, retrievedRule.Labels[v0alpha1.GroupLabelKey]) require.Equal(t, fmt.Sprintf("%d", i), retrievedRule.Labels[v0alpha1.GroupIndexLabelKey]) require.Equal(t, ngmodels.ProvenanceAPI, ngmodels.Provenance(retrievedRule.GetProvenanceStatus())) - require.EqualValues(t, r.Data[0].DatasourceUID, retrievedRule.Spec.Data["A"].DatasourceUID) + require.EqualValues(t, r.Data[0].DatasourceUID, *retrievedRule.Spec.Expressions["A"].DatasourceUID) expectedDuration, err := prom_model.ParseDuration(fmt.Sprintf("%ds", created.Interval)) require.NoError(t, err) require.Equal(t, expectedDuration.String(), string(retrievedRule.Spec.Trigger.Interval)) @@ -255,9 +255,9 @@ func TestIntegrationRecordingRuleCompatCreateViaProvisioning(t *testing.T) { err = json.Unmarshal(r.Data[0].Model, &expectedModel) require.NoError(t, err) require.NotNil(t, expectedModel) - retrievedModel, ok := retrievedRule.Spec.Data["A"].Model.(map[string]interface{}) + retrievedModel, ok := retrievedRule.Spec.Expressions["A"].Model.(map[string]interface{}) if !ok { - t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Data["A"].Model) + t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Expressions["A"].Model) } for k, v := range expectedModel { require.EqualValues(t, v, retrievedModel[k], "Model field %s should match", k) @@ -391,7 +391,7 @@ func TestIntegrationRecordingRuleCompatCreateViaProvisioningChangeGroupInK8s(t * require.Equal(t, created.Title, retrievedRule.Labels[v0alpha1.GroupLabelKey]) require.Equal(t, fmt.Sprintf("%d", i), retrievedRule.Labels[v0alpha1.GroupIndexLabelKey]) require.Equal(t, ngmodels.ProvenanceAPI, ngmodels.Provenance(retrievedRule.GetProvenanceStatus())) - require.EqualValues(t, r.Data[0].DatasourceUID, retrievedRule.Spec.Data["X"].DatasourceUID) + require.EqualValues(t, r.Data[0].DatasourceUID, *retrievedRule.Spec.Expressions["X"].DatasourceUID) expectedDuration, err := prom_model.ParseDuration(fmt.Sprintf("%ds", created.Interval)) require.NoError(t, err) require.Equal(t, expectedDuration.String(), string(retrievedRule.Spec.Trigger.Interval)) @@ -399,9 +399,9 @@ func TestIntegrationRecordingRuleCompatCreateViaProvisioningChangeGroupInK8s(t * err = json.Unmarshal(r.Data[0].Model, &expectedModel) require.NoError(t, err) require.NotNil(t, expectedModel) - retrievedModel, ok := retrievedRule.Spec.Data["X"].Model.(map[string]interface{}) + retrievedModel, ok := retrievedRule.Spec.Expressions["X"].Model.(map[string]interface{}) if !ok { - t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Data["X"].Model) + t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Expressions["X"].Model) } for k, v := range expectedModel { require.EqualValues(t, v, retrievedModel[k], "Model field %s should match", k) diff --git a/pkg/tests/apis/alerting/rules/recordingrule/recordingrule_test.go b/pkg/tests/apis/alerting/rules/recordingrule/recordingrule_test.go index df706099124..1f024e9fb7f 100644 --- a/pkg/tests/apis/alerting/rules/recordingrule/recordingrule_test.go +++ b/pkg/tests/apis/alerting/rules/recordingrule/recordingrule_test.go @@ -55,10 +55,10 @@ func TestIntegrationResourceIdentifier(t *testing.T) { Spec: v0alpha1.RecordingRuleSpec{ Title: rule.Title, Metric: rule.Record.Metric, - Data: map[string]v0alpha1.RecordingRuleQuery{ + Expressions: v0alpha1.RecordingRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ @@ -155,10 +155,10 @@ func TestIntegrationAccessControl(t *testing.T) { Spec: v0alpha1.RecordingRuleSpec{ Title: rule.Title, Metric: rule.Record.Metric, - Data: map[string]v0alpha1.RecordingRuleQuery{ + Expressions: v0alpha1.RecordingRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ @@ -242,10 +242,10 @@ func TestIntegrationCRUD(t *testing.T) { Spec: v0alpha1.RecordingRuleSpec{ Title: rule.Title, Metric: rule.Record.Metric, - Data: map[string]v0alpha1.RecordingRuleQuery{ + Expressions: v0alpha1.RecordingRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ @@ -294,10 +294,10 @@ func TestIntegrationCRUD(t *testing.T) { Spec: v0alpha1.RecordingRuleSpec{ Title: rule.Title, Metric: rule.Record.Metric, - Data: map[string]v0alpha1.RecordingRuleQuery{ + Expressions: v0alpha1.RecordingRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ @@ -325,8 +325,8 @@ func TestIntegrationCRUD(t *testing.T) { }, }, Spec: v0alpha1.RecordingRuleSpec{ - Title: "invalid-recording-rule", - Data: map[string]v0alpha1.RecordingRuleQuery{}, // Empty data should fail + Title: "invalid-recording-rule", + Expressions: v0alpha1.RecordingRuleExpressionMap{}, // Empty data should fail Trigger: v0alpha1.RecordingRuleIntervalTrigger{ Interval: "30s", }, @@ -352,10 +352,10 @@ func TestIntegrationCRUD(t *testing.T) { Spec: v0alpha1.RecordingRuleSpec{ Title: rule.Title, Metric: rule.Record.Metric, - Data: map[string]v0alpha1.RecordingRuleQuery{ + Expressions: v0alpha1.RecordingRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ @@ -404,10 +404,10 @@ func TestIntegrationCRUD(t *testing.T) { Spec: v0alpha1.RecordingRuleSpec{ Title: rule.Title, Metric: rule.Record.Metric, - Data: map[string]v0alpha1.RecordingRuleQuery{ + Expressions: v0alpha1.RecordingRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ From: v0alpha1.RecordingRulePromDurationWMillis("5m"), @@ -440,10 +440,10 @@ func TestIntegrationCRUD(t *testing.T) { Spec: v0alpha1.RecordingRuleSpec{ Title: rule.Title, Metric: rule.Record.Metric, - Data: map[string]v0alpha1.RecordingRuleQuery{ + Expressions: v0alpha1.RecordingRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ @@ -496,10 +496,10 @@ func TestIntegrationPatch(t *testing.T) { Spec: v0alpha1.RecordingRuleSpec{ Title: rule.Title, Metric: rule.Record.Metric, - Data: map[string]v0alpha1.RecordingRuleQuery{ + Expressions: v0alpha1.RecordingRuleExpressionMap{ "A": { - QueryType: "query", - DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID), + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), Model: rule.Data[0].Model, Source: util.Pointer(true), RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ diff --git a/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json index d26d36d1225..afd80701b8c 100644 --- a/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json @@ -2369,10 +2369,10 @@ "type": "object", "required": [ "title", - "data", "trigger", "noDataState", - "execErrState" + "execErrState", + "expressions" ], "properties": { "annotations": { @@ -2381,22 +2381,32 @@ "type": "string" } }, - "data": { + "execErrState": { + "type": "string", + "default": "Error", + "enum": [ + "Error", + "Ok", + "Alerting", + "KeepLast" + ] + }, + "expressions": { "type": "object", "additionalProperties": { "type": "object", "required": [ - "queryType", - "datasourceUID", "model" ], "properties": { "datasourceUID": { + "description": "The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource", "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" }, "model": {}, "queryType": { + "description": "The type of query if this is a query expression", "type": "string" }, "relativeTimeRange": { @@ -2417,21 +2427,12 @@ } }, "source": { + "description": "Used to mark the expression to be used as the final source for the rule evaluation\nOnly one expression in a rule can be marked as the source\nFor AlertRules, this is the expression that will be evaluated against the alerting condition\nFor RecordingRules, this is the expression that will be recorded", "type": "boolean" } } } }, - "execErrState": { - "type": "string", - "default": "Error", - "enum": [ - "Error", - "Ok", - "Alerting", - "KeepLast" - ] - }, "for": { "type": "string", "allOf": [ @@ -2730,28 +2731,28 @@ "type": "object", "required": [ "title", - "data", "trigger", "metric", + "expressions", "targetDatasourceUID" ], "properties": { - "data": { + "expressions": { "type": "object", "additionalProperties": { "type": "object", "required": [ - "queryType", - "datasourceUID", "model" ], "properties": { "datasourceUID": { + "description": "The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource", "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" }, "model": {}, "queryType": { + "description": "The type of query if this is a query expression", "type": "string" }, "relativeTimeRange": { @@ -2772,6 +2773,7 @@ } }, "source": { + "description": "Used to mark the expression to be used as the final source for the rule evaluation\nOnly one expression in a rule can be marked as the source\nFor AlertRules, this is the expression that will be evaluated against the alerting condition\nFor RecordingRules, this is the expression that will be recorded", "type": "boolean" } } diff --git a/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts b/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts index 01247d97dd1..604940505db 100644 --- a/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts @@ -846,19 +846,25 @@ export type AlertRuleSpec = { annotations?: { [key: string]: string; }; - data: { + execErrState: 'Error' | 'Ok' | 'Alerting' | 'KeepLast'; + expressions: { [key: string]: { - datasourceUID: string; + /** The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource */ + datasourceUID?: string; model: any; - queryType: string; + /** The type of query if this is a query expression */ + queryType?: string; relativeTimeRange?: { from: string; to: string; }; + /** Used to mark the expression to be used as the final source for the rule evaluation + Only one expression in a rule can be marked as the source + For AlertRules, this is the expression that will be evaluated against the alerting condition + For RecordingRules, this is the expression that will be recorded */ source?: boolean; }; }; - execErrState: 'Error' | 'Ok' | 'Alerting' | 'KeepLast'; for?: any & any; keepFiringFor?: any & any; labels?: { @@ -982,15 +988,21 @@ export type Status = { }; export type Patch = object; export type RecordingRuleSpec = { - data: { + expressions: { [key: string]: { - datasourceUID: string; + /** The UID of the datasource to run this expression against. If omitted, the expression will be run against the `__expr__` datasource */ + datasourceUID?: string; model: any; - queryType: string; + /** The type of query if this is a query expression */ + queryType?: string; relativeTimeRange?: { from: string; to: string; }; + /** Used to mark the expression to be used as the final source for the rule evaluation + Only one expression in a rule can be marked as the source + For AlertRules, this is the expression that will be evaluated against the alerting condition + For RecordingRules, this is the expression that will be recorded */ source?: boolean; }; };