From 1f039a1553b132d6c046530db6ec8aa2ce4daf46 Mon Sep 17 00:00:00 2001 From: Paulin Todev Date: Fri, 21 Nov 2025 12:50:31 +0000 Subject: [PATCH 1/2] Vendor Alertmanager config --- Makefile | 7 +- .../testdata/expected_1.yml | 9 +- .../testdata/expected_2.yml | 8 +- .../mimir/alerts/kubernetes/alerts.go | 8 +- .../mimir/alerts/kubernetes/events.go | 16 +- .../mimir/alerts/kubernetes/events_test.go | 38 +- internal/mimir/alertmanager/types.go | 515 ++++++++++++++++++ internal/mimir/client/alerts.go | 10 +- internal/mimir/client/alerts_test.go | 11 +- internal/mimir/client/client.go | 2 +- .../testdata/alertmanager/conf.good.yml | 35 +- .../testdata/alertmanager/response.good.yml | 177 +----- 12 files changed, 574 insertions(+), 262 deletions(-) create mode 100644 internal/mimir/alertmanager/types.go diff --git a/Makefile b/Makefile index c2d7822a442..ccb5e1a7b1b 100644 --- a/Makefile +++ b/Makefile @@ -15,9 +15,10 @@ ## ## Targets for running tests: ## -## test Run tests -## lint Lint code -## integration-test Run integration tests +## test Run tests +## lint Lint code +## integration-test Run integration tests +## integration-test-k8s Run Kubernetes integration tests ## ## Targets for building binaries: ## diff --git a/internal/cmd/integration-tests-k8s/tests/mimir-alerts-kubernetes/testdata/expected_1.yml b/internal/cmd/integration-tests-k8s/tests/mimir-alerts-kubernetes/testdata/expected_1.yml index 3250aad9f43..5b32e9a83f5 100644 --- a/internal/cmd/integration-tests-k8s/tests/mimir-alerts-kubernetes/testdata/expected_1.yml +++ b/internal/cmd/integration-tests-k8s/tests/mimir-alerts-kubernetes/testdata/expected_1.yml @@ -12,7 +12,6 @@ alertmanager_config: | smtp_require_tls: true route: receiver: "null" - continue: false routes: - receiver: testing/alertmgr-config1/null matchers: @@ -29,7 +28,6 @@ alertmanager_config: | - receiver: testing/alertmgr-config2/database-pager matchers: - service="webapp" - continue: false group_wait: 10s receivers: - name: "null" @@ -37,14 +35,9 @@ alertmanager_config: | - name: testing/alertmgr-config1/null - name: testing/alertmgr-config1/myamc webhook_configs: - - send_resolved: false + - url: http://test.url http_config: follow_redirects: true - enable_http2: true - url: http://test.url - url_file: "" - max_alerts: 0 - timeout: 0s - name: testing/alertmgr-config2/null - name: testing/alertmgr-config2/database-pager templates: diff --git a/internal/cmd/integration-tests-k8s/tests/mimir-alerts-kubernetes/testdata/expected_2.yml b/internal/cmd/integration-tests-k8s/tests/mimir-alerts-kubernetes/testdata/expected_2.yml index 088f6537d43..0b6890638a2 100644 --- a/internal/cmd/integration-tests-k8s/tests/mimir-alerts-kubernetes/testdata/expected_2.yml +++ b/internal/cmd/integration-tests-k8s/tests/mimir-alerts-kubernetes/testdata/expected_2.yml @@ -12,7 +12,6 @@ alertmanager_config: | smtp_require_tls: true route: receiver: "null" - continue: false routes: - receiver: testing/alertmgr-config1/null matchers: @@ -27,13 +26,8 @@ alertmanager_config: | - name: testing/alertmgr-config1/null - name: testing/alertmgr-config1/myamc webhook_configs: - - send_resolved: false + - url: http://test.url http_config: follow_redirects: true - enable_http2: true - url: http://test.url - url_file: "" - max_alerts: 0 - timeout: 0s templates: - default_template diff --git a/internal/component/mimir/alerts/kubernetes/alerts.go b/internal/component/mimir/alerts/kubernetes/alerts.go index db028b9d43f..fe2cffc46fb 100644 --- a/internal/component/mimir/alerts/kubernetes/alerts.go +++ b/internal/component/mimir/alerts/kubernetes/alerts.go @@ -9,13 +9,12 @@ import ( "time" "github.com/go-kit/log" + alertmgr_cfg "github.com/grafana/alloy/internal/mimir/alertmanager" "github.com/grafana/dskit/backoff" - alertmgr_cfg "github.com/prometheus/alertmanager/config" coreListers "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/util/workqueue" _ "k8s.io/component-base/metrics/prometheus/workqueue" controller "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/yaml" "github.com/grafana/alloy/internal/component" "github.com/grafana/alloy/internal/component/mimir/util" @@ -230,13 +229,12 @@ func (c *Component) Startup(ctx context.Context) error { return err } - var baseCfg alertmgr_cfg.Config - err = yaml.Unmarshal([]byte(c.args.GlobalConfig), &baseCfg) + baseCfg, err := alertmgr_cfg.Unmarshal([]byte(c.args.GlobalConfig)) if err != nil { return fmt.Errorf("failed to unmarshal global config: %w", err) } - c.eventProcessor = c.newEventProcessor(queue, informerStopChan, namespaceLister, cfgLister, baseCfg) + c.eventProcessor = c.newEventProcessor(queue, informerStopChan, namespaceLister, cfgLister, *baseCfg) go c.eventProcessor.run(ctx) return nil diff --git a/internal/component/mimir/alerts/kubernetes/events.go b/internal/component/mimir/alerts/kubernetes/events.go index 1d4bcf4648a..3bcf528cad0 100644 --- a/internal/component/mimir/alerts/kubernetes/events.go +++ b/internal/component/mimir/alerts/kubernetes/events.go @@ -8,6 +8,7 @@ import ( "github.com/blang/semver/v4" "github.com/go-kit/log" + alertmgr_cfg "github.com/grafana/alloy/internal/mimir/alertmanager" "github.com/grafana/dskit/instrument" "github.com/prometheus-operator/prometheus-operator/pkg/alertmanager" validation_v1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/alertmanager/validation/v1alpha1" @@ -15,13 +16,11 @@ import ( promv1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1alpha1" "github.com/prometheus-operator/prometheus-operator/pkg/assets" promListers_v1alpha "github.com/prometheus-operator/prometheus-operator/pkg/client/listers/monitoring/v1alpha1" - alertmgr_cfg "github.com/prometheus/alertmanager/config" "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/labels" go_k8s "k8s.io/client-go/kubernetes" coreListers "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/util/workqueue" - "sigs.k8s.io/yaml" // Used for CRD compatibility instead of gopkg.in/yaml.v2 "github.com/grafana/alloy/internal/component/common/kubernetes" "github.com/grafana/alloy/internal/component/mimir/util" @@ -180,8 +179,12 @@ func (c *eventProcessor) provisionAlertmanagerConfiguration(ctx context.Context, cfgBuilder = alertmanager.NewConfigBuilder(slog.New(logging.NewSlogGoKitHandler(c.logger)), *version, store, &monitoringv1.Alertmanager{}) ) - convertedCfg := c.baseCfg.String() - err := cfgBuilder.InitializeFromRawConfiguration([]byte(convertedCfg)) + convertedCfg, err := c.baseCfg.String() + if err != nil { + return nil, err + } + + err = cfgBuilder.InitializeFromRawConfiguration([]byte(convertedCfg)) if err != nil { return nil, fmt.Errorf("failed to initialize from global AlertmangerConfig: %w", err) } @@ -195,13 +198,12 @@ func (c *eventProcessor) provisionAlertmanagerConfiguration(ctx context.Context, return nil, fmt.Errorf("failed to marshal configuration: %w", err) } - var res alertmgr_cfg.Config - err = yaml.Unmarshal(generatedConfig, &res) + res, err := alertmgr_cfg.Unmarshal(generatedConfig) if err != nil { return nil, fmt.Errorf("failed to unmarshal generated final configuration: %w", err) } - return &res, nil + return res, nil } func (e *eventProcessor) reconcileState(ctx context.Context) error { diff --git a/internal/component/mimir/alerts/kubernetes/events_test.go b/internal/component/mimir/alerts/kubernetes/events_test.go index c7b120f8ee0..ce5da66856a 100644 --- a/internal/component/mimir/alerts/kubernetes/events_test.go +++ b/internal/component/mimir/alerts/kubernetes/events_test.go @@ -10,10 +10,10 @@ import ( "sigs.k8s.io/yaml" "github.com/go-kit/log" + "github.com/grafana/alloy/internal/mimir/alertmanager" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" monitoringv1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1alpha1" promListers_v1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/client/listers/monitoring/v1alpha1" - "github.com/prometheus/alertmanager/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -33,7 +33,7 @@ import ( type fakeMimirClient struct { alertMgrConfigsMut sync.RWMutex - alertMgrConfig config.Config + alertMgrConfig alertmanager.Config templateFiles map[string]string } @@ -43,7 +43,7 @@ func newFakeMimirClient() *fakeMimirClient { return &fakeMimirClient{} } -func (m *fakeMimirClient) CreateAlertmanagerConfigs(ctx context.Context, conf *config.Config, templateFiles map[string]string) error { +func (m *fakeMimirClient) CreateAlertmanagerConfigs(ctx context.Context, conf *alertmanager.Config, templateFiles map[string]string) error { m.alertMgrConfigsMut.Lock() defer m.alertMgrConfigsMut.Unlock() // These are just shallow copies, but it should be sufficient. @@ -52,19 +52,16 @@ func (m *fakeMimirClient) CreateAlertmanagerConfigs(ctx context.Context, conf *c return nil } -func (m *fakeMimirClient) getAlertmanagerConfig() config.Config { +func (m *fakeMimirClient) getAlertmanagerConfig() alertmanager.Config { m.alertMgrConfigsMut.RLock() defer m.alertMgrConfigsMut.RUnlock() return m.alertMgrConfig } -func convertToAlertmanagerType(t *testing.T, alertmanagerConf string) config.Config { - config.MarshalSecretValue = true - - var res config.Config - err := yaml.Unmarshal([]byte(alertmanagerConf), &res) +func convertToAlertmanagerType(t *testing.T, alertmanagerConf string) alertmanager.Config { + cfg, err := alertmanager.Unmarshal([]byte(alertmanagerConf)) assert.NoError(t, err) - return res + return *cfg } // createTestLoggerWithBuffer creates a logger that writes to a thread-safe buffer for testing @@ -94,7 +91,6 @@ global: smtp_require_tls: true route: receiver: "null" - continue: false receivers: - name: "null" templates: []` @@ -152,7 +148,6 @@ spec: smtp_require_tls: true route: receiver: "null" - continue: false routes: - receiver: mynamespace/alertmgr-config1/null matchers: @@ -166,14 +161,9 @@ receivers: - name: mynamespace/alertmgr-config1/null - name: mynamespace/alertmgr-config1/myamc webhook_configs: - - send_resolved: false - http_config: + - http_config: follow_redirects: true - enable_http2: true url: http://test.url - url_file: "" - max_alerts: 0 - timeout: 0s templates: []` final_amConf_1_and_2 := `global: @@ -185,7 +175,6 @@ templates: []` smtp_require_tls: true route: receiver: "null" - continue: false routes: - receiver: mynamespace/alertmgr-config1/null matchers: @@ -202,21 +191,15 @@ route: - receiver: mynamespace/alertmgr-config2/database-pager matchers: - "service=\"webapp\"" - continue: false group_wait: 10s receivers: - name: "null" - name: mynamespace/alertmgr-config1/null - name: mynamespace/alertmgr-config1/myamc webhook_configs: - - send_resolved: false - timeout: 0s - http_config: + - http_config: follow_redirects: true - enable_http2: true url: http://test.url - url_file: "" - max_alerts: 0 - name: mynamespace/alertmgr-config2/null - name: mynamespace/alertmgr-config2/database-pager templates: []` @@ -405,7 +388,8 @@ spec: // Wait for the configs to be added to mimir require.EventuallyWithT(t, func(c *assert.CollectT) { - actual := mimirClient.getAlertmanagerConfig().String() + actual, err := mimirClient.getAlertmanagerConfig().String() + require.NoError(c, err) require.YAMLEq(c, tt.want, actual, "want", tt.want, "actual", actual) }, 10*time.Second, 100*time.Millisecond) diff --git a/internal/mimir/alertmanager/types.go b/internal/mimir/alertmanager/types.go new file mode 100644 index 00000000000..792a0f697f0 --- /dev/null +++ b/internal/mimir/alertmanager/types.go @@ -0,0 +1,515 @@ +// Copyright 2020 The prometheus-operator Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file was copied from: +// https://github.com/prometheus-operator/prometheus-operator/blob/v0.86.1/pkg/alertmanager/types.go +// Some fields were exported by renaming them to uppercase, and some functions were added. +// TODO: Look into importing it from the upstream repo? + +package alertmanager + +import ( + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/common/model" + "gopkg.in/yaml.v2" +) + +// Customization of Config type from alertmanager repo: +// https://github.com/prometheus/alertmanager/blob/main/config/config.go +// +// Custom global type to get around obfuscation of secret values when +// marshalling. See the following issue for details: +// https://github.com/prometheus/alertmanager/issues/1985 +type Config struct { + Global *GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` + Route *Route `yaml:"route,omitempty" json:"route,omitempty"` + InhibitRules []*InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` + Receivers []*Receiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` + MuteTimeIntervals []*TimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` + TimeIntervals []*TimeInterval `yaml:"time_intervals,omitempty" json:"time_intervals,omitempty"` + Templates []string `yaml:"templates" json:"templates"` +} + +func (c Config) String() (string, error) { + confBytes, err := yaml.Marshal(c) + if err != nil { + return "", err + } + return string(confBytes), nil +} + +// This is a standard Unmarshal functioion. +// The benefit of having it here is that it's guaranteed to use the correct "gopkg.in/yaml.v2" package. +func Unmarshal(data []byte) (*Config, error) { + var c Config + err := yaml.Unmarshal(data, &c) + if err != nil { + return nil, err + } + return &c, nil +} + +type GlobalConfig struct { + // ResolveTimeout is the time after which an alert is declared resolved + // if it has not been updated. + ResolveTimeout *model.Duration `yaml:"resolve_timeout,omitempty" json:"resolve_timeout,omitempty"` + + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + + SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` + SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` + SMTPSmarthost config.HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` + SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` + SMTPAuthPassword string `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` + SMTPAuthPasswordFile string `yaml:"smtp_auth_password_file,omitempty" json:"smtp_auth_password_file,omitempty"` + SMTPAuthSecret string `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` + SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` + SMTPRequireTLS *bool `yaml:"smtp_require_tls,omitempty" json:"smtp_require_tls,omitempty"` + SMTPTLSConfig *TLSConfig `yaml:"smtp_tls_config,omitempty" json:"smtp_tls_config,omitempty"` + SlackAPIURL *config.URL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` + SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"` + PagerdutyURL *config.URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` + HipchatAPIURL *config.URL `yaml:"hipchat_api_url,omitempty" json:"hipchat_api_url,omitempty"` + HipchatAuthToken string `yaml:"hipchat_auth_token,omitempty" json:"hipchat_auth_token,omitempty"` + OpsGenieAPIURL *config.URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` + OpsGenieAPIKey string `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` + OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"` + WeChatAPIURL *config.URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` + WeChatAPISecret string `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` + WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` + VictorOpsAPIURL *config.URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` + VictorOpsAPIKey string `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` + VictorOpsAPIKeyFile string `yaml:"victorops_api_key_file,omitempty" json:"victorops_api_key_file,omitempty"` + TelegramAPIURL *config.URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` + WebexAPIURL *config.URL `yaml:"webex_api_url,omitempty" json:"webex_api_url,omitempty"` + JiraAPIURL *config.URL `yaml:"jira_api_url,omitempty" json:"jira_api_url,omitempty"` + RocketChatAPIURL *config.URL `yaml:"rocketchat_api_url,omitempty" json:"rocketchat_api_url,omitempty"` + RocketChatToken string `yaml:"rocketchat_token,omitempty" json:"rocketchat_token,omitempty"` + RocketChatTokenFile string `yaml:"rocketchat_token_file,omitempty" json:"rocketchat_token_file,omitempty"` + RocketChatTokenID string `yaml:"rocketchat_token_id,omitempty" json:"rocketchat_token_id,omitempty"` + RocketChatTokenIDFile string `yaml:"rocketchat_token_id_file,omitempty" json:"rocketchat_token_id_file,omitempty"` +} + +type Route struct { + Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` + GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"` + Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` + MatchRE map[string]string `yaml:"match_re,omitempty" json:"match_re,omitempty"` + Matchers []string `yaml:"matchers,omitempty" json:"matchers,omitempty"` + Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` + Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` + GroupWait string `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` + GroupInterval string `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` + RepeatInterval string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"` + MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` + ActiveTimeIntervals []string `yaml:"active_time_intervals,omitempty" json:"active_time_intervals,omitempty"` +} + +type InhibitRule struct { + TargetMatch map[string]string `yaml:"target_match,omitempty" json:"target_match,omitempty"` + TargetMatchRE map[string]string `yaml:"target_match_re,omitempty" json:"target_match_re,omitempty"` + TargetMatchers []string `yaml:"target_matchers,omitempty" json:"target_matchers,omitempty"` + SourceMatch map[string]string `yaml:"source_match,omitempty" json:"source_match,omitempty"` + SourceMatchRE map[string]string `yaml:"source_match_re,omitempty" json:"source_match_re,omitempty"` + SourceMatchers []string `yaml:"source_matchers,omitempty" json:"source_matchers,omitempty"` + Equal []string `yaml:"equal,omitempty" json:"equal,omitempty"` +} + +type Receiver struct { + Name string `yaml:"name" json:"name"` + OpsgenieConfigs []*OpsgenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"` + PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` + SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` + WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` + WeChatConfigs []*WeChatConfig `yaml:"wechat_configs,omitempty" json:"wechat_config,omitempty"` + EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` + PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` + VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` + SNSConfigs []*SNSConfig `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"` + TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"` + DiscordConfigs []*DiscordConfig `yaml:"discord_configs,omitempty"` + WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty"` + MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty"` + MSTeamsV2Configs []*MSTeamsV2Config `yaml:"msteamsv2_configs,omitempty"` + JiraConfigs []*JiraConfig `yaml:"jira_configs,omitempty"` + RocketChatConfigs []*RocketChatConfig `yaml:"rocketchat_configs,omitempty"` +} + +type WebhookConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + URL string `yaml:"url,omitempty" json:"url,omitempty"` + URLFile string `yaml:"url_file,omitempty" json:"url_file,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + MaxAlerts int32 `yaml:"max_alerts,omitempty" json:"max_alerts,omitempty"` + Timeout *model.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` +} + +type PagerdutyConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + ServiceKey string `yaml:"service_key,omitempty" json:"service_key,omitempty"` + ServiceKeyFile string `yaml:"service_key_file,omitempty" json:"service_key_file,omitempty"` + RoutingKey string `yaml:"routing_key,omitempty" json:"routing_key,omitempty"` + RoutingKeyFile string `yaml:"routing_key_file,omitempty" json:"routing_key_file,omitempty"` + URL string `yaml:"url,omitempty" json:"url,omitempty"` + Client string `yaml:"client,omitempty" json:"client,omitempty"` + ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"` + Images []PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"` + Links []PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"` + Severity string `yaml:"severity,omitempty" json:"severity,omitempty"` + Class string `yaml:"class,omitempty" json:"class,omitempty"` + Component string `yaml:"component,omitempty" json:"component,omitempty"` + Group string `yaml:"group,omitempty" json:"group,omitempty"` + Source string `yaml:"source,omitempty" json:"source,omitempty"` +} + +type OpsgenieConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"` + APIKeyFile string `yaml:"api_key_file,omitempty" json:"api_key_file,omitempty"` + APIURL string `yaml:"api_url,omitempty" json:"api_url,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Source string `yaml:"source,omitempty" json:"source,omitempty"` + Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"` + Responders []OpsgenieResponder `yaml:"responders,omitempty" json:"responders,omitempty"` + Tags string `yaml:"tags,omitempty" json:"tags,omitempty"` + Note string `yaml:"note,omitempty" json:"note,omitempty"` + Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` + UpdateAlerts *bool `yaml:"update_alerts,omitempty" json:"update_alerts,omitempty"` + Entity string `yaml:"entity,omitempty" json:"entity,omitempty"` + Actions string `yaml:"actions,omitempty" json:"actions,omitempty"` +} + +type WeChatConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + APISecret string `yaml:"api_secret,omitempty" json:"api_secret,omitempty"` + APIURL string `yaml:"api_url,omitempty" json:"api_url,omitempty"` + CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"` + AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"` + ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"` + ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"` + ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` +} + +type SlackConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + APIURL string `yaml:"api_url,omitempty" json:"api_url,omitempty"` + APIURLFile string `yaml:"api_url_file,omitempty" json:"api_url_file,omitempty"` + Channel string `yaml:"channel,omitempty" json:"channel,omitempty"` + Username string `yaml:"username,omitempty" json:"username,omitempty"` + Color string `yaml:"color,omitempty" json:"color,omitempty"` + Title string `yaml:"title,omitempty" json:"title,omitempty"` + TitleLink string `yaml:"title_link,omitempty" json:"title_link,omitempty"` + Pretext string `yaml:"pretext,omitempty" json:"pretext,omitempty"` + Text string `yaml:"text,omitempty" json:"text,omitempty"` + Fields []SlackField `yaml:"fields,omitempty" json:"fields,omitempty"` + ShortFields bool `yaml:"short_fields,omitempty" json:"short_fields,omitempty"` + Footer string `yaml:"footer,omitempty" json:"footer,omitempty"` + Fallback string `yaml:"fallback,omitempty" json:"fallback,omitempty"` + CallbackID string `yaml:"callback_id,omitempty" json:"callback_id,omitempty"` + IconEmoji string `yaml:"icon_emoji,omitempty" json:"icon_emoji,omitempty"` + IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"` + ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"` + ThumbURL string `yaml:"thumb_url,omitempty" json:"thumb_url,omitempty"` + LinkNames bool `yaml:"link_names,omitempty" json:"link_names,omitempty"` + MrkdwnIn []string `yaml:"mrkdwn_in,omitempty" json:"mrkdwn_in,omitempty"` + Actions []SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"` +} + +type HTTPClientConfig struct { + Authorization *Authorization `yaml:"authorization,omitempty"` + BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"` + OAuth2 *OAuth2 `yaml:"oauth2,omitempty"` + BearerToken string `yaml:"bearer_token,omitempty"` + BearerTokenFile string `yaml:"bearer_token_file,omitempty"` + TLSConfig *TLSConfig `yaml:"tls_config,omitempty"` + FollowRedirects *bool `yaml:"follow_redirects,omitempty"` + EnableHTTP2 *bool `yaml:"enable_http2,omitempty"` + + ProxyConfig `yaml:",inline"` +} + +type ProxyConfig struct { + ProxyURL string `yaml:"proxy_url,omitempty"` + NoProxy string `yaml:"no_proxy,omitempty"` + ProxyFromEnvironment bool `yaml:"proxy_from_environment,omitempty"` + ProxyConnectHeader map[string][]string `yaml:"proxy_connect_header,omitempty"` +} + +type TLSConfig struct { + CAFile string `yaml:"ca_file,omitempty"` + CertFile string `yaml:"cert_file,omitempty"` + KeyFile string `yaml:"key_file,omitempty"` + ServerName string `yaml:"server_name,omitempty"` + InsecureSkipVerify bool `yaml:"insecure_skip_verify"` + MinVersion string `yaml:"min_version,omitempty"` + MaxVersion string `yaml:"max_version,omitempty"` +} + +type Authorization struct { + Type string `yaml:"type,omitempty"` + Credentials string `yaml:"credentials,omitempty"` + CredentialsFile string `yaml:"credentials_file,omitempty"` +} + +type BasicAuth struct { + Username string `yaml:"username"` + Password string `yaml:"password,omitempty"` + PasswordFile string `yaml:"password_file,omitempty"` +} + +type OAuth2 struct { + ClientID string `yaml:"client_id"` + ClientSecret string `yaml:"client_secret"` + ClientSecretFile string `yaml:"client_secret_file,omitempty"` + Scopes []string `yaml:"scopes,omitempty"` + TokenURL string `yaml:"token_url"` + EndpointParams map[string]string `yaml:"endpoint_params,omitempty"` + ProxyConfig `yaml:",inline"` + + TLSConfig *TLSConfig `yaml:"tls_config,omitempty"` +} + +type PagerdutyLink struct { + Href string `yaml:"href,omitempty" json:"href,omitempty"` + Text string `yaml:"text,omitempty" json:"text,omitempty"` +} + +type PagerdutyImage struct { + Src string `yaml:"src,omitempty" json:"src,omitempty"` + Alt string `yaml:"alt,omitempty" json:"alt,omitempty"` + Href string `yaml:"href,omitempty" json:"href,omitempty"` +} + +type OpsgenieResponder struct { + ID string `yaml:"id,omitempty" json:"id,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Username string `yaml:"username,omitempty" json:"username,omitempty"` + Type string `yaml:"type,omitempty" json:"type,omitempty"` +} + +type SlackField struct { + Title string `yaml:"title,omitempty" json:"title,omitempty"` + Value string `yaml:"value,omitempty" json:"value,omitempty"` + Short bool `yaml:"short,omitempty" json:"short,omitempty"` +} + +type SlackAction struct { + Type string `yaml:"type,omitempty" json:"type,omitempty"` + Text string `yaml:"text,omitempty" json:"text,omitempty"` + URL string `yaml:"url,omitempty" json:"url,omitempty"` + Style string `yaml:"style,omitempty" json:"style,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Value string `yaml:"value,omitempty" json:"value,omitempty"` + ConfirmField *SlackConfirmationField `yaml:"confirm,omitempty" json:"confirm,omitempty"` +} + +type SlackConfirmationField struct { + Text string `yaml:"text,omitempty" json:"text,omitempty"` + Title string `yaml:"title,omitempty" json:"title,omitempty"` + OkText string `yaml:"ok_text,omitempty" json:"ok_text,omitempty"` + DismissText string `yaml:"dismiss_text,omitempty" json:"dismiss_text,omitempty"` +} + +type EmailConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + To string `yaml:"to,omitempty" json:"to,omitempty"` + From string `yaml:"from,omitempty" json:"from,omitempty"` + Hello string `yaml:"hello,omitempty" json:"hello,omitempty"` + Smarthost config.HostPort `yaml:"smarthost,omitempty" json:"smarthost,omitempty"` + AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"` + AuthPassword string `yaml:"auth_password,omitempty" json:"auth_password,omitempty"` + AuthPasswordFile string `yaml:"auth_password_file,omitempty" json:"auth_password_file,omitempty"` + AuthSecret string `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"` + AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` + HTML *string `yaml:"html,omitempty" json:"html,omitempty"` + Text *string `yaml:"text,omitempty" json:"text,omitempty"` + RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"` + TLSConfig *TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` +} + +type PushoverConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + UserKey string `yaml:"user_key,omitempty" json:"user_key,omitempty"` + UserKeyFile string `yaml:"user_key_file,omitempty" json:"user_key_file,omitempty"` + Token string `yaml:"token,omitempty" json:"token,omitempty"` + TokenFile string `yaml:"token_file,omitempty" json:"token_file,omitempty"` + Title string `yaml:"title,omitempty" json:"title,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + URL string `yaml:"url,omitempty" json:"url,omitempty"` + URLTitle string `yaml:"url_title,omitempty" json:"url_title,omitempty"` + TTL string `yaml:"ttl,omitempty" json:"ttl,omitempty"` + Device string `yaml:"device,omitempty" json:"device,omitempty"` + Sound string `yaml:"sound,omitempty" json:"sound,omitempty"` + Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` + Retry *model.Duration `yaml:"retry,omitempty" json:"retry,omitempty"` + Expire *model.Duration `yaml:"expire,omitempty" json:"expire,omitempty"` + HTML bool `yaml:"html,omitempty" json:"html,omitempty"` +} + +type SNSConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + APIUrl string `yaml:"api_url,omitempty" json:"api_url,omitempty"` + Sigv4 SigV4Config `yaml:"sigv4,omitempty" json:"sigv4,omitempty"` + TopicARN string `yaml:"topic_arn,omitempty" json:"topic_arn,omitempty"` + PhoneNumber string `yaml:"phone_number,omitempty" json:"phone_number,omitempty"` + TargetARN string `yaml:"target_arn,omitempty" json:"target_arn,omitempty"` + Subject string `yaml:"subject,omitempty" json:"subject,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + Attributes map[string]string `yaml:"attributes,omitempty" json:"attributes,omitempty"` +} + +type TelegramConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + APIUrl string `yaml:"api_url,omitempty" json:"api_url,omitempty"` + BotToken string `yaml:"bot_token,omitempty" json:"bot_token,omitempty"` + BotTokenFile string `yaml:"bot_token_file,omitempty" json:"bot_token_file,omitempty"` + ChatID int64 `yaml:"chat_id,omitempty" json:"chat_id,omitempty"` + MessageThreadID int `yaml:"message_thread_id,omitempty" json:"message_thread_id,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + DisableNotifications bool `yaml:"disable_notifications,omitempty" json:"disable_notifications,omitempty"` + ParseMode string `yaml:"parse_mode,omitempty" json:"parse_mode,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` +} + +type DiscordConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty"` + WebhookURL string `yaml:"webhook_url,omitempty"` + Title string `yaml:"title,omitempty"` + Message string `yaml:"message,omitempty"` + Content string `yaml:"content,omitempty"` + Username string `yaml:"username,omitempty"` + AvatarURL string `yaml:"avatar_url,omitempty"` +} + +type WebexConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty"` + APIURL string `yaml:"api_url,omitempty"` + Message string `yaml:"message,omitempty"` + RoomID string `yaml:"room_id"` +} + +type SigV4Config struct { + Region string `yaml:"region,omitempty" json:"region,omitempty"` + AccessKey string `yaml:"access_key,omitempty" json:"access_key,omitempty"` + SecretKey string `yaml:"secret_key,omitempty" json:"secret_key,omitempty"` + Profile string `yaml:"profile,omitempty" json:"profile,omitempty"` + RoleARN string `yaml:"role_arn,omitempty" json:"role_arn,omitempty"` +} + +type VictorOpsConfig struct { + VSendResolved *bool `yaml:"send_resolved,omitempty" json:"send_resolved,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"` + APIKeyFile string `yaml:"api_key_file,omitempty" json:"api_key_file,omitempty"` + APIURL string `yaml:"api_url,omitempty" json:"api_url,omitempty"` + RoutingKey string `yaml:"routing_key,omitempty" json:"routing_key,omitempty"` + MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"` + StateMessage string `yaml:"state_message,omitempty" json:"state_message,omitempty"` + EntityDisplayName string `yaml:"entity_display_name,omitempty" json:"entity_display_name,omitempty"` + MonitoringTool string `yaml:"monitoring_tool,omitempty" json:"monitoring_tool,omitempty"` + CustomFields map[string]string `yaml:"custom_fields,omitempty" json:"custom_fields,omitempty"` +} + +type MSTeamsConfig struct { + SendResolved *bool `yaml:"send_resolved,omitempty"` + WebhookURL string `yaml:"webhook_url"` + Title string `yaml:"title,omitempty"` + Summary string `yaml:"summary,omitempty"` + Text string `yaml:"text,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty"` +} + +type MSTeamsV2Config struct { + SendResolved *bool `yaml:"send_resolved,omitempty"` + WebhookURL string `yaml:"webhook_url,omitempty"` + WebhookURLFile string `yaml:"webhook_url_file,omitempty"` + Title string `yaml:"title,omitempty"` + Text string `yaml:"text,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty"` +} + +type JiraConfig struct { + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty"` + SendResolved *bool `yaml:"send_resolved,omitempty"` + APIURL string `yaml:"api_url,omitempty"` + Project string `yaml:"project,omitempty"` + Summary string `yaml:"summary,omitempty"` + Description string `yaml:"description,omitempty"` + Labels []string `yaml:"labels,omitempty"` + Priority string `yaml:"priority,omitempty"` + IssueType string `yaml:"issue_type,omitempty"` + ReopenTransition string `yaml:"reopen_transition,omitempty"` + ResolveTransition string `yaml:"resolve_transition,omitempty"` + WontFixResolution string `yaml:"wont_fix_resolution,omitempty"` + ReopenDuration model.Duration `yaml:"reopen_duration,omitempty"` + Fields map[string]any `yaml:"fields,omitempty"` +} + +type RocketchatAttachmentField struct { + Short *bool `yaml:"short"` + Title string `yaml:"title,omitempty"` + Value string `yaml:"value,omitempty"` +} + +type RocketchatAttachmentAction struct { + Type string `yaml:"type,omitempty"` + Text string `yaml:"text,omitempty"` + URL string `yaml:"url,omitempty"` + ImageURL string `yaml:"image_url,omitempty"` + IsWebView bool `yaml:"is_webview"` + WebviewHeightRatio string `yaml:"webview_height_ratio,omitempty"` + Msg string `yaml:"msg,omitempty"` + MsgInChatWindow bool `yaml:"msg_in_chat_window"` + MsgProcessingType string `yaml:"msg_processing_type,omitempty"` +} + +type RocketChatConfig struct { + SendResolved *bool `yaml:"send_resolved,omitempty"` + HTTPConfig *HTTPClientConfig `yaml:"http_config,omitempty"` + APIURL string `yaml:"api_url,omitempty"` + TokenID *string `yaml:"token_id,omitempty"` + TokenIDFile string `yaml:"token_id_file,omitempty"` + Token *string `yaml:"token,omitempty"` + TokenFile string `yaml:"token_file,omitempty"` + // RocketChat channel override, (like #other-channel or @username). + Channel string `yaml:"channel,omitempty"` + Color string `yaml:"color,omitempty"` + Title string `yaml:"title,omitempty"` + TitleLink string `yaml:"title_link,omitempty"` + Text string `yaml:"text,omitempty"` + Fields []*RocketchatAttachmentField `yaml:"fields,omitempty"` + ShortFields bool `yaml:"short_fields"` + Emoji string `yaml:"emoji,omitempty"` + IconURL string `yaml:"icon_url,omitempty"` + ImageURL string `yaml:"image_url,omitempty"` + ThumbURL string `yaml:"thumb_url,omitempty"` + LinkNames bool `yaml:"link_names"` + Actions []*RocketchatAttachmentAction `yaml:"actions,omitempty"` +} + +type TimeInterval config.TimeInterval diff --git a/internal/mimir/client/alerts.go b/internal/mimir/client/alerts.go index d54b3802d39..e9f7b339231 100644 --- a/internal/mimir/client/alerts.go +++ b/internal/mimir/client/alerts.go @@ -3,8 +3,8 @@ package client import ( "context" + alertmgr_cfg "github.com/grafana/alloy/internal/mimir/alertmanager" "github.com/grafana/alloy/internal/runtime/logging/level" - alertmgr_cfg "github.com/prometheus/alertmanager/config" "gopkg.in/yaml.v3" ) @@ -17,11 +17,13 @@ type configCompat struct { } func (r *MimirClient) CreateAlertmanagerConfigs(ctx context.Context, conf *alertmgr_cfg.Config, templateFiles map[string]string) error { - // If we don't set this, secrets will be obfuscated by being set to "". - alertmgr_cfg.MarshalSecretValue = true + confStr, err := conf.String() + if err != nil { + return err + } payload := configCompat{ - AlertmanagerConfig: conf.String(), + AlertmanagerConfig: confStr, TemplateFiles: templateFiles, } payloadStr, err := yaml.Marshal(&payload) diff --git a/internal/mimir/client/alerts_test.go b/internal/mimir/client/alerts_test.go index d6dc3599acc..7acc25073b5 100644 --- a/internal/mimir/client/alerts_test.go +++ b/internal/mimir/client/alerts_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/go-kit/log" - alertmgr_cfg "github.com/prometheus/alertmanager/config" + alertmgr_cfg "github.com/grafana/alloy/internal/mimir/alertmanager" "github.com/stretchr/testify/require" ) @@ -36,9 +36,11 @@ func TestMimirClient_CreateAlertmanagerConfigs(t *testing.T) { // This Alertmanager config was copied from: // https://github.com/prometheus/alertmanager/blob/v0.28.1/config/testdata/conf.good.yml - config, err := alertmgr_cfg.LoadFile("testdata/alertmanager/conf.good.yml") + configBytes, err := os.ReadFile("testdata/alertmanager/conf.good.yml") + require.NoError(t, err) + + config, err := alertmgr_cfg.Unmarshal(configBytes) require.NoError(t, err) - require.NotNil(t, config) templateFiles := map[string]string{ "template1.tmpl": "{{ range .Alerts }}Alert: {{ .Summary }}{{ end }}", @@ -62,5 +64,6 @@ func TestMimirClient_CreateAlertmanagerConfigs(t *testing.T) { require.NoError(t, err) expectedResponse := string(expectedResponseBytes) - require.Equal(t, expectedResponse, string(body)) + actualResponse := string(body) + require.YAMLEq(t, expectedResponse, actualResponse) } diff --git a/internal/mimir/client/client.go b/internal/mimir/client/client.go index 1d901153a2c..f131ff7e883 100644 --- a/internal/mimir/client/client.go +++ b/internal/mimir/client/client.go @@ -12,11 +12,11 @@ import ( "strings" "github.com/go-kit/log" + alertmgr_cfg "github.com/grafana/alloy/internal/mimir/alertmanager" "github.com/grafana/alloy/internal/mimir/client/internal" "github.com/grafana/alloy/internal/useragent" "github.com/grafana/dskit/instrument" "github.com/grafana/dskit/user" - alertmgr_cfg "github.com/prometheus/alertmanager/config" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/config" ) diff --git a/internal/mimir/client/testdata/alertmanager/conf.good.yml b/internal/mimir/client/testdata/alertmanager/conf.good.yml index 2429f7961cd..f3bb211e1e6 100644 --- a/internal/mimir/client/testdata/alertmanager/conf.good.yml +++ b/internal/mimir/client/testdata/alertmanager/conf.good.yml @@ -1,5 +1,4 @@ global: - # The smarthost and SMTP sender used for mail notifications. smtp_smarthost: 'localhost:25' smtp_from: 'alertmanager@example.org' smtp_auth_username: 'alertmanager' @@ -7,43 +6,21 @@ global: smtp_hello: "host.example.org" slack_api_url: "http://mysecret.example.com/" http_config: + follow_redirects: true + enable_http2: true proxy_url: 'http://127.0.0.1:1025' -# The directory from which notification templates are read. templates: - '/etc/alertmanager/template/*.tmpl' -# The root route on which each incoming alert enters. route: - # The labels by which incoming alerts are grouped together. For example, - # multiple alerts coming in for cluster=A and alertname=LatencyHigh would - # be batched into a single group. group_by: ['alertname', 'cluster', 'service'] - # When a new group of alerts is created by an incoming alert, wait at - # least 'group_wait' to send the initial notification. - # This way ensures that you get multiple alerts for the same group that start - # firing shortly after another are batched together on the first - # notification. group_wait: 30s - # When the first notification was sent, wait 'group_interval' to send a batch - # of new alerts that started firing for that group. group_interval: 5m - # If an alert has successfully been sent, wait 'repeat_interval' to - # resend them. repeat_interval: 3h - # A default receiver receiver: team-X-mails - # All the above attributes are inherited by all child routes and can - # overwritten on each. - - # The child route trees. routes: - # This routes performs a regular expression match on alert labels to - # catch alerts that are related to a list of services. - match_re: service: ^(foo1|foo2|baz)$ receiver: team-X-mails - # The service has a sub-route for critical alerts, any alerts - # that do not match, i.e. severity != critical, fall-back to the - # parent node and are sent to 'team-X-mails' routes: - match: severity: critical @@ -55,12 +32,9 @@ route: - match: severity: critical receiver: team-Y-pager - # This route handles all alerts coming from a database service. If there's - # no team to handle it, it defaults to the DB team. - match: service: database receiver: team-DB-pager - # Also group alerts by affected database. group_by: [alertname, cluster, database] routes: - match: @@ -71,16 +45,11 @@ route: owner: team-Y receiver: team-Y-pager # continue: true -# Inhibition rules allow to mute a set of alerts given that another alert is -# firing. -# We use this to mute any warning-level notifications if the same alert is -# already critical. inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' - # Apply inhibition if the alertname is the same. equal: ['alertname', 'cluster', 'service'] receivers: - name: 'team-X-mails' diff --git a/internal/mimir/client/testdata/alertmanager/response.good.yml b/internal/mimir/client/testdata/alertmanager/response.good.yml index 0c96af8d1f2..791e70a2da8 100644 --- a/internal/mimir/client/testdata/alertmanager/response.good.yml +++ b/internal/mimir/client/testdata/alertmanager/response.good.yml @@ -3,7 +3,6 @@ template_files: template2.tmpl: '{{ .CommonLabels.alertname }}' alertmanager_config: | global: - resolve_timeout: 5m http_config: follow_redirects: true enable_http2: true @@ -15,43 +14,28 @@ alertmanager_config: | smtp_auth_password: |- multiline mysecret - smtp_require_tls: true - smtp_tls_config: - insecure_skip_verify: false slack_api_url: http://mysecret.example.com/ - pagerduty_url: https://events.pagerduty.com/v2/enqueue - opsgenie_api_url: https://api.opsgenie.com/ - wechat_api_url: https://qyapi.weixin.qq.com/cgi-bin/ - victorops_api_url: https://alert.victorops.com/integrations/generic/20131114/alert/ - telegram_api_url: https://api.telegram.org - webex_api_url: https://webexapis.com/v1/messages - rocketchat_api_url: https://open.rocket.chat/ route: receiver: team-X-mails group_by: - alertname - cluster - service - continue: false routes: - receiver: team-X-mails match_re: service: ^(foo1|foo2|baz)$ - continue: false routes: - receiver: team-X-pager match: severity: critical - continue: false - receiver: team-Y-mails match: service: files - continue: false routes: - receiver: team-Y-pager match: severity: critical - continue: false - receiver: team-DB-pager group_by: - alertname @@ -59,7 +43,6 @@ alertmanager_config: | - database match: service: database - continue: false routes: - receiver: team-X-pager match: @@ -68,15 +51,14 @@ alertmanager_config: | - receiver: team-Y-pager match: owner: team-Y - continue: false group_wait: 30s group_interval: 5m repeat_interval: 3h inhibit_rules: - - source_match: - severity: critical - target_match: + - target_match: severity: warning + source_match: + severity: critical equal: - alertname - cluster @@ -84,166 +66,35 @@ alertmanager_config: | receivers: - name: team-X-mails email_configs: - - send_resolved: false - to: team-X+alerts@example.org - from: alertmanager@example.org - hello: host.example.org - smarthost: localhost:25 - auth_username: alertmanager - auth_password: |- - multiline - mysecret - html: '{{ template "email.default.html" . }}' - require_tls: true - tls_config: - insecure_skip_verify: false + - to: team-X+alerts@example.org - name: team-X-pager - email_configs: - - send_resolved: false - to: team-X+alerts-critical@example.org - from: alertmanager@example.org - hello: host.example.org - smarthost: localhost:25 - auth_username: alertmanager - auth_password: |- - multiline - mysecret - html: '{{ template "email.default.html" . }}' - require_tls: true - tls_config: - insecure_skip_verify: false pagerduty_configs: - - send_resolved: true - http_config: - follow_redirects: true - enable_http2: true - proxy_url: http://127.0.0.1:1025 - routing_key: mysecret - url: https://events.pagerduty.com/v2/enqueue - client: '{{ template "pagerduty.default.client" . }}' - client_url: '{{ template "pagerduty.default.clientURL" . }}' - description: '{{ template "pagerduty.default.description" .}}' - details: - firing: '{{ template "pagerduty.default.instances" .Alerts.Firing }}' - num_firing: '{{ .Alerts.Firing | len }}' - num_resolved: '{{ .Alerts.Resolved | len }}' - resolved: '{{ template "pagerduty.default.instances" .Alerts.Resolved }}' - source: '{{ template "pagerduty.default.client" . }}' + - routing_key: mysecret + email_configs: + - to: team-X+alerts-critical@example.org - name: team-Y-mails email_configs: - - send_resolved: false - to: team-Y+alerts@example.org - from: alertmanager@example.org - hello: host.example.org - smarthost: localhost:25 - auth_username: alertmanager - auth_password: |- - multiline - mysecret - html: '{{ template "email.default.html" . }}' - require_tls: true - tls_config: - insecure_skip_verify: false + - to: team-Y+alerts@example.org - name: team-Y-pager pagerduty_configs: - - send_resolved: true - http_config: - follow_redirects: true - enable_http2: true - proxy_url: http://127.0.0.1:1025 - routing_key: mysecret - url: https://events.pagerduty.com/v2/enqueue - client: '{{ template "pagerduty.default.client" . }}' - client_url: '{{ template "pagerduty.default.clientURL" . }}' - description: '{{ template "pagerduty.default.description" .}}' - details: - firing: '{{ template "pagerduty.default.instances" .Alerts.Firing }}' - num_firing: '{{ .Alerts.Firing | len }}' - num_resolved: '{{ .Alerts.Resolved | len }}' - resolved: '{{ template "pagerduty.default.instances" .Alerts.Resolved }}' - source: '{{ template "pagerduty.default.client" . }}' + - routing_key: mysecret - name: team-DB-pager pagerduty_configs: - - send_resolved: true - http_config: - follow_redirects: true - enable_http2: true - proxy_url: http://127.0.0.1:1025 - routing_key: mysecret - url: https://events.pagerduty.com/v2/enqueue - client: '{{ template "pagerduty.default.client" . }}' - client_url: '{{ template "pagerduty.default.clientURL" . }}' - description: '{{ template "pagerduty.default.description" .}}' - details: - firing: '{{ template "pagerduty.default.instances" .Alerts.Firing }}' - num_firing: '{{ .Alerts.Firing | len }}' - num_resolved: '{{ .Alerts.Resolved | len }}' - resolved: '{{ template "pagerduty.default.instances" .Alerts.Resolved }}' - source: '{{ template "pagerduty.default.client" . }}' + - routing_key: mysecret - name: victorOps-receiver victorops_configs: - - send_resolved: true - http_config: - follow_redirects: true - enable_http2: true - proxy_url: http://127.0.0.1:1025 - api_key: mysecret - api_url: https://alert.victorops.com/integrations/generic/20131114/alert/ + - api_key: mysecret routing_key: Sample_route - message_type: CRITICAL - state_message: '{{ template "victorops.default.state_message" . }}' - entity_display_name: '{{ template "victorops.default.entity_display_name" . }}' - monitoring_tool: '{{ template "victorops.default.monitoring_tool" . }}' - name: opsGenie-receiver opsgenie_configs: - - send_resolved: true - http_config: - follow_redirects: true - enable_http2: true - proxy_url: http://127.0.0.1:1025 - api_key: mysecret - api_url: https://api.opsgenie.com/ - message: '{{ template "opsgenie.default.message" . }}' - description: '{{ template "opsgenie.default.description" . }}' - source: '{{ template "opsgenie.default.source" . }}' + - api_key: mysecret - name: pushover-receiver pushover_configs: - - send_resolved: true - http_config: - follow_redirects: true - enable_http2: true - proxy_url: http://127.0.0.1:1025 - user_key: key + - user_key: key token: mysecret - title: '{{ template "pushover.default.title" . }}' - message: '{{ template "pushover.default.message" . }}' - url: '{{ template "pushover.default.url" . }}' - priority: '{{ if eq .Status "firing" }}2{{ else }}0{{ end }}' - retry: 1m0s - expire: 1h0m0s - html: false - name: slack-receiver slack_configs: - - send_resolved: false - http_config: - follow_redirects: true - enable_http2: true - proxy_url: http://127.0.0.1:1025 - api_url: http://mysecret.example.com/ - channel: '#my-channel' - username: '{{ template "slack.default.username" . }}' - color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}' - title: '{{ template "slack.default.title" . }}' - title_link: '{{ template "slack.default.titlelink" . }}' - pretext: '{{ template "slack.default.pretext" . }}' - text: '{{ template "slack.default.text" . }}' - short_fields: false - footer: '{{ template "slack.default.footer" . }}' - fallback: '{{ template "slack.default.fallback" . }}' - callback_id: '{{ template "slack.default.callbackid" . }}' - icon_emoji: '{{ template "slack.default.iconemoji" . }}' - icon_url: '{{ template "slack.default.iconurl" . }}' + - channel: '#my-channel' image_url: http://some.img.com/img.png - link_names: false templates: - /etc/alertmanager/template/*.tmpl From 58041e9812d141c9de7809ab82412ac2db10181d Mon Sep 17 00:00:00 2001 From: Paulin Todev Date: Fri, 21 Nov 2025 17:02:31 +0000 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/mimir/alertmanager/types.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/mimir/alertmanager/types.go b/internal/mimir/alertmanager/types.go index 792a0f697f0..72411a0d4a5 100644 --- a/internal/mimir/alertmanager/types.go +++ b/internal/mimir/alertmanager/types.go @@ -49,7 +49,7 @@ func (c Config) String() (string, error) { return string(confBytes), nil } -// This is a standard Unmarshal functioion. +// This is a standard Unmarshal function. // The benefit of having it here is that it's guaranteed to use the correct "gopkg.in/yaml.v2" package. func Unmarshal(data []byte) (*Config, error) { var c Config @@ -132,7 +132,7 @@ type Receiver struct { PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` - WeChatConfigs []*WeChatConfig `yaml:"wechat_configs,omitempty" json:"wechat_config,omitempty"` + WeChatConfigs []*WeChatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"` EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"`