@@ -114,6 +114,223 @@ func (s *SecretTemplateURL) UnmarshalJSON(data []byte) error {
114114 return nil
115115}
116116
117+ // applyGlobalReceiverTemplates merges global template defaults into each
118+ // receiver config, using the 3-tier priority:
119+ //
120+ // built-in default < global template override < per-receiver explicit value
121+ func applyGlobalReceiverTemplates (receivers []Receiver , global * GlobalConfig ) {
122+ var gt * GlobalReceiverTemplates
123+ if global != nil {
124+ gt = global .ReceiverTemplates
125+ }
126+
127+ for i := range receivers {
128+ r := & receivers [i ]
129+
130+ for j := range r .SlackConfigs {
131+ var slackGt * GlobalSlackTemplates
132+ if gt != nil {
133+ slackGt = gt .Slack
134+ }
135+ applySlackTemplates (r .SlackConfigs [j ], slackGt )
136+ }
137+
138+ for j := range r .EmailConfigs {
139+ var emailGt * GlobalEmailTemplates
140+ if gt != nil {
141+ emailGt = gt .Email
142+ }
143+ applyEmailTemplates (r .EmailConfigs [j ], emailGt )
144+ }
145+
146+ for j := range r .PagerdutyConfigs {
147+ var pdGt * GlobalPagerDutyTemplates
148+ if gt != nil {
149+ pdGt = gt .PagerDuty
150+ }
151+ applyPagerDutyTemplates (r .PagerdutyConfigs [j ], pdGt )
152+ }
153+
154+ for j := range r .OpsGenieConfigs {
155+ var ogGt * GlobalOpsGenieTemplates
156+ if gt != nil {
157+ ogGt = gt .OpsGenie
158+ }
159+ applyOpsGenieTemplates (r .OpsGenieConfigs [j ], ogGt )
160+ }
161+
162+ for j := range r .VictorOpsConfigs {
163+ var voGt * GlobalVictorOpsTemplates
164+ if gt != nil {
165+ voGt = gt .VictorOps
166+ }
167+ applyVictorOpsTemplates (r .VictorOpsConfigs [j ], voGt )
168+ }
169+
170+ for j := range r .PushoverConfigs {
171+ var poGt * GlobalPushoverTemplates
172+ if gt != nil {
173+ poGt = gt .Pushover
174+ }
175+ applyPushoverTemplates (r .PushoverConfigs [j ], poGt )
176+ }
177+
178+ for j := range r .WechatConfigs {
179+ var wcGt * GlobalWeChatTemplates
180+ if gt != nil {
181+ wcGt = gt .WeChat
182+ }
183+ applyWeChatTemplates (r .WechatConfigs [j ], wcGt )
184+ }
185+
186+ for j := range r .TelegramConfigs {
187+ var tcGt * GlobalTelegramTemplates
188+ if gt != nil {
189+ tcGt = gt .Telegram
190+ }
191+ applyTelegramTemplates (r .TelegramConfigs [j ], tcGt )
192+ }
193+ }
194+ }
195+
196+ func applySlackTemplates (sc * SlackConfig , gt * GlobalSlackTemplates ) {
197+ if gt != nil {
198+ applyGlobalTemplateOverride (& sc .Username , gt .Username , DefaultSlackConfig .Username )
199+ applyGlobalTemplateOverride (& sc .IconEmoji , gt .IconEmoji , DefaultSlackConfig .IconEmoji )
200+ applyGlobalTemplateOverride (& sc .IconURL , gt .IconURL , DefaultSlackConfig .IconURL )
201+ applyGlobalTemplateOverride (& sc .Pretext , gt .Pretext , DefaultSlackConfig .Pretext )
202+ applyGlobalTemplateOverride (& sc .Title , gt .Title , DefaultSlackConfig .Title )
203+ applyGlobalTemplateOverride (& sc .TitleLink , gt .TitleLink , DefaultSlackConfig .TitleLink )
204+ applyGlobalTemplateOverride (& sc .Text , gt .Text , DefaultSlackConfig .Text )
205+ applyGlobalTemplateOverride (& sc .Fallback , gt .Fallback , DefaultSlackConfig .Fallback )
206+ applyGlobalTemplateOverride (& sc .Footer , gt .Footer , DefaultSlackConfig .Footer )
207+ applyGlobalTemplateOverride (& sc .Color , gt .Color , DefaultSlackConfig .Color )
208+ }
209+ }
210+
211+ func applyEmailTemplates (ec * EmailConfig , gt * GlobalEmailTemplates ) {
212+ if gt != nil {
213+ if gt .Subject != "" {
214+ if ec .Headers == nil {
215+ ec .Headers = make (map [string ]string )
216+ }
217+ if val , ok := ec .Headers ["Subject" ]; ! ok || val == DefaultEmailSubject {
218+ ec .Headers ["Subject" ] = gt .Subject
219+ }
220+ }
221+ applyGlobalTemplateOverride (& ec .HTML , gt .HTML , DefaultEmailConfig .HTML )
222+ applyGlobalTemplateOverride (& ec .Text , gt .Text , DefaultEmailConfig .Text )
223+ }
224+ }
225+
226+ func applyPagerDutyTemplates (pc * PagerdutyConfig , gt * GlobalPagerDutyTemplates ) {
227+ if gt == nil {
228+ return
229+ }
230+
231+ applyGlobalTemplateOverride (& pc .Description , gt .Description , DefaultPagerdutyConfig .Description )
232+ applyGlobalTemplateOverride (& pc .Client , gt .Client , DefaultPagerdutyConfig .Client )
233+ applyGlobalTemplateOverride (& pc .ClientURL , gt .ClientURL , DefaultPagerdutyConfig .ClientURL )
234+
235+ // Merge global template overrides into the receiver `details` map.
236+ //
237+ // Precedence model:
238+ // - built-in defaults < global receiver_templates < per-receiver explicit value
239+ //
240+ // For "standard" keys present in DefaultPagerdutyDetails, we only override
241+ // receiver values when they are still set to the built-in default template
242+ // string (so explicit empty-string overrides like `firing: ""` are preserved).
243+ //
244+ // For arbitrary/custom keys not present in DefaultPagerdutyDetails, we never
245+ // override existing per-receiver values; we only add the key if it's absent.
246+ if gt .Details == nil {
247+ return
248+ }
249+ if pc .Details == nil {
250+ pc .Details = make (map [string ]any )
251+ }
252+
253+ for k , gv := range gt .Details {
254+ if gv == "" {
255+ // Treat global empty string as "not set" (consistent with applyGlobalTemplateOverride).
256+ continue
257+ }
258+
259+ receiverVal , receiverHasKey := pc .Details [k ]
260+ if ! receiverHasKey {
261+ // Key absent on receiver: global value becomes the default.
262+ pc .Details [k ] = gv
263+ continue
264+ }
265+
266+ // Key exists on receiver:
267+ // - if it's a standard key, override only when receiver still equals the built-in default
268+ // - if it's a custom key, never override an existing per-receiver value
269+ builtInVal , builtInHasKey := DefaultPagerdutyDetails [k ]
270+ if ! builtInHasKey {
271+ continue
272+ }
273+ builtInStr , builtInOK := builtInVal .(string )
274+ receiverStr , receiverOK := receiverVal .(string )
275+ if ! builtInOK || ! receiverOK {
276+ continue
277+ }
278+ if receiverStr == builtInStr {
279+ pc .Details [k ] = gv
280+ }
281+ }
282+ }
283+
284+ func applyOpsGenieTemplates (oc * OpsGenieConfig , gt * GlobalOpsGenieTemplates ) {
285+ if gt != nil {
286+ applyGlobalTemplateOverride (& oc .Message , gt .Message , DefaultOpsGenieConfig .Message )
287+ applyGlobalTemplateOverride (& oc .Description , gt .Description , DefaultOpsGenieConfig .Description )
288+ applyGlobalTemplateOverride (& oc .Source , gt .Source , DefaultOpsGenieConfig .Source )
289+ applyGlobalTemplateOverride (& oc .Note , gt .Note , DefaultOpsGenieConfig .Note )
290+ }
291+ }
292+
293+ func applyVictorOpsTemplates (vc * VictorOpsConfig , gt * GlobalVictorOpsTemplates ) {
294+ if gt != nil {
295+ applyGlobalTemplateOverride (& vc .MessageType , gt .MessageType , DefaultVictorOpsConfig .MessageType )
296+ applyGlobalTemplateOverride (& vc .EntityDisplayName , gt .EntityDisplayName , DefaultVictorOpsConfig .EntityDisplayName )
297+ applyGlobalTemplateOverride (& vc .StateMessage , gt .StateMessage , DefaultVictorOpsConfig .StateMessage )
298+ }
299+ }
300+
301+ func applyPushoverTemplates (pc * PushoverConfig , gt * GlobalPushoverTemplates ) {
302+ if gt != nil {
303+ applyGlobalTemplateOverride (& pc .Title , gt .Title , DefaultPushoverConfig .Title )
304+ applyGlobalTemplateOverride (& pc .Message , gt .Message , DefaultPushoverConfig .Message )
305+ applyGlobalTemplateOverride (& pc .URL , gt .URL , DefaultPushoverConfig .URL )
306+ }
307+ }
308+
309+ func applyWeChatTemplates (wc * WechatConfig , gt * GlobalWeChatTemplates ) {
310+ if gt != nil {
311+ applyGlobalTemplateOverride (& wc .Message , gt .Message , DefaultWechatConfig .Message )
312+ applyGlobalTemplateOverride (& wc .MessageType , gt .MessageType , DefaultWechatConfig .MessageType )
313+ }
314+ }
315+
316+ func applyTelegramTemplates (tc * TelegramConfig , gt * GlobalTelegramTemplates ) {
317+ if gt != nil {
318+ applyGlobalTemplateOverride (& tc .Message , gt .Message , DefaultTelegramConfig .Message )
319+ }
320+ }
321+
322+ // applyGlobalTemplateOverride sets *dst to gtValue only if *dst is currently exactly equal to defaultVal.
323+ // This properly distinguishes between an omitted field (which defaults to defaultVal) and an explicit
324+ // empty string override (like `title: ""`), assuming defaultVal is not literally `""`.
325+ func applyGlobalTemplateOverride (dst * string , gtValue string , defaultVal string ) {
326+ if gtValue == "" {
327+ return
328+ }
329+ if * dst == defaultVal {
330+ * dst = gtValue
331+ }
332+ }
333+
117334// Load parses the YAML input s into a Config.
118335func Load (s string ) (* Config , error ) {
119336 cfg := & Config {}
@@ -350,6 +567,8 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error {
350567 return errors .New ("at most one of mattermost_webhook_url & mattermost_webhook_url_file must be configured" )
351568 }
352569
570+ applyGlobalReceiverTemplates (c .Receivers , c .Global )
571+
353572 names := map [string ]struct {}{}
354573
355574 for _ , rcv := range c .Receivers {
@@ -838,6 +1057,9 @@ type GlobalConfig struct {
8381057 RocketchatTokenIDFile string `yaml:"rocketchat_token_id_file,omitempty" json:"rocketchat_token_id_file,omitempty"`
8391058 MattermostWebhookURL * amcommoncfg.SecretURL `yaml:"mattermost_webhook_url,omitempty" json:"mattermost_webhook_url,omitempty"`
8401059 MattermostWebhookURLFile string `yaml:"mattermost_webhook_url_file,omitempty" json:"mattermost_webhook_url_file,omitempty"`
1060+
1061+ // per-receiver-type global template overrides
1062+ ReceiverTemplates * GlobalReceiverTemplates `yaml:"receiver_templates,omitempty" json:"receiver_templates,omitempty"`
8411063}
8421064
8431065// UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig.
0 commit comments