From e6ce7b1120e3fc604fe92b2e921214a68960d96e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:52:38 +0000 Subject: [PATCH] fix(deps): update github.com/grafana/alerting digest to d49e2e0 --- go.mod | 2 +- go.sum | 4 +- .../alerting/notify/grafana_alertmanager.go | 55 +++++--- .../grafana/alerting/notify/receivers.go | 2 +- .../notify/stages/coordination_stage.go | 81 +++++++++++ .../grafana/alerting/notify/testing.go | 3 +- .../alerting/receivers/slack/config.go | 5 +- .../grafana/alerting/receivers/slack/slack.go | 2 +- .../alerting/receivers/slack/testing.go | 3 +- .../alerting/receivers/victorops/config.go | 4 +- .../alerting/receivers/victorops/testing.go | 5 + .../alerting/templates/default_template.go | 1 + .../alerting/templates/template_data.go | 132 ++++++++++-------- vendor/modules.txt | 3 +- 14 files changed, 220 insertions(+), 82 deletions(-) create mode 100644 vendor/github.com/grafana/alerting/notify/stages/coordination_stage.go diff --git a/go.mod b/go.mod index 705a726dfa8..2b3998fb29b 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,7 @@ require ( github.com/google/go-github/v57 v57.0.0 github.com/google/uuid v1.6.0 github.com/grafana-tools/sdk v0.0.0-20220919052116-6562121319fc - github.com/grafana/alerting v0.0.0-20250113170557-b4ab2ba363a8 + github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65 github.com/grafana/regexp v0.0.0-20240607082908-2cb410fa05da github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/vault/api v1.15.0 diff --git a/go.sum b/go.sum index 2c05c10b0f0..41aedda7b3d 100644 --- a/go.sum +++ b/go.sum @@ -1269,8 +1269,8 @@ github.com/gosimple/slug v1.1.1 h1:fRu/digW+NMwBIP+RmviTK97Ho/bEj/C9swrCspN3D4= github.com/gosimple/slug v1.1.1/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0= github.com/grafana-tools/sdk v0.0.0-20220919052116-6562121319fc h1:PXZQA2WCxe85Tnn+WEvr8fDpfwibmEPgfgFEaC87G24= github.com/grafana-tools/sdk v0.0.0-20220919052116-6562121319fc/go.mod h1:AHHlOEv1+GGQ3ktHMlhuTUwo3zljV3QJbC0+8o2kn+4= -github.com/grafana/alerting v0.0.0-20250113170557-b4ab2ba363a8 h1:mdI6P22PgFD7bQ0Yf4h8cfHSldak4nxogvlsTHZyZmc= -github.com/grafana/alerting v0.0.0-20250113170557-b4ab2ba363a8/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65 h1:dmsycYQzl5JexuV8UxQpT3B79maSvhiIahid4/tezAM= +github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/dskit v0.0.0-20250128193928-104df19e2080 h1:IHRsAMdemxPu9g9zPxTFcU3hLhfd5cl6W4fqRovAzkU= github.com/grafana/dskit v0.0.0-20250128193928-104df19e2080/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ= github.com/grafana/e2e v0.1.2-0.20240118170847-db90b84177fc h1:BW+LjKJDz0So5LI8UZfW5neWeKpSkWqhmGjQFzcFfLM= diff --git a/vendor/github.com/grafana/alerting/notify/grafana_alertmanager.go b/vendor/github.com/grafana/alerting/notify/grafana_alertmanager.go index 693dbff52c4..e62a57f04f9 100644 --- a/vendor/github.com/grafana/alerting/notify/grafana_alertmanager.go +++ b/vendor/github.com/grafana/alerting/notify/grafana_alertmanager.go @@ -18,8 +18,6 @@ import ( "github.com/go-openapi/strfmt" "golang.org/x/sync/errgroup" - "github.com/grafana/alerting/cluster" - "github.com/grafana/alerting/notify/nfstatus" amv2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/dispatch" @@ -37,6 +35,10 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" + "github.com/grafana/alerting/cluster" + "github.com/grafana/alerting/notify/nfstatus" + "github.com/grafana/alerting/notify/stages" + "github.com/grafana/alerting/models" "github.com/grafana/alerting/templates" ) @@ -107,6 +109,8 @@ type GrafanaAlertmanager struct { // templates contains the template name -> template contents for each user-defined template. templates []templates.TemplateDefinition + // controls whether instance runs pipeline.PipelineAndStateTimestampCoordinationStage and in what mode. The stage is run after notify.DedupStage + pipelineAndStateTimestampsMismatchAction stages.Action } // State represents any of the two 'states' of the alertmanager. Notification log or Silences. @@ -172,6 +176,9 @@ type GrafanaAlertmanagerConfig struct { Nflog MaintenanceOptions Limits Limits + + // PipelineAndStateTimestampsMismatchAction defines the action to take when there's a mismatch in pipeline and state timestamps. + PipelineAndStateTimestampsMismatchAction stages.Action } func (c *GrafanaAlertmanagerConfig) Validate() error { @@ -190,16 +197,17 @@ func (c *GrafanaAlertmanagerConfig) Validate() error { func NewGrafanaAlertmanager(tenantKey string, tenantID int64, config *GrafanaAlertmanagerConfig, peer ClusterPeer, logger log.Logger, m *GrafanaAlertmanagerMetrics) (*GrafanaAlertmanager, error) { // TODO: Remove the context. am := &GrafanaAlertmanager{ - stopc: make(chan struct{}), - logger: log.With(logger, "component", "alertmanager", tenantKey, tenantID), - marker: types.NewMarker(m.Registerer), - stageMetrics: notify.NewMetrics(m.Registerer, featurecontrol.NoopFlags{}), - dispatcherMetrics: dispatch.NewDispatcherMetrics(false, m.Registerer), - peer: peer, - peerTimeout: config.PeerTimeout, - Metrics: m, - tenantID: tenantID, - externalURL: config.ExternalURL, + stopc: make(chan struct{}), + logger: log.With(logger, "component", "alertmanager", tenantKey, tenantID), + marker: types.NewMarker(m.Registerer), + stageMetrics: notify.NewMetrics(m.Registerer, featurecontrol.NoopFlags{}), + dispatcherMetrics: dispatch.NewDispatcherMetrics(false, m.Registerer), + peer: peer, + peerTimeout: config.PeerTimeout, + Metrics: m, + tenantID: tenantID, + externalURL: config.ExternalURL, + pipelineAndStateTimestampsMismatchAction: config.PipelineAndStateTimestampsMismatchAction, } if err := config.Validate(); err != nil { @@ -663,13 +671,24 @@ func (am *GrafanaAlertmanager) ApplyConfig(cfg Configuration) (err error) { // Finally, build the integrations map using the receiver configuration and templates. apiReceivers := cfg.Receivers() + nameToReceiver := make(map[string]*APIReceiver, len(apiReceivers)) + for _, receiver := range apiReceivers { + if existing, ok := nameToReceiver[receiver.Name]; ok { + itypes := make([]string, 0, len(existing.GrafanaIntegrations.Integrations)) + for _, i := range existing.GrafanaIntegrations.Integrations { + itypes = append(itypes, i.Type) + } + level.Warn(am.logger).Log("msg", "receiver with same name is defined multiple times. Only the last one will be used", "receiver_name", receiver.Name, "overwritten_integrations", itypes) + } + nameToReceiver[receiver.Name] = receiver + } integrationsMap := make(map[string][]*Integration, len(apiReceivers)) - for _, apiReceiver := range apiReceivers { + for name, apiReceiver := range nameToReceiver { integrations, err := cfg.BuildReceiverIntegrationsFunc()(apiReceiver, tmpl) if err != nil { return err } - integrationsMap[apiReceiver.Name] = integrations + integrationsMap[name] = integrations } // Now, let's put together our notification pipeline @@ -698,7 +717,7 @@ func (am *GrafanaAlertmanager) ApplyConfig(cfg Configuration) (err error) { var receivers []*nfstatus.Receiver activeReceivers := GetActiveReceiversMap(am.route) for name := range integrationsMap { - stage := am.createReceiverStage(name, nfstatus.GetIntegrations(integrationsMap[name]), am.waitFunc, am.notificationLog) + stage := am.createReceiverStage(name, nfstatus.GetIntegrations(integrationsMap[name]), am.waitFunc, am.notificationLog, am.pipelineAndStateTimestampsMismatchAction) routingStage[name] = notify.MultiStage{meshStage, silencingStage, timeMuteStage, inhibitionStage, stage} _, isActive := activeReceivers[name] @@ -865,7 +884,7 @@ func (e AlertValidationError) Error() string { } // createReceiverStage creates a pipeline of stages for a receiver. -func (am *GrafanaAlertmanager) createReceiverStage(name string, integrations []*notify.Integration, wait func() time.Duration, notificationLog notify.NotificationLog) notify.Stage { +func (am *GrafanaAlertmanager) createReceiverStage(name string, integrations []*notify.Integration, wait func() time.Duration, notificationLog notify.NotificationLog, act stages.Action) notify.Stage { var fs notify.FanoutStage for i := range integrations { recv := &nflogpb.Receiver{ @@ -876,6 +895,10 @@ func (am *GrafanaAlertmanager) createReceiverStage(name string, integrations []* var s notify.MultiStage s = append(s, notify.NewWaitStage(wait)) s = append(s, notify.NewDedupStage(integrations[i], notificationLog, recv)) + stage := stages.NewPipelineAndStateTimestampCoordinationStage(notificationLog, recv, act) + if stage != nil { + s = append(s, stage) + } s = append(s, notify.NewRetryStage(integrations[i], name, am.stageMetrics)) s = append(s, notify.NewSetNotifiesStage(notificationLog, recv)) diff --git a/vendor/github.com/grafana/alerting/notify/receivers.go b/vendor/github.com/grafana/alerting/notify/receivers.go index de300802504..2229699fc12 100644 --- a/vendor/github.com/grafana/alerting/notify/receivers.go +++ b/vendor/github.com/grafana/alerting/notify/receivers.go @@ -395,7 +395,7 @@ func parseNotifier(ctx context.Context, result *GrafanaReceiverConfig, receiver } result.ThreemaConfigs = append(result.ThreemaConfigs, newNotifierConfig(receiver, cfg)) case "victorops": - cfg, err := victorops.NewConfig(receiver.Settings) + cfg, err := victorops.NewConfig(receiver.Settings, decryptFn) if err != nil { return err } diff --git a/vendor/github.com/grafana/alerting/notify/stages/coordination_stage.go b/vendor/github.com/grafana/alerting/notify/stages/coordination_stage.go new file mode 100644 index 00000000000..8bf2d2df649 --- /dev/null +++ b/vendor/github.com/grafana/alerting/notify/stages/coordination_stage.go @@ -0,0 +1,81 @@ +package stages + +import ( + "context" + "errors" + "fmt" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/alertmanager/nflog" + "github.com/prometheus/alertmanager/nflog/nflogpb" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/types" +) + +type Action string + +const ( + Disabled Action = "disabled" + LogOnly Action = "log-only" + StopPipeline Action = "stop-pipeline" +) + +type PipelineAndStateTimestampCoordinationStage struct { + nflog notify.NotificationLog + recv *nflogpb.Receiver + stopPipeline bool +} + +func NewPipelineAndStateTimestampCoordinationStage(l notify.NotificationLog, recv *nflogpb.Receiver, action Action) *PipelineAndStateTimestampCoordinationStage { + var stop bool + switch action { + case LogOnly: + stop = false + case StopPipeline: + stop = true + default: + return nil + } + return &PipelineAndStateTimestampCoordinationStage{ + nflog: l, + recv: recv, + stopPipeline: stop, + } +} + +// Exec implements the Stage interface. +func (n *PipelineAndStateTimestampCoordinationStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { + gkey, ok := notify.GroupKey(ctx) + if !ok { + return ctx, nil, errors.New("group key missing") + } + + entries, err := n.nflog.Query(nflog.QGroupKey(gkey), nflog.QReceiver(n.recv)) + if err != nil && !errors.Is(err, nflog.ErrNotFound) { + return ctx, nil, err + } + + var entry *nflogpb.Entry + switch len(entries) { + case 0: + return ctx, alerts, nil + case 1: + entry = entries[0] + default: + return ctx, nil, fmt.Errorf("unexpected entry result size %d", len(entries)) + } + + // get the tick time from the context. + timeNow, ok := notify.Now(ctx) + // now make sure that the current state is from past + if ok && entry.Timestamp.After(timeNow) { + diff := entry.Timestamp.Sub(timeNow) + // this usually means that the WaitStage took longer than the group_wait, and the subsequent node in the cluster sees the event from the first node + _ = level.Warn(l).Log("msg", "timestamp of notification log entry is after the current pipeline timestamp.", "entry_time", entry.Timestamp, "pipeline_time", timeNow, "diff", diff, "dropped", n.stopPipeline) + if n.stopPipeline { + return ctx, nil, nil + } + } + return ctx, alerts, nil +} diff --git a/vendor/github.com/grafana/alerting/notify/testing.go b/vendor/github.com/grafana/alerting/notify/testing.go index d4ccdf7f86c..f3e065ba032 100644 --- a/vendor/github.com/grafana/alerting/notify/testing.go +++ b/vendor/github.com/grafana/alerting/notify/testing.go @@ -186,7 +186,8 @@ var AllKnownConfigsForTesting = map[string]NotifierConfigTest{ Secrets: threema.FullValidSecretsForTesting, }, "victorops": {NotifierType: "victorops", - Config: victorops.FullValidConfigForTesting, + Config: victorops.FullValidConfigForTesting, + Secrets: victorops.FullValidSecretsForTesting, }, "webhook": {NotifierType: "webhook", Config: webhook.FullValidConfigForTesting, diff --git a/vendor/github.com/grafana/alerting/receivers/slack/config.go b/vendor/github.com/grafana/alerting/receivers/slack/config.go index edd82b4131b..de32eff9ee2 100644 --- a/vendor/github.com/grafana/alerting/receivers/slack/config.go +++ b/vendor/github.com/grafana/alerting/receivers/slack/config.go @@ -24,6 +24,7 @@ type Config struct { MentionChannel string `json:"mentionChannel,omitempty" yaml:"mentionChannel,omitempty"` MentionUsers receivers.CommaSeparatedStrings `json:"mentionUsers,omitempty" yaml:"mentionUsers,omitempty"` MentionGroups receivers.CommaSeparatedStrings `json:"mentionGroups,omitempty" yaml:"mentionGroups,omitempty"` + Color string `json:"color,omitempty" yaml:"color,omitempty"` } func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Config, error) { @@ -67,6 +68,8 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi if settings.Title == "" { settings.Title = templates.DefaultMessageTitleEmbed } - + if settings.Color == "" { + settings.Color = templates.DefaultMessageColor + } return settings, nil } diff --git a/vendor/github.com/grafana/alerting/receivers/slack/slack.go b/vendor/github.com/grafana/alerting/receivers/slack/slack.go index cd65cb2e35c..e4839f1e930 100644 --- a/vendor/github.com/grafana/alerting/receivers/slack/slack.go +++ b/vendor/github.com/grafana/alerting/receivers/slack/slack.go @@ -258,7 +258,7 @@ func (sn *Notifier) createSlackMessage(ctx context.Context, alerts []*types.Aler // https://api.slack.com/messaging/composing/layouts#when-to-use-attachments Attachments: []attachment{ { - Color: receivers.GetAlertStatusColor(types.Alerts(alerts...).Status()), + Color: tmpl(sn.settings.Color), Title: title, Fallback: title, Footer: "Grafana v" + sn.appVersion, diff --git a/vendor/github.com/grafana/alerting/receivers/slack/testing.go b/vendor/github.com/grafana/alerting/receivers/slack/testing.go index 0d7f13964c5..c448fe17673 100644 --- a/vendor/github.com/grafana/alerting/receivers/slack/testing.go +++ b/vendor/github.com/grafana/alerting/receivers/slack/testing.go @@ -13,7 +13,8 @@ const FullValidConfigForTesting = `{ "icon_url": "http://localhost/icon_url", "mentionChannel": "channel", "mentionUsers": "test-mentionUsers", - "mentionGroups": "test-mentionGroups" + "mentionGroups": "test-mentionGroups", + "color": "test-color" }` // FullValidSecretsForTesting is a string representation of JSON object that contains all fields that can be overridden from secrets diff --git a/vendor/github.com/grafana/alerting/receivers/victorops/config.go b/vendor/github.com/grafana/alerting/receivers/victorops/config.go index edfe8ff86d8..c0940fd5400 100644 --- a/vendor/github.com/grafana/alerting/receivers/victorops/config.go +++ b/vendor/github.com/grafana/alerting/receivers/victorops/config.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/grafana/alerting/receivers" "github.com/grafana/alerting/templates" ) @@ -20,12 +21,13 @@ type Config struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` } -func NewConfig(jsonData json.RawMessage) (Config, error) { +func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Config, error) { settings := Config{} err := json.Unmarshal(jsonData, &settings) if err != nil { return settings, fmt.Errorf("failed to unmarshal settings: %w", err) } + settings.URL = decryptFn("url", settings.URL) if settings.URL == "" { return settings, errors.New("could not find victorops url property in settings") } diff --git a/vendor/github.com/grafana/alerting/receivers/victorops/testing.go b/vendor/github.com/grafana/alerting/receivers/victorops/testing.go index e616a5b6c6d..66c8dc771d9 100644 --- a/vendor/github.com/grafana/alerting/receivers/victorops/testing.go +++ b/vendor/github.com/grafana/alerting/receivers/victorops/testing.go @@ -7,3 +7,8 @@ const FullValidConfigForTesting = `{ "title" :"test-title", "description" :"test-description" }` + +// FullValidSecretsForTesting is a string representation of JSON object that contains all fields that can be overridden from secrets +const FullValidSecretsForTesting = `{ + "url": "http://localhost-secret" +}` diff --git a/vendor/github.com/grafana/alerting/templates/default_template.go b/vendor/github.com/grafana/alerting/templates/default_template.go index 601a11d8dc7..ec6665c418f 100644 --- a/vendor/github.com/grafana/alerting/templates/default_template.go +++ b/vendor/github.com/grafana/alerting/templates/default_template.go @@ -9,6 +9,7 @@ import ( const ( DefaultMessageTitleEmbed = `{{ template "default.title" . }}` DefaultMessageEmbed = `{{ template "default.message" . }}` + DefaultMessageColor = `{{ if eq .Status "firing" }}#D63232{{ else }}#36a64f{{ end }}` ) var DefaultTemplateString = ` diff --git a/vendor/github.com/grafana/alerting/templates/template_data.go b/vendor/github.com/grafana/alerting/templates/template_data.go index f9b452624ec..db144c3678f 100644 --- a/vendor/github.com/grafana/alerting/templates/template_data.go +++ b/vendor/github.com/grafana/alerting/templates/template_data.go @@ -8,7 +8,6 @@ import ( "net/url" "path" "slices" - "sort" "strings" tmpltext "text/template" "time" @@ -173,89 +172,110 @@ func extendAlert(alert template.Alert, externalURL string, logger log.Logger) *E Fingerprint: alert.Fingerprint, } + if generatorURL, err := url.Parse(extended.GeneratorURL); err != nil { + level.Warn(logger).Log("msg", "failed to parse generator URL while extending template data", "url", extended.GeneratorURL, "error", err.Error()) + } else if orgID := alert.Annotations[models.OrgIDAnnotation]; len(orgID) > 0 { + // Refactor note: We only modify the URL if there is something to add. Otherwise, the original string is kept. + setQueryParam(generatorURL, "orgId", orgID) + extended.GeneratorURL = generatorURL.String() + } + + if alert.Annotations != nil { + if s, ok := alert.Annotations[models.ValuesAnnotation]; ok { + if err := json.Unmarshal([]byte(s), &extended.Values); err != nil { + level.Warn(logger).Log("msg", "failed to unmarshal values annotation", "error", err.Error()) + } + } + + // TODO: Remove in Grafana 12 + extended.ValueString = alert.Annotations[models.ValueStringAnnotation] + } + // fill in some grafana-specific urls if len(externalURL) == 0 { return extended } - u, err := url.Parse(externalURL) + baseURL, err := url.Parse(externalURL) if err != nil { - level.Debug(logger).Log("msg", "failed to parse external URL while extending template data", "url", externalURL, "error", err.Error()) - return extended - } - externalPath := u.Path - - generatorURL, err := url.Parse(extended.GeneratorURL) - if err != nil { - level.Debug(logger).Log("msg", "failed to parse generator URL while extending template data", "url", extended.GeneratorURL, "error", err.Error()) + level.Warn(logger).Log("msg", "failed to parse external URL while extending template data", "url", externalURL, "error", err.Error()) return extended } orgID := alert.Annotations[models.OrgIDAnnotation] if len(orgID) > 0 { - extended.GeneratorURL = setOrgIDQueryParam(generatorURL, orgID) + setQueryParam(baseURL, "orgId", orgID) } - dashboardUID := alert.Annotations[models.DashboardUIDAnnotation] - if len(dashboardUID) > 0 { - u.Path = path.Join(externalPath, "/d/", dashboardUID) - extended.DashboardURL = u.String() - panelID := alert.Annotations[models.PanelIDAnnotation] - if len(panelID) > 0 { - u.RawQuery = "viewPanel=" + panelID - extended.PanelURL = u.String() - } - dashboardURL, err := url.Parse(extended.DashboardURL) - if err != nil { - level.Debug(logger).Log("msg", "failed to parse dashboard URL while extending template data", "url", extended.DashboardURL, "error", err.Error()) - return extended - } - if len(orgID) > 0 { - extended.DashboardURL = setOrgIDQueryParam(dashboardURL, orgID) - extended.PanelURL = setOrgIDQueryParam(u, orgID) + if dashboardURL := generateDashboardURL(alert, *baseURL); dashboardURL != nil { + extended.DashboardURL = dashboardURL.String() + if panelURL := generatePanelURL(alert, *dashboardURL); panelURL != nil { + extended.PanelURL = panelURL.String() } } + if silenceURL := generateSilenceURL(alert, *baseURL); silenceURL != nil { + extended.SilenceURL = silenceURL.String() + } - if alert.Annotations != nil { - if s, ok := alert.Annotations[models.ValuesAnnotation]; ok { - if err := json.Unmarshal([]byte(s), &extended.Values); err != nil { - level.Warn(logger).Log("msg", "failed to unmarshal values annotation", "error", err.Error()) - } - } + return extended +} - // TODO: Remove in Grafana 10 - extended.ValueString = alert.Annotations[models.ValueStringAnnotation] +// generateDashboardURL generates a URL to the attached dashboard for the given alert in Grafana. Returns a new URL. +func generateDashboardURL(alert template.Alert, baseURL url.URL) *url.URL { + dashboardUID := alert.Annotations[models.DashboardUIDAnnotation] + if dashboardUID == "" { + return nil } - matchers := make([]string, 0) - for key, value := range alert.Labels { - if !(strings.HasPrefix(key, "__") && strings.HasSuffix(key, "__")) { - matchers = append(matchers, key+"="+value) - } + return baseURL.JoinPath("/d/", dashboardUID) +} + +// generatePanelURL generates a URL to the attached dashboard panel for a given alert in Grafana. Returns a new URL. +func generatePanelURL(alert template.Alert, dashboardURL url.URL) *url.URL { + panelID := alert.Annotations[models.PanelIDAnnotation] + if panelID == "" { + return nil } - sort.Strings(matchers) - u.Path = path.Join(externalPath, "/alerting/silence/new") + setQueryParam(&dashboardURL, "viewPanel", panelID) + + return &dashboardURL +} - query := make(url.Values) +// generateSilenceURL generates a URL to silence the given alert in Grafana. Returns a new URL. +func generateSilenceURL(alert template.Alert, baseURL url.URL) *url.URL { + silenceURL := baseURL.JoinPath("/alerting/silence/new") + + query := silenceURL.Query() query.Add("alertmanager", "grafana") - for _, matcher := range matchers { - query.Add("matcher", matcher) + + ruleUID := alert.Labels[models.RuleUIDLabel] + if ruleUID != "" { + query.Add("matcher", models.RuleUIDLabel+"="+ruleUID) } - u.RawQuery = query.Encode() - if len(orgID) > 0 { - extended.SilenceURL = setOrgIDQueryParam(u, orgID) - } else { - extended.SilenceURL = u.String() + for _, pair := range alert.Labels.SortedPairs() { + if strings.HasPrefix(pair.Name, "__") && strings.HasSuffix(pair.Name, "__") { + continue + } + + // If the alert has a rule uid available, it can more succinctly and accurately replace alertname + folder labels. + // In addition, using rule uid is more compatible with minimal permission RBAC users as they require the rule uid to silence. + if ruleUID != "" && (pair.Name == models.FolderTitleLabel || pair.Name == model.AlertNameLabel) { + continue + } + + query.Add("matcher", pair.Name+"="+pair.Value) } - return extended + + silenceURL.RawQuery = query.Encode() + + return silenceURL } -func setOrgIDQueryParam(url *url.URL, orgID string) string { +// setQueryParam sets the query parameter key to value in the given URL. Modifies the URL in place. +func setQueryParam(url *url.URL, key, value string) { q := url.Query() - q.Set("orgId", orgID) + q.Set(key, value) url.RawQuery = q.Encode() - - return url.String() } func ExtendData(data *Data, logger log.Logger) *ExtendedData { diff --git a/vendor/modules.txt b/vendor/modules.txt index 4044ea8b048..84faae87a46 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -610,7 +610,7 @@ github.com/gosimple/slug # github.com/grafana-tools/sdk v0.0.0-20220919052116-6562121319fc ## explicit; go 1.13 github.com/grafana-tools/sdk -# github.com/grafana/alerting v0.0.0-20250113170557-b4ab2ba363a8 +# github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65 ## explicit; go 1.22 github.com/grafana/alerting/cluster github.com/grafana/alerting/definition @@ -619,6 +619,7 @@ github.com/grafana/alerting/logging github.com/grafana/alerting/models github.com/grafana/alerting/notify github.com/grafana/alerting/notify/nfstatus +github.com/grafana/alerting/notify/stages github.com/grafana/alerting/receivers github.com/grafana/alerting/receivers/alertmanager github.com/grafana/alerting/receivers/dinding