Skip to content

Commit 4200259

Browse files
committed
fix: pageduty custom_details in payload
This change fixes the `custom_details` value in PagerDuty V2 API payloads. Fixes #2477 Fixes #3218 Alertmanager PagerDuty configuration allows users to define `details` under `pagerduty_configs`: ```yaml [ details: { <string>: <tmpl_string>, ... } | default = { firing: '{{ template "pagerduty.default.instances" .Alerts.Firing }}' resolved: '{{ template "pagerduty.default.instances" .Alerts.Resolved }}' num_firing: '{{ .Alerts.Firing | len }}' num_resolved: '{{ .Alerts.Resolved | len }}' } ] ``` The internal Alertmanager configuration structure is defined as: ```go // PagerdutyConfig configures notifications via PagerDuty. type PagerdutyConfig struct { ... Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"` } ``` And the PagerDuty payload is defined as: ```go type pagerDutyPayload struct { ... CustomDetails map[string]string `json:"custom_details,omitempty"` } ``` The current flow of configuration parsing and encoding is: 1. `pagerduty_configs[].details` are read from configuration as `map[string]string` 2. The `details` map values are parsed as text templates 3. The result is passed as `custom_details`, part of the JSON payload sent to PagerDuty API V2 Here is an example payload using `.Alerts.Firing`, which returns a list of currently firing alert objects in this group. Documented here: https://prometheus.io/docs/alerting/latest/notifications/#data This results in a payload like below: ```json { "client": "Alertmanager", ... "custom_details": { "firing": "Labels: - alertname = Server_Down ... Annotations: - summary = Server is down ... " } } ``` Using `map[string]string` in payloads cause the rendered YAML templates to be encoded as JSON strings. This is undesirable in most cases as the end-user might expect a nested JSON object which is machine readable/parsable by it's fields. This change: - adds logic to unmarshall `details` values as YAML with fallback to string - uses `map[string]interface{}` to encode `custom_details` to JSON - requires gopkg.in/yaml.v3, see go-yaml/yaml#591 - does not change existing yaml.v2 dependency, see #3322 Signed-off-by: Siavash Safi <[email protected]>
1 parent 22bfc8a commit 4200259

File tree

3 files changed

+59
-14
lines changed

3 files changed

+59
-14
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ require (
4848
golang.org/x/tools v0.23.0
4949
gopkg.in/telebot.v3 v3.3.6
5050
gopkg.in/yaml.v2 v2.4.0
51+
gopkg.in/yaml.v3 v3.0.1
5152
)
5253

5354
require (
@@ -100,5 +101,4 @@ require (
100101
golang.org/x/sync v0.7.0 // indirect
101102
golang.org/x/sys v0.22.0 // indirect
102103
google.golang.org/protobuf v1.34.2 // indirect
103-
gopkg.in/yaml.v3 v3.0.1 // indirect
104104
)

notify/pagerduty/pagerduty.go

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/go-kit/log/level"
3030
commoncfg "github.com/prometheus/common/config"
3131
"github.com/prometheus/common/model"
32+
"gopkg.in/yaml.v3"
3233

3334
"github.com/prometheus/alertmanager/config"
3435
"github.com/prometheus/alertmanager/notify"
@@ -107,14 +108,14 @@ type pagerDutyImage struct {
107108
}
108109

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

120121
func (n *Notifier) encodeMessage(msg *pagerDutyMessage) (bytes.Buffer, error) {
@@ -129,7 +130,7 @@ func (n *Notifier) encodeMessage(msg *pagerDutyMessage) (bytes.Buffer, error) {
129130
if n.apiV1 != "" {
130131
msg.Details = map[string]string{"error": truncatedMsg}
131132
} else {
132-
msg.Payload.CustomDetails = map[string]string{"error": truncatedMsg}
133+
msg.Payload.CustomDetails = map[string]interface{}{"error": truncatedMsg}
133134
}
134135

135136
warningMsg := fmt.Sprintf("Truncated Details because message of size %s exceeds limit %s", units.MetricBytes(buf.Len()).String(), units.MetricBytes(maxEventSize).String())
@@ -246,7 +247,7 @@ func (n *Notifier) notifyV2(
246247
Summary: summary,
247248
Source: tmpl(n.conf.Source),
248249
Severity: tmpl(n.conf.Severity),
249-
CustomDetails: details,
250+
CustomDetails: toCustomDetails(details),
250251
Class: tmpl(n.conf.Class),
251252
Component: tmpl(n.conf.Component),
252253
Group: tmpl(n.conf.Group),
@@ -333,6 +334,7 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
333334
if n.apiV1 != "" {
334335
return n.notifyV1(ctx, eventType, key, data, details, as...)
335336
}
337+
336338
return n.notifyV2(ctx, eventType, key, data, details, as...)
337339
}
338340

@@ -352,3 +354,17 @@ func errDetails(status int, body io.Reader) string {
352354
}
353355
return fmt.Sprintf("%s: %s", pgr.Message, strings.Join(pgr.Errors, ","))
354356
}
357+
358+
func toCustomDetails(details map[string]string) map[string]interface{} {
359+
customDetails := make(map[string]interface{}, len(details))
360+
for k, v := range details {
361+
var v2 interface{}
362+
// Try to unmarshall any rendered templates as YAML.
363+
if err := yaml.Unmarshal([]byte(v), &v2); err != nil {
364+
customDetails[k] = v
365+
continue
366+
}
367+
customDetails[k] = v2
368+
}
369+
return customDetails
370+
}

notify/pagerduty/pagerduty_test.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"net/http/httptest"
2424
"net/url"
2525
"os"
26+
"reflect"
2627
"strings"
2728
"testing"
2829
"time"
@@ -323,15 +324,18 @@ func TestErrDetails(t *testing.T) {
323324
}
324325

325326
func TestEventSizeEnforcement(t *testing.T) {
326-
bigDetails := map[string]string{
327+
bigDetailsV1 := map[string]string{
328+
"firing": strings.Repeat("a", 513000),
329+
}
330+
bigDetailsV2 := map[string]interface{}{
327331
"firing": strings.Repeat("a", 513000),
328332
}
329333

330334
// V1 Messages
331335
msgV1 := &pagerDutyMessage{
332336
ServiceKey: "01234567890123456789012345678901",
333337
EventType: "trigger",
334-
Details: bigDetails,
338+
Details: bigDetailsV1,
335339
}
336340

337341
notifierV1, err := New(
@@ -353,7 +357,7 @@ func TestEventSizeEnforcement(t *testing.T) {
353357
RoutingKey: "01234567890123456789012345678901",
354358
EventAction: "trigger",
355359
Payload: &pagerDutyPayload{
356-
CustomDetails: bigDetails,
360+
CustomDetails: bigDetailsV2,
357361
},
358362
}
359363

@@ -497,3 +501,28 @@ func TestPagerDutyEmptySrcHref(t *testing.T) {
497501
}...)
498502
require.NoError(t, err)
499503
}
504+
505+
func Test_toCustomDetails(t *testing.T) {
506+
type args struct {
507+
details map[string]string
508+
}
509+
tests := []struct {
510+
name string
511+
args args
512+
want map[string]interface{}
513+
}{
514+
{"number", args{map[string]string{"number": "123456789"}}, map[string]interface{}{"number": 123456789}},
515+
{"string", args{map[string]string{"string": "test"}}, map[string]interface{}{"string": "test"}},
516+
{"map", args{map[string]string{"map": "{key1: value1, key2: 2}"}}, map[string]interface{}{"map": map[string]interface{}{"key1": "value1", "key2": 2}}},
517+
{"list_numbers", args{map[string]string{"list": "[1, 2, 3]"}}, map[string]interface{}{"list": []interface{}{1, 2, 3}}},
518+
{"list_strings", args{map[string]string{"list": "[one, two, three]"}}, map[string]interface{}{"list": []interface{}{"one", "two", "three"}}},
519+
{"nested", args{map[string]string{"parent": "child: {number: 123456789, string: test}"}}, map[string]interface{}{"parent": map[string]interface{}{"child": map[string]interface{}{"number": 123456789, "string": "test"}}}},
520+
}
521+
for _, tt := range tests {
522+
t.Run(tt.name, func(t *testing.T) {
523+
if got := toCustomDetails(tt.args.details); !reflect.DeepEqual(got, tt.want) {
524+
t.Errorf("toCustomDetails() = %v, want %v", got, tt.want)
525+
}
526+
})
527+
}
528+
}

0 commit comments

Comments
 (0)