Skip to content

Commit 20fa318

Browse files
committed
feat: allow nested details fields in pagerduty
This change allows nested key/value pair in pageduty configuration when using `Event API V2` integration. The configuration and payload types where changed from `map[string]string` to `map[string]interface{}`. The details map is walked in stages to render all template strings. Fixes #2477 Fixes #3218 Signed-off-by: Siavash Safi <[email protected]>
1 parent 9b35e76 commit 20fa318

File tree

5 files changed

+256
-54
lines changed

5 files changed

+256
-54
lines changed

config/notifiers.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -328,22 +328,22 @@ type PagerdutyConfig struct {
328328

329329
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
330330

331-
ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"`
332-
ServiceKeyFile string `yaml:"service_key_file,omitempty" json:"service_key_file,omitempty"`
333-
RoutingKey Secret `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
334-
RoutingKeyFile string `yaml:"routing_key_file,omitempty" json:"routing_key_file,omitempty"`
335-
URL *URL `yaml:"url,omitempty" json:"url,omitempty"`
336-
Client string `yaml:"client,omitempty" json:"client,omitempty"`
337-
ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"`
338-
Description string `yaml:"description,omitempty" json:"description,omitempty"`
339-
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
340-
Images []PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"`
341-
Links []PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"`
342-
Source string `yaml:"source,omitempty" json:"source,omitempty"`
343-
Severity string `yaml:"severity,omitempty" json:"severity,omitempty"`
344-
Class string `yaml:"class,omitempty" json:"class,omitempty"`
345-
Component string `yaml:"component,omitempty" json:"component,omitempty"`
346-
Group string `yaml:"group,omitempty" json:"group,omitempty"`
331+
ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"`
332+
ServiceKeyFile string `yaml:"service_key_file,omitempty" json:"service_key_file,omitempty"`
333+
RoutingKey Secret `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
334+
RoutingKeyFile string `yaml:"routing_key_file,omitempty" json:"routing_key_file,omitempty"`
335+
URL *URL `yaml:"url,omitempty" json:"url,omitempty"`
336+
Client string `yaml:"client,omitempty" json:"client,omitempty"`
337+
ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"`
338+
Description string `yaml:"description,omitempty" json:"description,omitempty"`
339+
Details map[string]interface{} `yaml:"details,omitempty" json:"details,omitempty"`
340+
Images []PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"`
341+
Links []PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"`
342+
Source string `yaml:"source,omitempty" json:"source,omitempty"`
343+
Severity string `yaml:"severity,omitempty" json:"severity,omitempty"`
344+
Class string `yaml:"class,omitempty" json:"class,omitempty"`
345+
Component string `yaml:"component,omitempty" json:"component,omitempty"`
346+
Group string `yaml:"group,omitempty" json:"group,omitempty"`
347347
}
348348

349349
// PagerdutyLink is a link.
@@ -376,7 +376,7 @@ func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
376376
return errors.New("at most one of service_key & service_key_file must be configured")
377377
}
378378
if c.Details == nil {
379-
c.Details = make(map[string]string)
379+
c.Details = make(map[string]interface{})
380380
}
381381
if c.Source == "" {
382382
c.Source = c.Client

config/notifiers_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,13 @@ service_key_file: 'xyz'
179179
func TestPagerdutyDetails(t *testing.T) {
180180
tests := []struct {
181181
in string
182-
checkFn func(map[string]string)
182+
checkFn func(map[string]interface{})
183183
}{
184184
{
185185
in: `
186186
routing_key: 'xyz'
187187
`,
188-
checkFn: func(d map[string]string) {
188+
checkFn: func(d map[string]interface{}) {
189189
if len(d) != 4 {
190190
t.Errorf("expected 4 items, got: %d", len(d))
191191
}
@@ -197,7 +197,7 @@ routing_key: 'xyz'
197197
details:
198198
key1: val1
199199
`,
200-
checkFn: func(d map[string]string) {
200+
checkFn: func(d map[string]interface{}) {
201201
if len(d) != 5 {
202202
t.Errorf("expected 5 items, got: %d", len(d))
203203
}
@@ -211,7 +211,7 @@ details:
211211
key2: val2
212212
firing: firing
213213
`,
214-
checkFn: func(d map[string]string) {
214+
checkFn: func(d map[string]interface{}) {
215215
if len(d) != 6 {
216216
t.Errorf("expected 6 items, got: %d", len(d))
217217
}

docs/configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,8 +1232,8 @@ service_key_file: <filepath>
12321232
# Unique location of the affected system.
12331233
[ source: <tmpl_string> | default = client ]
12341234

1235-
# A set of arbitrary key/value pairs that provide further detail
1236-
# about the incident.
1235+
# A set of arbitrary key/value pairs that provide further detail about the incident.
1236+
# Nested key/value pairs are accepted when using PagerDuty integration type `Events API v2`.
12371237
[ details: { <string>: <tmpl_string>, ... } | default = {
12381238
firing: '{{ template "pagerduty.default.instances" .Alerts.Firing }}'
12391239
resolved: '{{ template "pagerduty.default.instances" .Alerts.Resolved }}'

notify/pagerduty/pagerduty.go

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,14 @@ type pagerDutyImage struct {
106106
}
107107

108108
type pagerDutyPayload struct {
109-
Summary string `json:"summary"`
110-
Source string `json:"source"`
111-
Severity string `json:"severity"`
112-
Timestamp string `json:"timestamp,omitempty"`
113-
Class string `json:"class,omitempty"`
114-
Component string `json:"component,omitempty"`
115-
Group string `json:"group,omitempty"`
116-
CustomDetails map[string]string `json:"custom_details,omitempty"`
109+
Summary string `json:"summary"`
110+
Source string `json:"source"`
111+
Severity string `json:"severity"`
112+
Timestamp string `json:"timestamp,omitempty"`
113+
Class string `json:"class,omitempty"`
114+
Component string `json:"component,omitempty"`
115+
Group string `json:"group,omitempty"`
116+
CustomDetails map[string]interface{} `json:"custom_details,omitempty"`
117117
}
118118

119119
func (n *Notifier) encodeMessage(msg *pagerDutyMessage) (bytes.Buffer, error) {
@@ -128,7 +128,7 @@ func (n *Notifier) encodeMessage(msg *pagerDutyMessage) (bytes.Buffer, error) {
128128
if n.apiV1 != "" {
129129
msg.Details = map[string]string{"error": truncatedMsg}
130130
} else {
131-
msg.Payload.CustomDetails = map[string]string{"error": truncatedMsg}
131+
msg.Payload.CustomDetails = map[string]interface{}{"error": truncatedMsg}
132132
}
133133

134134
warningMsg := fmt.Sprintf("Truncated Details because message of size %s exceeds limit %s", units.MetricBytes(buf.Len()).String(), units.MetricBytes(maxEventSize).String())
@@ -149,7 +149,6 @@ func (n *Notifier) notifyV1(
149149
key notify.Key,
150150
data *template.Data,
151151
details map[string]string,
152-
as ...*types.Alert,
153152
) (bool, error) {
154153
var tmplErr error
155154
tmpl := notify.TmplText(n.tmpl, data, &tmplErr)
@@ -209,8 +208,7 @@ func (n *Notifier) notifyV2(
209208
eventType string,
210209
key notify.Key,
211210
data *template.Data,
212-
details map[string]string,
213-
as ...*types.Alert,
211+
details map[string]interface{},
214212
) (bool, error) {
215213
var tmplErr error
216214
tmpl := notify.TmplText(n.tmpl, data, &tmplErr)
@@ -320,19 +318,23 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
320318

321319
n.logger.Debug("extracted group key", "key", key, "eventType", eventType)
322320

323-
details := make(map[string]string, len(n.conf.Details))
324-
for k, v := range n.conf.Details {
325-
detail, err := n.tmpl.ExecuteTextString(v, data)
326-
if err != nil {
327-
return false, fmt.Errorf("%q: failed to template %q: %w", k, v, err)
321+
if n.apiV1 != "" {
322+
details := make(map[string]string, len(n.conf.Details))
323+
for k, v := range n.conf.Details {
324+
detail, err := n.tmpl.ExecuteTextString(v.(string), data)
325+
if err != nil {
326+
return false, fmt.Errorf("%q: failed to template %q: %w", k, v, err)
327+
}
328+
details[k] = detail
328329
}
329-
details[k] = detail
330+
return n.notifyV1(ctx, eventType, key, data, details)
330331
}
331332

332-
if n.apiV1 != "" {
333-
return n.notifyV1(ctx, eventType, key, data, details, as...)
333+
details, err := renderDetails(n.conf.Details, data, n.tmpl.ExecuteTextString)
334+
if err != nil {
335+
return false, err
334336
}
335-
return n.notifyV2(ctx, eventType, key, data, details, as...)
337+
return n.notifyV2(ctx, eventType, key, data, details)
336338
}
337339

338340
func errDetails(status int, body io.Reader) string {
@@ -351,3 +353,30 @@ func errDetails(status int, body io.Reader) string {
351353
}
352354
return fmt.Sprintf("%s: %s", pgr.Message, strings.Join(pgr.Errors, ","))
353355
}
356+
357+
func renderDetails(
358+
details map[string]interface{},
359+
data *template.Data,
360+
exec func(text string, data interface{}) (string, error),
361+
) (map[string]interface{}, error) {
362+
result := make(map[string]interface{}, len(details))
363+
for k, v := range details {
364+
switch t := v.(type) {
365+
case string:
366+
detail, err := exec(v.(string), data)
367+
if err != nil {
368+
return nil, fmt.Errorf("%q: failed to template %q: %w", k, v, err)
369+
}
370+
result[k] = detail
371+
case map[string]interface{}:
372+
detail, err := renderDetails(v.(map[string]interface{}), data, exec)
373+
if err != nil {
374+
return nil, fmt.Errorf("%q: failed to template %q: %w", k, v, err)
375+
}
376+
result[k] = detail
377+
default:
378+
return nil, fmt.Errorf("%q: unsupported detail field type %v: %v", k, t, v)
379+
}
380+
}
381+
return result, nil
382+
}

0 commit comments

Comments
 (0)