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:
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user