Alerting: Support query_offset in the Prometheus conversion (#102499)

Adds support for rule group-level query_offset in Prometheus to Grafana rule conversion. It allows specifying a time offset for rule evaluation, which gets applied and saved during the conversion.
This commit is contained in:
Alexander Akhmetov
2025-03-20 18:05:55 +01:00
committed by GitHub
parent 0845c781ae
commit ad71270ee0
11 changed files with 160 additions and 28 deletions
+24 -1
View File
@@ -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"
@@ -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
@@ -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"
@@ -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": {
+11 -3
View File
@@ -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
}
+14 -11
View File
@@ -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)
+4 -4
View File
@@ -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
+4 -3
View File
@@ -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",
+1 -1
View File
@@ -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
}
+24 -1
View File
@@ -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",
+24 -1
View File
@@ -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"
},