Skip to content

feat: allow nested details fields in pagerduty #3944

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,22 +328,22 @@ type PagerdutyConfig struct {

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

ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"`
ServiceKeyFile string `yaml:"service_key_file,omitempty" json:"service_key_file,omitempty"`
RoutingKey Secret `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
RoutingKeyFile string `yaml:"routing_key_file,omitempty" json:"routing_key_file,omitempty"`
URL *URL `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"`
Source string `yaml:"source,omitempty" json:"source,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"`
ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"`
ServiceKeyFile string `yaml:"service_key_file,omitempty" json:"service_key_file,omitempty"`
RoutingKey Secret `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
RoutingKeyFile string `yaml:"routing_key_file,omitempty" json:"routing_key_file,omitempty"`
URL *URL `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]interface{} `yaml:"details,omitempty" json:"details,omitempty"`
Images []PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"`
Links []PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"`
Source string `yaml:"source,omitempty" json:"source,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"`
}

// PagerdutyLink is a link.
Expand Down Expand Up @@ -376,7 +376,7 @@ func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
return errors.New("at most one of service_key & service_key_file must be configured")
}
if c.Details == nil {
c.Details = make(map[string]string)
c.Details = make(map[string]interface{})
}
if c.Source == "" {
c.Source = c.Client
Expand Down
8 changes: 4 additions & 4 deletions config/notifiers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ service_key_file: 'xyz'
func TestPagerdutyDetails(t *testing.T) {
tests := []struct {
in string
checkFn func(map[string]string)
checkFn func(map[string]interface{})
}{
{
in: `
routing_key: 'xyz'
`,
checkFn: func(d map[string]string) {
checkFn: func(d map[string]interface{}) {
if len(d) != 4 {
t.Errorf("expected 4 items, got: %d", len(d))
}
Expand All @@ -197,7 +197,7 @@ routing_key: 'xyz'
details:
key1: val1
`,
checkFn: func(d map[string]string) {
checkFn: func(d map[string]interface{}) {
if len(d) != 5 {
t.Errorf("expected 5 items, got: %d", len(d))
}
Expand All @@ -211,7 +211,7 @@ details:
key2: val2
firing: firing
`,
checkFn: func(d map[string]string) {
checkFn: func(d map[string]interface{}) {
if len(d) != 6 {
t.Errorf("expected 6 items, got: %d", len(d))
}
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1232,8 +1232,8 @@ service_key_file: <filepath>
# Unique location of the affected system.
[ source: <tmpl_string> | default = client ]

# A set of arbitrary key/value pairs that provide further detail
# about the incident.
# A set of arbitrary key/value pairs that provide further detail about the incident.
# Nested key/value pairs are accepted when using PagerDuty integration type `Events API v2`.
[ details: { <string>: <tmpl_string>, ... } | default = {
firing: '{{ template "pagerduty.default.instances" .Alerts.Firing }}'
resolved: '{{ template "pagerduty.default.instances" .Alerts.Resolved }}'
Expand Down
71 changes: 50 additions & 21 deletions notify/pagerduty/pagerduty.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,14 @@ type pagerDutyImage struct {
}

type pagerDutyPayload struct {
Summary string `json:"summary"`
Source string `json:"source"`
Severity string `json:"severity"`
Timestamp string `json:"timestamp,omitempty"`
Class string `json:"class,omitempty"`
Component string `json:"component,omitempty"`
Group string `json:"group,omitempty"`
CustomDetails map[string]string `json:"custom_details,omitempty"`
Summary string `json:"summary"`
Source string `json:"source"`
Severity string `json:"severity"`
Timestamp string `json:"timestamp,omitempty"`
Class string `json:"class,omitempty"`
Component string `json:"component,omitempty"`
Group string `json:"group,omitempty"`
CustomDetails map[string]interface{} `json:"custom_details,omitempty"`
}

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

warningMsg := fmt.Sprintf("Truncated Details because message of size %s exceeds limit %s", units.MetricBytes(buf.Len()).String(), units.MetricBytes(maxEventSize).String())
Expand All @@ -149,7 +149,6 @@ func (n *Notifier) notifyV1(
key notify.Key,
data *template.Data,
details map[string]string,
as ...*types.Alert,
) (bool, error) {
var tmplErr error
tmpl := notify.TmplText(n.tmpl, data, &tmplErr)
Expand Down Expand Up @@ -209,8 +208,7 @@ func (n *Notifier) notifyV2(
eventType string,
key notify.Key,
data *template.Data,
details map[string]string,
as ...*types.Alert,
details map[string]interface{},
) (bool, error) {
var tmplErr error
tmpl := notify.TmplText(n.tmpl, data, &tmplErr)
Expand Down Expand Up @@ -320,19 +318,23 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)

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

details := make(map[string]string, len(n.conf.Details))
for k, v := range n.conf.Details {
detail, err := n.tmpl.ExecuteTextString(v, data)
if err != nil {
return false, fmt.Errorf("%q: failed to template %q: %w", k, v, err)
if n.apiV1 != "" {
details := make(map[string]string, len(n.conf.Details))
for k, v := range n.conf.Details {
detail, err := n.tmpl.ExecuteTextString(v.(string), data)
if err != nil {
return false, fmt.Errorf("%q: failed to template %q: %w", k, v, err)
}
details[k] = detail
}
details[k] = detail
return n.notifyV1(ctx, eventType, key, data, details)
}

if n.apiV1 != "" {
return n.notifyV1(ctx, eventType, key, data, details, as...)
details, err := renderDetails(n.conf.Details, data, n.tmpl.ExecuteTextString)
if err != nil {
return false, err
}
return n.notifyV2(ctx, eventType, key, data, details, as...)
return n.notifyV2(ctx, eventType, key, data, details)
}

func errDetails(status int, body io.Reader) string {
Expand All @@ -351,3 +353,30 @@ func errDetails(status int, body io.Reader) string {
}
return fmt.Sprintf("%s: %s", pgr.Message, strings.Join(pgr.Errors, ","))
}

func renderDetails(
details map[string]interface{},
data *template.Data,
exec func(text string, data interface{}) (string, error),
) (map[string]interface{}, error) {
result := make(map[string]interface{}, len(details))
for k, v := range details {
switch t := v.(type) {
case string:
detail, err := exec(v.(string), data)
if err != nil {
return nil, fmt.Errorf("%q: failed to template %q: %w", k, v, err)
}
result[k] = detail
case map[string]interface{}:
detail, err := renderDetails(v.(map[string]interface{}), data, exec)
if err != nil {
return nil, fmt.Errorf("%q: failed to template %q: %w", k, v, err)
}
result[k] = detail
default:
return nil, fmt.Errorf("%q: unsupported detail field type %v: %v", k, t, v)
}
}
return result, nil
}
Loading