Improve PagerDuty templating

This commit is contained in:
Fabian Reinartz 2015-11-26 18:19:46 +01:00
parent 921c4b8b55
commit 9fbc76a52f
7 changed files with 101 additions and 29 deletions

View File

@ -340,7 +340,7 @@ func (c *Receiver) UnmarshalYAML(unmarshal func(interface{}) error) error {
return checkOverflow(c.XXX, "receiver config")
}
// Regexp encapsulates a regexp.Regexp and makes it YAML marshallable.
// Regexp encapsulates a regexp.Regexp and makes it YAML marshalable.
type Regexp struct {
*regexp.Regexp
}

View File

@ -21,10 +21,11 @@ import (
var (
// DefaultEmailConfig defines default values for Email configurations.
DefaultEmailConfig = EmailConfig{
HTML: `{{template "email.default.html" .}}`,
HTML: `{{ template "email.default.html" . }}`,
}
// DefaultEmailSubject is a template used if no email subject has been provided.
DefaultEmailSubject = `{{template "email.default.subject" .}}`
// DefaultEmailSubject defines the default Subject header of an Email.
DefaultEmailSubject = `{{ template "email.default.subject" . }}`
// DefaultHipchatConfig defines default values for Hipchat configurations.
DefaultHipchatConfig = HipchatConfig{
@ -34,23 +35,31 @@ var (
// DefaultPagerdutyConfig defines default values for PagerDuty configurations.
DefaultPagerdutyConfig = PagerdutyConfig{
Description: `{{template "pagerduty.default.description" .}}`,
// TODO: Add a details field with all the alerts.
Description: `{{ template "pagerduty.default.description" .}}`,
Client: `{{ template "pagerduty.default.client" . }}`,
ClientURL: `{{ template "pagerduty.default.clientURL" . }}`,
Details: map[string]string{
"firing": `{{ template "pagerduty.default.instances" (.Alerts | firing) }}`,
"resolved": `{{ template "pagerduty.default.instances" (.Alerts | resolved) }}`,
"num_firing": `{{ .Alerts | firing | len }}`,
"num_resolved": `{{ .Alerts | resolved | len }}`,
},
}
// DefaultSlackConfig defines default values for Slack configurations.
DefaultSlackConfig = SlackConfig{
Color: `{{ if eq .Status "firing" }}warning{{ else }}good{{ end }}`,
Title: `{{template "slack.default.title" . }}`,
TitleLink: `{{template "slack.default.title_link" . }}`,
Pretext: `{{template "slack.default.pretext" . }}`,
Text: `{{template "slack.default.text" . }}`,
Fallback: `{{template "slack.default.fallback" . }}`,
Title: `{{ template "slack.default.title" . }}`,
TitleLink: `{{ template "slack.default.title_link" . }}`,
Pretext: `{{ template "slack.default.pretext" . }}`,
Text: `{{ template "slack.default.text" . }}`,
Fallback: `{{ template "slack.default.fallback" . }}`,
}
// DefaultOpsGenieConfig defines default values for OpsGenie configurations.
DefaultOpsGenieConfig = OpsGenieConfig{
Description: `{{template "opsgenie.default.description" .}}`,
Description: `{{ template "opsgenie.default.description" . }}`,
Source: `{{ template "opsgenie.default.source" }}`,
// TODO: Add a details field with all the alerts.
}
)
@ -186,6 +195,8 @@ func (c *HipchatConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
type PagerdutyConfig struct {
ServiceKey string `yaml:"service_key"`
URL string `yaml:"url"`
Client string `yaml:"client"`
ClientURL string `yaml:"client_url"`
Description string `yaml:"description"`
Details map[string]string `yaml:"details"`
@ -292,6 +303,7 @@ type OpsGenieConfig struct {
APIKey string `yaml:"api_key"`
APIHost string `yaml:"api_host"`
Description string `yaml:"description"`
Source string `yaml:"source"`
Details map[string]string `yaml:"details"`
// Catches all undefined fields and must be empty after parsing.

33
main.go
View File

@ -18,7 +18,9 @@ import (
"database/sql"
"flag"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
@ -137,6 +139,9 @@ func main() {
if err != nil {
return err
}
if tmpl.ExternalURL, err = extURL(*externalURL); err != nil {
return err
}
disp.Stop()
@ -193,3 +198,31 @@ func printVersion() {
}
fmt.Fprintln(os.Stdout, strings.TrimSpace(buf.String()))
}
func extURL(s string) (*url.URL, error) {
if s == "" {
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
_, port, err := net.SplitHostPort(*listenAddress)
if err != nil {
return nil, err
}
s = fmt.Sprintf("http://%s:%s/", hostname, port)
}
u, err := url.Parse(s)
if err != nil {
return nil, err
}
ppref := strings.TrimRight(u.Path, "/")
if ppref != "" && !strings.HasPrefix(ppref, "/") {
ppref = "/" + ppref
}
u.Path = ppref
return u, nil
}

View File

@ -190,7 +190,7 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) error {
}
var (
data = template.NewData(groupLabels(ctx), as...)
data = n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
tmpl = tmplText(n.tmpl, data, &err)
from = tmpl(n.conf.From)
to = tmpl(n.conf.To)
@ -274,7 +274,7 @@ type pagerDutyMessage struct {
Description string `json:"description"`
Client string `json:"client,omitempty"`
ClientURL string `json:"client_url,omitempty"`
Details map[string]string `json:"details"`
Details map[string]string `json:"details,omitempty"`
}
// Notify implements the Notifier interface.
@ -289,7 +289,7 @@ func (n *PagerDuty) Notify(ctx context.Context, as ...*types.Alert) error {
var err error
var (
alerts = types.Alerts(as...)
data = template.NewData(groupLabels(ctx), as...)
data = n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
tmpl = tmplText(n.tmpl, data, &err)
eventType = pagerDutyEventTrigger
)
@ -310,11 +310,10 @@ func (n *PagerDuty) Notify(ctx context.Context, as ...*types.Alert) error {
IncidentKey: key,
Description: tmpl(n.conf.Description),
Details: details,
Client: "AlertManager",
}
if eventType == pagerDutyEventTrigger {
msg.Client = "Prometheus Alertmanager"
msg.ClientURL = ""
msg.Client = tmpl(n.conf.Client)
msg.ClientURL = tmpl(n.conf.ClientURL)
}
if err != nil {
return err
@ -375,7 +374,7 @@ func (n *Slack) Notify(ctx context.Context, as ...*types.Alert) error {
var err error
var (
alerts = types.Alerts(as...)
data = template.NewData(groupLabels(ctx), as...)
data = n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
tmplText = tmplText(n.tmpl, data, &err)
tmplHTML = tmplHTML(n.tmpl, data, &err)
)
@ -454,7 +453,7 @@ func (n *OpsGenie) Notify(ctx context.Context, as ...*types.Alert) error {
if !ok {
return fmt.Errorf("group key missing")
}
data := template.NewData(groupLabels(ctx), as...)
data := n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
log.With("incident", key).Debugln("notifying OpsGenie")

View File

@ -74,6 +74,14 @@ func WithNow(ctx context.Context, t time.Time) context.Context {
return context.WithValue(ctx, keyNow, t)
}
func receiver(ctx context.Context) string {
recv, ok := Receiver(ctx)
if !ok {
log.Error("missing receiver")
}
return recv
}
// Receiver extracts a receiver from the context. Iff none exists, the
// second argument is false.
func Receiver(ctx context.Context) (string, bool) {

View File

@ -1,22 +1,36 @@
{{ define "__alertmanager" }}AlertManager{{ end }}
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}{{ end }}
{{ define "__subject" }}{{$dot := .}}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts | firing | len }}{{ end }}] {{ range $i, $p := .GroupLabels | sortedPairs }}{{ $p.Value }} {{ end }}{{if gt (len .CommonLabels) (len .GroupLabels) }}({{ range $i, $p := .CommonLabels | sortedPairs }}{{ if eq "" (index $dot.GroupLabels $p.Name) }}{{ $p.Value }} {{ end }}{{ end }}){{ end }}{{ end }}
{{ define "__description" }}TODO{{ end }}
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }}{{ end }}
{{ define "__subject" }}{{$dot := .}}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts | firing | len }}{{ end }}] {{ range .GroupLabels | sortedPairs }}{{ .Value }} {{ end }}{{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ range .CommonLabels | sortedPairs }}{{ if eq "" (index $dot.GroupLabels .Name) }}{{ .Value }} {{ end }}{{ end }}){{ end }}{{ end }}
{{ define "__description" }}{{ end }}
{{ define "__text_alert_list" }}{{ range . }}Labels:
{{ range .Labels | sortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }}Annotations:
{{ range .Annotations | sortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end}}
{{ end }}{{ end }}
{{ define "slack.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "slack.default.fallback" }}{{ template "__subject" . }}{{ end }}
{{ define "slack.default.pretext" }}{{ end }}
{{ define "slack.default.titlelink" }}{{ template "__alertmanagerURL" }}/something{{ end }}
{{ define "slack.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "slack.default.text" }}{{ template "__subject" . }}{{ end }}
{{ define "pagerduty.default.description" }}{{ template "__subject" . }}{{ end }}
{{ define "pagerduty.default.client" }}{{ template "__alertmanager" . }}{{ end }}
{{ define "pagerduty.default.clientURL" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "pagerduty.default.instances" }}{{ template "__text_alert_list" . }}{{ end }}
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "opsgenie.default.description" }}{{ template "__subject" . }}{{ end }}
{{ define "opsgenie.default.source" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "email.default.html" }}
{{/*

View File

@ -15,6 +15,7 @@ package template
import (
"bytes"
"net/url"
"sort"
"strings"
@ -30,6 +31,8 @@ import (
type Template struct {
text *tmpltext.Template
html *tmplhtml.Template
ExternalURL *url.URL
}
// FromGlobs calls ParseGlob on all path globs provided and returns the
@ -143,8 +146,9 @@ type Pair struct {
// as this will confuse them and prevent simple things like
// simple equality checks to fail. Map everything to float64/string.
type Data struct {
Status string
Alerts []Alert
Receiver string
Status string
Alerts []Alert
GroupLabels map[string]string
CommonLabels map[string]string
@ -160,16 +164,18 @@ type Alert struct {
Annotations map[string]string
}
func NewData(groupLabels model.LabelSet, as ...*types.Alert) *Data {
// Data assembles data for template expansion.
func (t *Template) Data(recv string, groupLabels model.LabelSet, as ...*types.Alert) *Data {
alerts := types.Alerts(as...)
data := &Data{
Receiver: strings.SplitN(recv, "/", 2)[0],
Status: string(alerts.Status()),
Alerts: make([]Alert, 0, len(alerts)),
GroupLabels: map[string]string{},
CommonLabels: map[string]string{},
CommonAnnotations: map[string]string{},
ExternalURL: "something",
ExternalURL: t.ExternalURL.String(),
}
for _, a := range alerts {