Skip to content
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

Updates to allow for use of additional PD-CEF fields #53

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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased

### Added
- Add `--client-name` and `--sensu-base-url`. If provided, these options add a link to the Sensu dashboard from the
Pagerduty Alert.
- Add `--link-annotations` option. If set, this option will any links provided in the check or entity annotations as
links in the Pagerduty Event.
- Add ability to template additional PD-CEF Fields from Sensu event data:
- Add `--use-event-timestamp` option. If set this option will set the `Timestamp` field to the
timestamp of the Sensu event.
- Add `--class-template`, `--group-template` and `--component-template` option. If provided, this option allows for
the `Class`, `Group` and `Component` PD-CEF fields to be templated from Sensu event data. Note the
`Component` field is set to the Sensu events `.Check.Name` by default if no template is provided to retain
compatibility.

## 2.6.0 - 2024-02-08

### Changed
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,24 @@ Available Commands:

Flags:
-e, --alternate-endpoint string The endpoint to use to send the PagerDuty events, can be set with PAGERDUTY_ALTERNATE_ENDPOINT
--class-template string Template for PD-CEF class field, can be set with PAGERDUTY_CLASS_TEMPLATE
--client-name string Name for the client, this will appear in Pagerduty when events are logged (default "Sensu")
--component-template string Template for PD-CEF component field, can be set with PAGERDUTY_COMPONENT_TEMPLATE
--contact-routing Enable contact routing
-k, --dedup-key-template string The PagerDuty V2 API deduplication key template, can be set with PAGERDUTY_DEDUP_KEY_TEMPLATE (default "{{.Entity.Name}}-{{.Check.Name}}")
--details-format string The format of the details output ('string' or 'json'), can be set with PAGERDUTY_DETAILS_FORMAT (default "string")
-d, --details-template string The template for the alert details, can be set with PAGERDUTY_DETAILS_TEMPLATE (default full event JSON)
--group-template string Template for PD-CEF group field, can be set with PAGERDUTY_GROUP_TEMPLATE
-h, --help help for sensu-pagerduty-handler
-l, --link-annotations Add links for any annotations that are a URL
-u, --sensu-base-url string Base URL for sensu. The handler will add a link to the event using this
-s, --status-map string The status map used to translate a Sensu check status to a PagerDuty severity, can be set with PAGERDUTY_STATUS_MAP
-S, --summary-template string The template for the alert summary, can be set with PAGERDUTY_SUMMARY_TEMPLATE (default "{{.Entity.Name}}/{{.Check.Name}} : {{.Check.Output}}")
--team string Envvar name for pager team(alphanumeric and underscores) holding PagerDuty V2 API authentication token, can be set with PAGERDUTY_TEAM
--team-suffix string Pager team suffix string to append if missing from team name, can be set with PAGERDUTY_TEAM_SUFFIX (default "_pagerduty_token")
--timeout uint The maximum amount of time in seconds to wait for the event to be created, can be set with PAGERDUTY_TIMEOUT (default 30)
-t, --token string The PagerDuty V2 API authentication token, can be set with PAGERDUTY_TOKEN
-T, --use-event-timestamp Use the timestamp from the Sensu event for the PD-CEF timestamp field

Use "sensu-pagerduty-handler [command] --help" for more information about a command.
```
Expand Down Expand Up @@ -159,9 +166,13 @@ override the corresponding environment variable.
| Argument | Environment Variable |
|----------------------|------------------------------|
| --alternate-endpoint | PAGERDUTY_ALTERNATE_ENDPOINT |
| --class-template | PAGERDUTY_CLASS_TEMPLATE |
| --component-template | PAGERDUTY_COMPONENT_TEMPLATE |
| --group-template | PAGERDUTY_GROUP_TEMPLATE |
| --dedup-key-template | PAGERDUTY_DEDUP_KEY_TEMPLATE |
| --details-template | PAGERDUTY_DETAILS_TEMPLATE |
| --details-format | PAGERDUTY_DETAILS_FORMAT |
| --sensu-base-url | PAGERDUTY_SENSU_BASE_URL |
| --status-map | PAGERDUTY_STATUS_MAP |
| --summary-template | PAGERDUTY_SUMMARY_TEMPLATE |
| --team | PAGERDUTY_TEAM |
Expand Down
223 changes: 219 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ type HandlerConfig struct {
alternateEndpoint string
contactRouting bool
contacts []string
clientName string
sensuBaseUrl string
linkAnnotations bool
useEventTimestamp bool
classTemplate string
groupTemplate string
componentTemplate string
}

type eventStatusMap map[string][]uint32
Expand Down Expand Up @@ -163,6 +170,68 @@ var (
Value: &config.contactRouting,
Default: false,
},
&sensu.PluginConfigOption[string]{
Path: "client-name",
Env: "",
Argument: "client-name",
Usage: "Name for the client, this will appear in Pagerduty when events are logged",
Value: &config.clientName,
Default: "Sensu",
},
&sensu.PluginConfigOption[string]{
Path: "sensu-base-url",
Env: "PAGERDUTY_SENSU_BASE_URL",
Argument: "sensu-base-url",
Shorthand: "u",
Usage: "Base URL for sensu. The handler will add a link to the event using this",
Value: &config.sensuBaseUrl,
Default: "",
},
&sensu.PluginConfigOption[bool]{
Path: "link-annotations",
Env: "",
Argument: "link-annotations",
Shorthand: "l",
Usage: "Add links for any annotations that are a URL",
Value: &config.linkAnnotations,
Default: false,
},
&sensu.PluginConfigOption[bool]{
Path: "use-event-timestamp",
Env: "",
Argument: "use-event-timestamp",
Shorthand: "T",
Usage: "Use the timestamp from the Sensu event for the PD-CEF timestamp field",
Value: &config.useEventTimestamp,
Default: false,
},
&sensu.PluginConfigOption[string]{
Path: "class-template",
Env: "PAGERDUTY_CLASS_TEMPLATE",
Argument: "class-template",
Shorthand: "",
Usage: "Template for PD-CEF class field, can be set with PAGERDUTY_CLASS_TEMPLATE",
Value: &config.classTemplate,
Default: "",
},
&sensu.PluginConfigOption[string]{
Path: "group-template",
Env: "PAGERDUTY_GROUP_TEMPLATE",
Argument: "group-template",
Shorthand: "",
Usage: "Template for PD-CEF group field, can be set with PAGERDUTY_GROUP_TEMPLATE",
Value: &config.groupTemplate,
Default: "",
},
&sensu.PluginConfigOption[string]{
Path: "component-template",
Env: "PAGERDUTY_COMPONENT_TEMPLATE",
Argument: "component-template",
Shorthand: "",
Usage: "Template for PD-CEF component field, can be set with PAGERDUTY_COMPONENT_TEMPLATE",
Value: &config.componentTemplate,
Default: "",
},
}
)

Expand Down Expand Up @@ -217,7 +286,6 @@ func checkArgs(event *corev2.Event) error {
if len(teamToken) != 0 {
config.authToken = teamToken
}

}

if config.contactRouting {
Expand Down Expand Up @@ -352,6 +420,21 @@ func manageIncident(event *corev2.Event, token string) error {
return err
}

group, err := getGroup(event)
if err != nil {
return err
}

component, err := getComponent(event)
if err != nil {
return err
}

class, err := getClass(event)
if err != nil {
return err
}

// "The maximum permitted length of PG event is 512 KB. Let's limit check output to 256KB to prevent triggering a failed send"
if len(event.Check.Output) > 256000 {
log.Printf("Warning Incident Payload Truncated!")
Expand All @@ -360,10 +443,13 @@ func manageIncident(event *corev2.Event, token string) error {

pdPayload := pagerduty.V2Payload{
Source: event.Entity.Name,
Component: event.Check.Name,
Component: component,
Severity: severity,
Summary: summary,
Details: details,
Class: class,
Group: group,
Timestamp: getTimestamp(event),
}

action := "trigger"
Expand All @@ -384,6 +470,9 @@ func manageIncident(event *corev2.Event, token string) error {
Action: action,
Payload: &pdPayload,
DedupKey: dedupKey,
Client: config.clientName,
ClientURL: getClientUrl(event),
Links: getLinks(event),
}

client := pagerduty.NewClient()
Expand Down Expand Up @@ -413,11 +502,17 @@ func manageIncident(event *corev2.Event, token string) error {
return err
}
// FUTURE send to AH
log.Printf("Failback event (%s) submitted to PagerDuty, Status: %s, Dedup Key: %s, Message: %s", action, failResponse.Status, failResponse.DedupKey, failResponse.Message)
log.Printf(
"Failback event (%s) submitted to PagerDuty, Status: %s, Dedup Key: %s, Message: %s", action,
failResponse.Status, failResponse.DedupKey, failResponse.Message,
)
}

// FUTURE send to AH
log.Printf("Event (%s) submitted to PagerDuty, Status: %s, Dedup Key: %s, Message: %s", action, eventResponse.Status, eventResponse.DedupKey, eventResponse.Message)
log.Printf(
"Event (%s) submitted to PagerDuty, Status: %s, Dedup Key: %s, Message: %s", action, eventResponse.Status,
eventResponse.DedupKey, eventResponse.Message,
)
return nil
}

Expand Down Expand Up @@ -490,6 +585,69 @@ func getSummary(event *corev2.Event) (string, error) {
return summary, nil
}

func getTimestamp(event *corev2.Event) string {
timestamp := ""
if config.useEventTimestamp {
timestamp = time.Unix(event.Timestamp, 0).Format(time.RFC3339)
}

return timestamp
}

func getGroup(event *corev2.Event) (string, error) {
var (
group string
err error
)

if len(config.groupTemplate) > 0 {
group, err = templates.EvalTemplate("group", config.groupTemplate, event)
if err != nil {
return "", fmt.Errorf("failued to evaluate template %s: %v", config.groupTemplate, err)
}
} else {
group = ""
}

return group, nil
}

func getComponent(event *corev2.Event) (string, error) {
var (
component string
err error
)

if len(config.componentTemplate) > 0 {
component, err = templates.EvalTemplate("component", config.componentTemplate, event)
if err != nil {
return "", fmt.Errorf("failued to evaluate template %s: %v", config.componentTemplate, err)
}
} else {
component = event.Check.Name
}

return component, nil
}

func getClass(event *corev2.Event) (string, error) {
var (
class string
err error
)

if len(config.classTemplate) > 0 {
class, err = templates.EvalTemplate("class", config.classTemplate, event)
if err != nil {
return "", fmt.Errorf("failued to evaluate template %s: %v", config.classTemplate, err)
}
} else {
class = ""
}

return class, nil
}

func getDetails(event *corev2.Event) (details interface{}, err error) {
if len(config.detailsTemplate) > 0 {
detailsStr, err := templates.EvalTemplate("details", config.detailsTemplate, event)
Expand All @@ -511,3 +669,60 @@ func getDetails(event *corev2.Event) (details interface{}, err error) {
}
return details, nil
}

func getClientUrl(event *corev2.Event) string {
if config.sensuBaseUrl == "" {
return ""
}

return fmt.Sprintf(
"%s/c/~/n/%s/events/%s/%s",
strings.TrimSuffix(config.sensuBaseUrl, "/"),
event.Namespace,
event.Entity.Name,
event.Check.Name,
)
}

type Link struct {
Text string `json:"text"`
Href string `json:"href"`
}

func isLink(s string) bool {
_, err := url.ParseRequestURI(s)

return err == nil
}

func getLinks(event *corev2.Event) []interface{} {
links := make([]interface{}, 0, len(event.Check.Annotations))

if !config.linkAnnotations {
return links
}

for key, value := range event.Check.Annotations {
if isLink(value) {
links = append(
links, Link{
Text: fmt.Sprintf("check %s", key),
Href: value,
},
)
}
}

for key, value := range event.Entity.Annotations {
if isLink(value) {
links = append(
links, Link{
Text: fmt.Sprintf("entity %s", key),
Href: value,
},
)
}
}

return links
}
Loading