Alerting: Add rule conversion package (#100224)
This commit is contained in:
@@ -295,7 +295,8 @@ type AlertRule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AlertRuleMetadata struct {
|
type AlertRuleMetadata struct {
|
||||||
EditorSettings EditorSettings `json:"editor_settings"`
|
EditorSettings EditorSettings `json:"editor_settings"`
|
||||||
|
PrometheusStyleRule *PrometheusStyleRule `json:"prometheus_style_rule,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditorSettings struct {
|
type EditorSettings struct {
|
||||||
@@ -303,6 +304,10 @@ type EditorSettings struct {
|
|||||||
SimplifiedNotificationsSection bool `json:"simplified_notifications_section"`
|
SimplifiedNotificationsSection bool `json:"simplified_notifications_section"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PrometheusStyleRule struct {
|
||||||
|
OriginalRuleDefinition string `json:"original_rule_definition,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Namespaced describes a class of resources that are stored in a specific namespace.
|
// Namespaced describes a class of resources that are stored in a specific namespace.
|
||||||
type Namespaced interface {
|
type Namespaced interface {
|
||||||
GetNamespaceUID() string
|
GetNamespaceUID() string
|
||||||
@@ -748,6 +753,11 @@ func (alertRule *AlertRule) Copy() *AlertRule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if alertRule.Metadata.PrometheusStyleRule != nil {
|
||||||
|
prometheusStyleRule := *alertRule.Metadata.PrometheusStyleRule
|
||||||
|
result.Metadata.PrometheusStyleRule = &prometheusStyleRule
|
||||||
|
}
|
||||||
|
|
||||||
for _, s := range alertRule.NotificationSettings {
|
for _, s := range alertRule.NotificationSettings {
|
||||||
result.NotificationSettings = append(result.NotificationSettings, CopyNotificationSettings(s))
|
result.NotificationSettings = append(result.NotificationSettings, CopyNotificationSettings(s))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -841,7 +841,7 @@ func TestDiff(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should detect changes in Metadata", func(t *testing.T) {
|
t.Run("should detect changes in Metadata.EditorSettings", func(t *testing.T) {
|
||||||
rule1 := RuleGen.With(RuleGen.WithMetadata(AlertRuleMetadata{EditorSettings: EditorSettings{
|
rule1 := RuleGen.With(RuleGen.WithMetadata(AlertRuleMetadata{EditorSettings: EditorSettings{
|
||||||
SimplifiedQueryAndExpressionsSection: false,
|
SimplifiedQueryAndExpressionsSection: false,
|
||||||
SimplifiedNotificationsSection: false,
|
SimplifiedNotificationsSection: false,
|
||||||
@@ -858,6 +858,21 @@ func TestDiff(t *testing.T) {
|
|||||||
"Metadata.EditorSettings.SimplifiedNotificationsSection",
|
"Metadata.EditorSettings.SimplifiedNotificationsSection",
|
||||||
}, diff.Paths())
|
}, diff.Paths())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should detect changes in Metadata.PrometheusStyleRule", func(t *testing.T) {
|
||||||
|
rule1 := RuleGen.With(RuleGen.WithMetadata(AlertRuleMetadata{PrometheusStyleRule: &PrometheusStyleRule{
|
||||||
|
OriginalRuleDefinition: "data",
|
||||||
|
}})).GenerateRef()
|
||||||
|
|
||||||
|
rule2 := CopyRule(rule1, RuleGen.WithMetadata(AlertRuleMetadata{PrometheusStyleRule: &PrometheusStyleRule{
|
||||||
|
OriginalRuleDefinition: "updated data",
|
||||||
|
}}))
|
||||||
|
|
||||||
|
diff := rule1.Diff(rule2)
|
||||||
|
assert.ElementsMatch(t, []string{
|
||||||
|
"Metadata.PrometheusStyleRule.OriginalRuleDefinition",
|
||||||
|
}, diff.Paths())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSortByGroupIndex(t *testing.T) {
|
func TestSortByGroupIndex(t *testing.T) {
|
||||||
@@ -940,11 +955,21 @@ func TestAlertRuleGetKeyWithGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertRuleCopy(t *testing.T) {
|
func TestAlertRuleCopy(t *testing.T) {
|
||||||
for i := 0; i < 100; i++ {
|
t.Run("should return a copy of the rule", func(t *testing.T) {
|
||||||
rule := RuleGen.GenerateRef()
|
for i := 0; i < 100; i++ {
|
||||||
|
rule := RuleGen.GenerateRef()
|
||||||
|
copied := rule.Copy()
|
||||||
|
require.Empty(t, rule.Diff(copied))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should create a copy of the prometheus rule definition from the metadata", func(t *testing.T) {
|
||||||
|
rule := RuleGen.With(RuleGen.WithMetadata(AlertRuleMetadata{PrometheusStyleRule: &PrometheusStyleRule{
|
||||||
|
OriginalRuleDefinition: "data",
|
||||||
|
}})).GenerateRef()
|
||||||
copied := rule.Copy()
|
copied := rule.Copy()
|
||||||
require.Empty(t, rule.Diff(copied))
|
require.NotSame(t, rule.Metadata.PrometheusStyleRule, copied.Metadata.PrometheusStyleRule)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// This test makes sure the default generator
|
// This test makes sure the default generator
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package prom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DatasourceUID string
|
||||||
|
DatasourceType string
|
||||||
|
FromTimeRange *time.Duration
|
||||||
|
EvaluationOffset *time.Duration
|
||||||
|
ExecErrState models.ExecutionErrorState
|
||||||
|
NoDataState models.NoDataState
|
||||||
|
RecordingRules RulesConfig
|
||||||
|
AlertRules RulesConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type RulesConfig struct {
|
||||||
|
IsPaused bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultTimeRange = 600 * time.Second
|
||||||
|
defaultEvaluationOffset = 0 * time.Minute
|
||||||
|
|
||||||
|
defaultConfig = Config{
|
||||||
|
FromTimeRange: &defaultTimeRange,
|
||||||
|
EvaluationOffset: &defaultEvaluationOffset,
|
||||||
|
ExecErrState: models.ErrorErrState,
|
||||||
|
NoDataState: models.NoData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Converter struct {
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConverter(cfg Config) (*Converter, error) {
|
||||||
|
if cfg.DatasourceUID == "" {
|
||||||
|
return nil, fmt.Errorf("datasource UID is required")
|
||||||
|
}
|
||||||
|
if cfg.DatasourceType == "" {
|
||||||
|
return nil, fmt.Errorf("datasource type is required")
|
||||||
|
}
|
||||||
|
if cfg.FromTimeRange == nil {
|
||||||
|
cfg.FromTimeRange = defaultConfig.FromTimeRange
|
||||||
|
}
|
||||||
|
if cfg.EvaluationOffset == nil {
|
||||||
|
cfg.EvaluationOffset = defaultConfig.EvaluationOffset
|
||||||
|
}
|
||||||
|
if cfg.ExecErrState == "" {
|
||||||
|
cfg.ExecErrState = defaultConfig.ExecErrState
|
||||||
|
}
|
||||||
|
if cfg.NoDataState == "" {
|
||||||
|
cfg.NoDataState = defaultConfig.NoDataState
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.DatasourceType != datasources.DS_PROMETHEUS && cfg.DatasourceType != datasources.DS_LOKI {
|
||||||
|
return nil, fmt.Errorf("invalid datasource type: %s", cfg.DatasourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Converter{
|
||||||
|
cfg: cfg,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrometheusRulesToGrafana converts a Prometheus rule group into Grafana Alerting rule group.
|
||||||
|
func (p *Converter) PrometheusRulesToGrafana(orgID int64, namespaceUID string, group PrometheusRuleGroup) (*models.AlertRuleGroup, error) {
|
||||||
|
for _, rule := range group.Rules {
|
||||||
|
err := validatePrometheusRule(rule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid Prometheus rule '%s': %w", rule.Alert, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
grafanaGroup, err := p.convertRuleGroup(orgID, namespaceUID, group)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert rule group '%s': %w", group.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return grafanaGroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePrometheusRule(rule PrometheusRule) error {
|
||||||
|
if rule.KeepFiringFor != nil {
|
||||||
|
return fmt.Errorf("keep_firing_for is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Converter) convertRuleGroup(orgID int64, namespaceUID string, promGroup PrometheusRuleGroup) (*models.AlertRuleGroup, error) {
|
||||||
|
uniqueNames := map[string]int{}
|
||||||
|
rules := make([]models.AlertRule, 0, len(promGroup.Rules))
|
||||||
|
interval := time.Duration(promGroup.Interval)
|
||||||
|
for i, rule := range promGroup.Rules {
|
||||||
|
gr, err := p.convertRule(orgID, namespaceUID, promGroup.Name, rule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert Prometheus rule '%s' to Grafana rule: %w", rule.Alert, err)
|
||||||
|
}
|
||||||
|
gr.RuleGroupIndex = i + 1
|
||||||
|
gr.IntervalSeconds = int64(interval.Seconds())
|
||||||
|
|
||||||
|
// Check rule title uniqueness within the group.
|
||||||
|
uniqueNames[gr.Title]++
|
||||||
|
if val := uniqueNames[gr.Title]; val > 1 {
|
||||||
|
gr.Title = fmt.Sprintf("%s (%d)", gr.Title, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, gr)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &models.AlertRuleGroup{
|
||||||
|
FolderUID: namespaceUID,
|
||||||
|
Interval: int64(interval.Seconds()),
|
||||||
|
Rules: rules,
|
||||||
|
Title: promGroup.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Converter) convertRule(orgID int64, namespaceUID, group string, rule PrometheusRule) (models.AlertRule, error) {
|
||||||
|
var forInterval time.Duration
|
||||||
|
if rule.For != nil {
|
||||||
|
forInterval = time.Duration(*rule.For)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryNode, err := createAlertQueryNode(p.cfg.DatasourceUID, p.cfg.DatasourceType, rule.Expr, *p.cfg.FromTimeRange, *p.cfg.EvaluationOffset)
|
||||||
|
if err != nil {
|
||||||
|
return models.AlertRule{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var title string
|
||||||
|
if rule.Record != "" {
|
||||||
|
title = rule.Record
|
||||||
|
} else {
|
||||||
|
title = rule.Alert
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := make(map[string]string, len(rule.Labels)+1)
|
||||||
|
for k, v := range rule.Labels {
|
||||||
|
labels[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRuleDefinition, err := yaml.Marshal(rule)
|
||||||
|
if err != nil {
|
||||||
|
return models.AlertRule{}, fmt.Errorf("failed to marshal original rule definition: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := models.AlertRule{
|
||||||
|
OrgID: orgID,
|
||||||
|
NamespaceUID: namespaceUID,
|
||||||
|
Title: title,
|
||||||
|
Data: []models.AlertQuery{queryNode},
|
||||||
|
Condition: "A",
|
||||||
|
NoDataState: p.cfg.NoDataState,
|
||||||
|
ExecErrState: p.cfg.ExecErrState,
|
||||||
|
Annotations: rule.Annotations,
|
||||||
|
Labels: labels,
|
||||||
|
For: forInterval,
|
||||||
|
RuleGroup: group,
|
||||||
|
Metadata: models.AlertRuleMetadata{
|
||||||
|
PrometheusStyleRule: &models.PrometheusStyleRule{
|
||||||
|
OriginalRuleDefinition: string(originalRuleDefinition),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Record != "" {
|
||||||
|
result.Record = &models.Record{
|
||||||
|
From: "A",
|
||||||
|
Metric: rule.Record,
|
||||||
|
}
|
||||||
|
result.IsPaused = p.cfg.RecordingRules.IsPaused
|
||||||
|
} else {
|
||||||
|
result.IsPaused = p.cfg.AlertRules.IsPaused
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAlertQueryNode(datasourceUID, datasourceType, expr string, fromTimeRange, evaluationOffset time.Duration) (models.AlertQuery, error) {
|
||||||
|
modelData := map[string]interface{}{
|
||||||
|
"datasource": map[string]interface{}{
|
||||||
|
"type": datasourceType,
|
||||||
|
"uid": datasourceUID,
|
||||||
|
},
|
||||||
|
"expr": expr,
|
||||||
|
"instant": true,
|
||||||
|
"range": false,
|
||||||
|
"refId": "A",
|
||||||
|
}
|
||||||
|
|
||||||
|
if datasourceType == datasources.DS_LOKI {
|
||||||
|
modelData["queryType"] = "instant"
|
||||||
|
}
|
||||||
|
|
||||||
|
modelJSON, err := json.Marshal(modelData)
|
||||||
|
if err != nil {
|
||||||
|
return models.AlertQuery{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.AlertQuery{
|
||||||
|
DatasourceUID: datasourceUID,
|
||||||
|
Model: modelJSON,
|
||||||
|
RefID: "A",
|
||||||
|
RelativeTimeRange: models.RelativeTimeRange{
|
||||||
|
From: models.Duration(fromTimeRange + evaluationOffset),
|
||||||
|
To: models.Duration(0 + evaluationOffset),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package prom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
prommodel "github.com/prometheus/common/model"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrometheusRulesToGrafana(t *testing.T) {
|
||||||
|
fiveMin := prommodel.Duration(5 * time.Minute)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
orgID int64
|
||||||
|
namespace string
|
||||||
|
promGroup PrometheusRuleGroup
|
||||||
|
config Config
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid rule group",
|
||||||
|
orgID: 1,
|
||||||
|
namespace: "some-namespace-uid",
|
||||||
|
promGroup: PrometheusRuleGroup{
|
||||||
|
Name: "test-group-1",
|
||||||
|
Interval: prommodel.Duration(10 * time.Second),
|
||||||
|
Rules: []PrometheusRule{
|
||||||
|
{
|
||||||
|
Alert: "alert-1",
|
||||||
|
Expr: "cpu_usage > 80",
|
||||||
|
For: &fiveMin,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"severity": "critical",
|
||||||
|
},
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"summary": "CPU usage is critical",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rules with keep_firing_for are not supported",
|
||||||
|
orgID: 1,
|
||||||
|
namespace: "namespaceUID",
|
||||||
|
promGroup: PrometheusRuleGroup{
|
||||||
|
Name: "test-group-1",
|
||||||
|
Interval: prommodel.Duration(1 * time.Minute),
|
||||||
|
Rules: []PrometheusRule{
|
||||||
|
{
|
||||||
|
Alert: "alert-1",
|
||||||
|
Expr: "up == 0",
|
||||||
|
KeepFiringFor: &fiveMin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rule with empty interval",
|
||||||
|
orgID: 1,
|
||||||
|
namespace: "namespaceUID",
|
||||||
|
promGroup: PrometheusRuleGroup{
|
||||||
|
Name: "test-group-1",
|
||||||
|
Rules: []PrometheusRule{
|
||||||
|
{
|
||||||
|
Alert: "alert-1",
|
||||||
|
Expr: "up == 0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recording rule",
|
||||||
|
orgID: 1,
|
||||||
|
namespace: "namespaceUID",
|
||||||
|
promGroup: PrometheusRuleGroup{
|
||||||
|
Name: "test-group-1",
|
||||||
|
Rules: []PrometheusRule{
|
||||||
|
{
|
||||||
|
Record: "some_metric",
|
||||||
|
Expr: "sum(rate(http_requests_total[5m]))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
tc.config.DatasourceUID = "datasource-uid"
|
||||||
|
tc.config.DatasourceType = datasources.DS_PROMETHEUS
|
||||||
|
converter, err := NewConverter(tc.config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
grafanaGroup, err := converter.PrometheusRulesToGrafana(tc.orgID, tc.namespace, tc.promGroup)
|
||||||
|
|
||||||
|
if tc.expectError {
|
||||||
|
require.Error(t, err, tc.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err, tc.name)
|
||||||
|
|
||||||
|
require.Equal(t, tc.promGroup.Name, grafanaGroup.Title, tc.name)
|
||||||
|
expectedInterval := int64(time.Duration(tc.promGroup.Interval).Seconds())
|
||||||
|
require.Equal(t, expectedInterval, grafanaGroup.Interval, tc.name)
|
||||||
|
|
||||||
|
require.Equal(t, len(tc.promGroup.Rules), len(grafanaGroup.Rules), tc.name)
|
||||||
|
|
||||||
|
for j, promRule := range tc.promGroup.Rules {
|
||||||
|
grafanaRule := grafanaGroup.Rules[j]
|
||||||
|
|
||||||
|
if promRule.Record != "" {
|
||||||
|
require.Equal(t, promRule.Record, grafanaRule.Title)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, promRule.Alert, grafanaRule.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedFor time.Duration
|
||||||
|
if promRule.For != nil {
|
||||||
|
expectedFor = time.Duration(*promRule.For)
|
||||||
|
}
|
||||||
|
require.Equal(t, expectedFor, grafanaRule.For, tc.name)
|
||||||
|
|
||||||
|
expectedLabels := make(map[string]string, len(promRule.Labels)+1)
|
||||||
|
for k, v := range promRule.Labels {
|
||||||
|
expectedLabels[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, expectedLabels, grafanaRule.Labels, tc.name)
|
||||||
|
require.Equal(t, promRule.Annotations, grafanaRule.Annotations, tc.name)
|
||||||
|
require.Equal(t, models.Duration(0*time.Minute), grafanaRule.Data[0].RelativeTimeRange.To)
|
||||||
|
require.Equal(t, models.Duration(10*time.Minute), grafanaRule.Data[0].RelativeTimeRange.From)
|
||||||
|
|
||||||
|
originalRuleDefinition, err := yaml.Marshal(promRule)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, string(originalRuleDefinition), grafanaRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrometheusRulesToGrafanaWithDuplicateRuleNames(t *testing.T) {
|
||||||
|
cfg := Config{
|
||||||
|
DatasourceUID: "datasource-uid",
|
||||||
|
DatasourceType: datasources.DS_PROMETHEUS,
|
||||||
|
}
|
||||||
|
converter, err := NewConverter(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
promGroup := PrometheusRuleGroup{
|
||||||
|
Name: "test-group-1",
|
||||||
|
Interval: prommodel.Duration(10 * time.Second),
|
||||||
|
Rules: []PrometheusRule{
|
||||||
|
{
|
||||||
|
Alert: "alert",
|
||||||
|
Expr: "up",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Alert: "alert",
|
||||||
|
Expr: "up",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Alert: "another alert",
|
||||||
|
Expr: "up",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Alert: "alert",
|
||||||
|
Expr: "up",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
group, err := converter.PrometheusRulesToGrafana(1, "namespaceUID", promGroup)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, "test-group-1", group.Title)
|
||||||
|
require.Len(t, group.Rules, 4)
|
||||||
|
require.Equal(t, "alert", group.Rules[0].Title)
|
||||||
|
require.Equal(t, "alert (2)", group.Rules[1].Title)
|
||||||
|
require.Equal(t, "another alert", group.Rules[2].Title)
|
||||||
|
require.Equal(t, "alert (3)", group.Rules[3].Title)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package prom
|
||||||
|
|
||||||
|
import (
|
||||||
|
prommodel "github.com/prometheus/common/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PrometheusRulesFile struct {
|
||||||
|
Groups []PrometheusRuleGroup `yaml:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrometheusRuleGroup struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Interval prommodel.Duration `yaml:"interval"`
|
||||||
|
Rules []PrometheusRule `yaml:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrometheusRule struct {
|
||||||
|
Alert string `yaml:"alert,omitempty"`
|
||||||
|
Expr string `yaml:"expr,omitempty"`
|
||||||
|
For *prommodel.Duration `yaml:"for,omitempty"`
|
||||||
|
KeepFiringFor *prommodel.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"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package prom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
prommodel "github.com/prometheus/common/model"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrometheusRulesFileYAML(t *testing.T) {
|
||||||
|
interval := prommodel.Duration(5 * time.Minute)
|
||||||
|
alertFor := prommodel.Duration(10 * time.Minute)
|
||||||
|
keepFiring := prommodel.Duration(15 * time.Minute)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input PrometheusRulesFile
|
||||||
|
expectedYAML string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple alert rule and a recording rule",
|
||||||
|
input: PrometheusRulesFile{
|
||||||
|
Groups: []PrometheusRuleGroup{
|
||||||
|
{
|
||||||
|
Name: "test_group",
|
||||||
|
Interval: interval,
|
||||||
|
Rules: []PrometheusRule{
|
||||||
|
{
|
||||||
|
Alert: "alert-1",
|
||||||
|
Expr: "vector(0) > 90",
|
||||||
|
For: &alertFor,
|
||||||
|
KeepFiringFor: &keepFiring,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"team": "alerting",
|
||||||
|
},
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"summary": "some summary",
|
||||||
|
"description": "some description",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Record: "vector(1)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedYAML: `
|
||||||
|
groups:
|
||||||
|
- name: test_group
|
||||||
|
interval: 5m
|
||||||
|
rules:
|
||||||
|
- alert: alert-1
|
||||||
|
expr: vector(0) > 90
|
||||||
|
for: 10m
|
||||||
|
keep_firing_for: 15m
|
||||||
|
labels:
|
||||||
|
team: alerting
|
||||||
|
annotations:
|
||||||
|
description: some description
|
||||||
|
summary: some summary
|
||||||
|
- record: vector(1)
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty rules file",
|
||||||
|
input: PrometheusRulesFile{
|
||||||
|
Groups: []PrometheusRuleGroup{},
|
||||||
|
},
|
||||||
|
expectedYAML: `groups: []`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty group",
|
||||||
|
input: PrometheusRulesFile{
|
||||||
|
Groups: []PrometheusRuleGroup{
|
||||||
|
{
|
||||||
|
Name: "empty_group",
|
||||||
|
Interval: interval,
|
||||||
|
Rules: []PrometheusRule{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedYAML: `
|
||||||
|
groups:
|
||||||
|
- name: empty_group
|
||||||
|
interval: 5m
|
||||||
|
rules: []`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
yamlData, err := yaml.Marshal(tt.input)
|
||||||
|
require.NoError(t, err, "Failed to marshal to YAML")
|
||||||
|
require.YAMLEq(t, tt.expectedYAML, string(yamlData))
|
||||||
|
|
||||||
|
var parsed PrometheusRulesFile
|
||||||
|
err = yaml.Unmarshal(yamlData, &parsed)
|
||||||
|
require.NoError(t, err, "Failed to unmarshal from YAML")
|
||||||
|
|
||||||
|
require.Equal(t, tt.input, parsed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user