Alerting: Support for Mimir configuration in Grafana Alertmanager (#106402)

This commit is contained in:
Yuri Tseretyan
2025-06-13 16:32:23 +02:00
committed by GitHub
parent 3c0d087186
commit b0ff51a903
6 changed files with 476 additions and 21 deletions
@@ -3,13 +3,17 @@ package definitions
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/go-openapi/strfmt"
alertingTemplates "github.com/grafana/alerting/templates"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"gopkg.in/yaml.v3"
"github.com/grafana/alerting/definition"
@@ -263,6 +267,7 @@ type (
PostableApiReceiver = definition.PostableApiReceiver
PostableGrafanaReceivers = definition.PostableGrafanaReceivers
ReceiverType = definition.ReceiverType
MergeResult = definition.MergeResult
)
const (
@@ -643,13 +648,82 @@ type DatasourceUIDReference struct {
DatasourceUID string
}
type ExtraConfiguration struct {
Identifier string `yaml:"identifier" json:"identifier"`
MergeMatchers config.Matchers `yaml:"merge_matchers" json:"merge_matchers"`
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
}
func (c ExtraConfiguration) Validate() error {
if c.Identifier == "" {
return errors.New("identifier is required")
}
if len(c.MergeMatchers) == 0 {
return errors.New("at least one matcher is required")
}
for _, m := range c.MergeMatchers {
if m.Type != labels.MatchEqual {
return errors.New("only matchers with type equal are supported")
}
}
err := c.AlertmanagerConfig.Validate()
if err != nil {
return fmt.Errorf("invalid alertmanager configuration: %w", err)
}
return nil
}
// swagger:model
type PostableUserConfig struct {
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
ExtraConfigs []ExtraConfiguration `yaml:"extra_config,omitempty" json:"extra_config,omitempty"`
amSimple map[string]interface{} `yaml:"-" json:"-"`
}
func (c *PostableUserConfig) GetMergedAlertmanagerConfig() (MergeResult, error) {
if len(c.ExtraConfigs) == 0 {
return MergeResult{
Config: c.AlertmanagerConfig,
}, nil
}
// support only one config for now
mimirCfg := c.ExtraConfigs[0]
opts := definition.MergeOpts{
DedupSuffix: mimirCfg.Identifier,
SubtreeMatchers: mimirCfg.MergeMatchers,
}
if err := opts.Validate(); err != nil {
return MergeResult{}, fmt.Errorf("invalid merge options: %w", err)
}
return definition.Merge(c.AlertmanagerConfig, mimirCfg.AlertmanagerConfig, opts) // for now support only the first extra config
}
// GetMergedTemplateDefinitions converts the given PostableUserConfig's TemplateFiles to a slice of TemplateDefinitions.
func (c *PostableUserConfig) GetMergedTemplateDefinitions() []alertingTemplates.TemplateDefinition {
out := make([]alertingTemplates.TemplateDefinition, 0, len(c.TemplateFiles))
for name, tmpl := range c.TemplateFiles {
out = append(out, alertingTemplates.TemplateDefinition{
Name: name,
Template: tmpl,
Kind: alertingTemplates.GrafanaKind,
})
}
if len(c.ExtraConfigs) == 0 {
return out
}
// support only one config for now
for name, tmpl := range c.ExtraConfigs[0].TemplateFiles {
out = append(out, alertingTemplates.TemplateDefinition{
Name: name,
Template: tmpl,
Kind: alertingTemplates.MimirKind,
})
}
return out
}
func (c *PostableUserConfig) UnmarshalJSON(b []byte) error {
type plain PostableUserConfig
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
@@ -661,6 +735,15 @@ func (c *PostableUserConfig) UnmarshalJSON(b []byte) error {
return err
}
if len(c.ExtraConfigs) > 1 {
return errors.New("only one extra config is supported")
}
for _, extraConfig := range c.ExtraConfigs {
if err := extraConfig.Validate(); err != nil {
return fmt.Errorf("extra configuration is invalid: %w", err)
}
}
type intermediate struct {
AlertmanagerConfig map[string]interface{} `yaml:"alertmanager_config" json:"alertmanager_config"`
}
@@ -6,7 +6,9 @@ import (
"strings"
"testing"
alertingTemplates "github.com/grafana/alerting/templates"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -215,3 +217,201 @@ func Test_RawMessageMarshaling(t *testing.T) {
assert.Equal(t, RawMessage(`{"data":"test"}`), n.Field)
})
}
func TestPostableUserConfig_GetMergedAlertmanagerConfig(t *testing.T) {
alertmanagerCfg := PostableApiAlertingConfig{
Config: Config{
Route: &Route{
Receiver: "default",
},
},
Receivers: []*PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "default",
},
},
},
}
testCases := []struct {
name string
config PostableUserConfig
expectedError string
}{
{
name: "no extra configs",
config: PostableUserConfig{
AlertmanagerConfig: alertmanagerCfg,
},
},
{
name: "valid mimir config",
config: PostableUserConfig{
AlertmanagerConfig: alertmanagerCfg,
ExtraConfigs: []ExtraConfiguration{
{
Identifier: "mimir-1",
MergeMatchers: config.Matchers{
{
Type: labels.MatchEqual,
Name: "cluster",
Value: "prod",
},
},
AlertmanagerConfig: PostableApiAlertingConfig{
Config: Config{
Route: &Route{
Receiver: "mimir-receiver",
},
},
Receivers: []*PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "mimir-receiver",
},
},
},
},
},
},
},
},
{
name: "empty identifier",
config: PostableUserConfig{
AlertmanagerConfig: alertmanagerCfg,
ExtraConfigs: []ExtraConfiguration{
{
Identifier: "",
MergeMatchers: config.Matchers{},
AlertmanagerConfig: PostableApiAlertingConfig{
Config: Config{
Route: &Route{
Receiver: "test",
},
},
},
},
},
},
expectedError: "invalid merge options",
},
{
name: "bad matcher type",
config: PostableUserConfig{
AlertmanagerConfig: alertmanagerCfg,
ExtraConfigs: []ExtraConfiguration{
{
Identifier: "test",
MergeMatchers: config.Matchers{
{
Type: labels.MatchNotEqual,
Name: "cluster",
Value: "prod",
},
},
},
},
},
expectedError: "only equality matchers are allowed",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := tc.config.GetMergedAlertmanagerConfig()
if tc.expectedError != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.expectedError)
} else {
require.NoError(t, err)
require.NotNil(t, result.Config)
}
})
}
}
func TestPostableUserConfig_GetMergedTemplateDefinitions(t *testing.T) {
testCases := []struct {
name string
config PostableUserConfig
expectedTemplates int
}{
{
name: "no templates",
config: PostableUserConfig{
TemplateFiles: map[string]string{},
ExtraConfigs: []ExtraConfiguration{},
},
expectedTemplates: 0,
},
{
name: "grafana templates only",
config: PostableUserConfig{
TemplateFiles: map[string]string{
"grafana-template1": "{{ define \"test\" }}Hello{{ end }}",
"grafana-template2": "{{ define \"test2\" }}World{{ end }}",
},
ExtraConfigs: []ExtraConfiguration{},
},
expectedTemplates: 2,
},
{
name: "mimir templates only",
config: PostableUserConfig{
TemplateFiles: map[string]string{},
ExtraConfigs: []ExtraConfiguration{
{
TemplateFiles: map[string]string{
"mimir-template": "{{ define \"mimir\" }}Mimir{{ end }}",
},
},
},
},
expectedTemplates: 1,
},
{
name: "mixed templates",
config: PostableUserConfig{
TemplateFiles: map[string]string{
"grafana-template": "{{ define \"grafana\" }}Grafana{{ end }}",
},
ExtraConfigs: []ExtraConfiguration{
{
TemplateFiles: map[string]string{
"mimir-template": "{{ define \"mimir\" }}Mimir{{ end }}",
},
},
},
},
expectedTemplates: 2,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := tc.config.GetMergedTemplateDefinitions()
require.Len(t, result, tc.expectedTemplates)
templateMap := make(map[string]string)
kindMap := make(map[string]alertingTemplates.Kind)
for _, tmpl := range result {
templateMap[tmpl.Name] = tmpl.Template
kindMap[tmpl.Name] = tmpl.Kind
}
for name, content := range tc.config.TemplateFiles {
require.Equal(t, content, templateMap[name])
require.Equal(t, alertingTemplates.GrafanaKind, kindMap[name])
}
if len(tc.config.ExtraConfigs) > 0 {
for name, content := range tc.config.ExtraConfigs[0].TemplateFiles {
require.Equal(t, content, templateMap[name])
require.Equal(t, alertingTemplates.MimirKind, kindMap[name])
}
}
})
}
}