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
+40 -7
View File
@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
alertingNotify "github.com/grafana/alerting/notify"
@@ -314,11 +315,43 @@ func (am *alertmanager) aggregateInhibitMatchers(rules []config.InhibitRule, amu
}
}
func logMergeResult(l log.Logger, m apimodels.MergeResult) {
if len(m.RenamedReceivers) == 0 && len(m.RenamedTimeIntervals) == 0 {
return
}
logCtx := make([]any, 0, 4)
if len(m.RenamedTimeIntervals) > 0 {
rcvBuilder := strings.Builder{}
for from, to := range m.RenamedReceivers {
rcvBuilder.WriteString(fmt.Sprintf("'%s'->'%s',", from, to))
}
logCtx = append(logCtx, "renamedReceivers", fmt.Sprintf("[%s]", rcvBuilder.String()[0:rcvBuilder.Len()-1]))
}
if len(m.RenamedTimeIntervals) > 0 {
rcvBuilder := strings.Builder{}
for from, to := range m.RenamedTimeIntervals {
rcvBuilder.WriteString(fmt.Sprintf("'%s'->'%s',", from, to))
}
logCtx = append(logCtx, "renamedTimeIntervals", fmt.Sprintf("[%s]", rcvBuilder.String()[0:rcvBuilder.Len()-1]))
}
l.Info("Configurations merged successfully but some resources were renamed", logCtx...)
}
// applyConfig applies a new configuration by re-initializing all components using the configuration provided.
// It returns a boolean indicating whether the user config was changed and an error.
// It is not safe to call concurrently.
func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.PostableUserConfig, skipInvalid bool) (bool, error) {
err := AddAutogenConfig(ctx, am.logger, am.Store, am.Base.TenantID(), &cfg.AlertmanagerConfig, skipInvalid)
mergeResult, err := cfg.GetMergedAlertmanagerConfig()
if err != nil {
return false, fmt.Errorf("failed to get full alertmanager configuration: %w", err)
}
logMergeResult(am.logger, mergeResult)
amConfig := mergeResult.Config
templates := cfg.GetMergedTemplateDefinitions()
// Now add autogenerated config to the route.
err = AddAutogenConfig(ctx, am.logger, am.Store, am.Base.TenantID(), &amConfig, skipInvalid)
if err != nil {
return false, err
}
@@ -337,7 +370,7 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable
return false, nil
}
receivers := PostableApiAlertingConfigToApiReceivers(cfg.AlertmanagerConfig)
receivers := PostableApiAlertingConfigToApiReceivers(amConfig)
for _, recv := range receivers {
err = patchNewSecureFields(ctx, recv, alertingNotify.DecodeSecretsFromBase64, am.decryptFn)
if err != nil {
@@ -347,11 +380,11 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable
am.logger.Info("Applying new configuration to Alertmanager", "configHash", fmt.Sprintf("%x", configHash))
err = am.Base.ApplyConfig(alertingNotify.NotificationsConfiguration{
RoutingTree: cfg.AlertmanagerConfig.Route.AsAMRoute(),
InhibitRules: cfg.AlertmanagerConfig.InhibitRules,
MuteTimeIntervals: cfg.AlertmanagerConfig.MuteTimeIntervals,
TimeIntervals: cfg.AlertmanagerConfig.TimeIntervals,
Templates: ToTemplateDefinitions(cfg),
RoutingTree: amConfig.Route.AsAMRoute(),
InhibitRules: amConfig.InhibitRules,
MuteTimeIntervals: amConfig.MuteTimeIntervals,
TimeIntervals: amConfig.TimeIntervals,
Templates: templates,
Receivers: receivers,
DispatcherLimits: &nilLimits{},
Raw: rawConfig,
@@ -2,16 +2,22 @@ package notifier
import (
"context"
"net/url"
"testing"
"time"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/client_golang/prometheus"
promcfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
@@ -62,3 +68,145 @@ func TestAlertmanager_newAlertmanager(t *testing.T) {
am := setupAMTest(t)
require.False(t, am.Ready())
}
func TestAlertmanager_ApplyConfig(t *testing.T) {
basicConfig := func() definitions.PostableApiAlertingConfig {
return definitions.PostableApiAlertingConfig{
Config: definitions.Config{
Route: &definitions.Route{
Receiver: "default-receiver",
ObjectMatchers: definitions.ObjectMatchers{
&labels.Matcher{
Type: labels.MatchEqual,
Name: "__grafana_autogenerated__",
Value: "true",
},
},
},
},
Receivers: []*definitions.PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "default-receiver",
},
},
},
}
}
testCases := []struct {
name string
config *definitions.PostableUserConfig
expectedError string
skipInvalid bool
}{
{
name: "basic config",
config: &definitions.PostableUserConfig{
AlertmanagerConfig: basicConfig(),
TemplateFiles: map[string]string{
"grafana-template": "{{ define \"grafana.title\" }}Alert{{ end }}",
},
},
skipInvalid: false,
},
{
name: "with mimir config",
config: &definitions.PostableUserConfig{
AlertmanagerConfig: basicConfig(),
TemplateFiles: map[string]string{
"grafana-template": "{{ define \"grafana.title\" }}Grafana Alert{{ end }}",
},
ExtraConfigs: []definitions.ExtraConfiguration{
{
Identifier: "mimir-prod",
MergeMatchers: config.Matchers{
{
Type: labels.MatchEqual,
Name: "__mimir__",
Value: "true",
},
},
TemplateFiles: map[string]string{
"mimir-template": "{{ define \"mimir.title\" }}Mimir Alert{{ end }}",
},
AlertmanagerConfig: definitions.PostableApiAlertingConfig{
Config: definitions.Config{
Route: &definitions.Route{
Receiver: "mimir-webhook",
GroupBy: []model.LabelName{"alertname", "cluster"},
},
},
Receivers: []*definitions.PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "mimir-webhook",
WebhookConfigs: []*config.WebhookConfig{
{
URL: &config.SecretURL{
URL: &url.URL{
Scheme: "https",
Host: "webhook.example.com",
Path: "/alerts",
},
},
HTTPConfig: &promcfg.DefaultHTTPClientConfig,
},
},
},
},
},
},
},
},
},
skipInvalid: false,
},
{
name: "invalid config fails",
config: &definitions.PostableUserConfig{
AlertmanagerConfig: basicConfig(),
ExtraConfigs: []definitions.ExtraConfiguration{
{
Identifier: "", // invalid: empty identifier
MergeMatchers: config.Matchers{},
AlertmanagerConfig: definitions.PostableApiAlertingConfig{
Config: definitions.Config{
Route: &definitions.Route{
Receiver: "test-receiver",
},
},
},
},
},
},
expectedError: "failed to get full alertmanager configuration",
skipInvalid: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
am := setupAMTest(t)
ctx := context.Background()
changed, err := am.applyConfig(ctx, tc.config, false)
if tc.expectedError != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.expectedError)
require.False(t, changed)
} else {
require.NoError(t, err)
require.True(t, changed)
templateDefs := tc.config.GetMergedTemplateDefinitions()
expectedTemplateCount := len(tc.config.TemplateFiles)
if len(tc.config.ExtraConfigs) > 0 {
expectedTemplateCount += len(tc.config.ExtraConfigs[0].TemplateFiles)
}
require.Len(t, templateDefs, expectedTemplateCount)
}
})
}
}
-14
View File
@@ -5,7 +5,6 @@ import (
"fmt"
alertingNotify "github.com/grafana/alerting/notify"
alertingTemplates "github.com/grafana/alerting/templates"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
@@ -132,19 +131,6 @@ func PostableApiAlertingConfigToApiReceivers(c apimodels.PostableApiAlertingConf
return apiReceivers
}
// ToTemplateDefinitions converts the given PostableUserConfig's TemplateFiles to a slice of TemplateDefinitions.
func ToTemplateDefinitions(cfg *apimodels.PostableUserConfig) []alertingTemplates.TemplateDefinition {
out := make([]alertingTemplates.TemplateDefinition, 0, len(cfg.TemplateFiles))
for name, tmpl := range cfg.TemplateFiles {
out = append(out, alertingTemplates.TemplateDefinition{
Name: name,
Template: tmpl,
Kind: alertingTemplates.GrafanaKind,
})
}
return out
}
// Silence-specific compat functions to convert between grafana/alerting and model types.
func GettableSilenceToSilence(s alertingNotify.GettableSilence) *models.Silence {