Implement deterministic incident keys, complete PD integration
This commit is contained in:
parent
d365519ed3
commit
c045a6285b
|
@ -150,9 +150,10 @@ func (d *Dispatcher) processAlert(alert *types.Alert, opts *RouteOpts) {
|
||||||
// common set of routing options applies.
|
// common set of routing options applies.
|
||||||
// It emits notifications in the specified intervals.
|
// It emits notifications in the specified intervals.
|
||||||
type aggrGroup struct {
|
type aggrGroup struct {
|
||||||
labels model.LabelSet
|
labels model.LabelSet
|
||||||
opts *RouteOpts
|
opts *RouteOpts
|
||||||
log log.Logger
|
routeFP model.Fingerprint
|
||||||
|
log log.Logger
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel func()
|
cancel func()
|
||||||
|
@ -210,6 +211,7 @@ func (ag *aggrGroup) run(nf notifyFunc) {
|
||||||
ctx = notify.WithNow(ctx, now)
|
ctx = notify.WithNow(ctx, now)
|
||||||
|
|
||||||
// Populate context with information needed along the pipeline.
|
// Populate context with information needed along the pipeline.
|
||||||
|
ctx = notify.WithGroupKey(ctx, ag.labels.Fingerprint()^ag.routeFP)
|
||||||
ctx = notify.WithGroupLabels(ctx, ag.labels)
|
ctx = notify.WithGroupLabels(ctx, ag.labels)
|
||||||
ctx = notify.WithDestination(ctx, ag.opts.SendTo)
|
ctx = notify.WithDestination(ctx, ag.opts.SendTo)
|
||||||
ctx = notify.WithRepeatInterval(ctx, ag.opts.RepeatInterval)
|
ctx = notify.WithRepeatInterval(ctx, ag.opts.RepeatInterval)
|
||||||
|
|
|
@ -231,13 +231,13 @@ type pagerDutyMessage struct {
|
||||||
ServiceKey string `json:"service_key"`
|
ServiceKey string `json:"service_key"`
|
||||||
EventType string `json:"event_type"`
|
EventType string `json:"event_type"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
IncidentKey uint64 `json:"incident_key"`
|
IncidentKey model.Fingerprint `json:"incident_key"`
|
||||||
Client string `json:"client,omitempty"`
|
Client string `json:"client,omitempty"`
|
||||||
ClientURL string `json:"client_url,omitempty"`
|
ClientURL string `json:"client_url,omitempty"`
|
||||||
Details map[string]string `json:"details"`
|
Details map[string]string `json:"details"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pd *PagerDuty) Notify(ctx context.Context, as ...*types.Alert) error {
|
func (n *PagerDuty) Notify(ctx context.Context, as ...*types.Alert) error {
|
||||||
// http://developer.pagerduty.com/documentation/integration/events/trigger
|
// http://developer.pagerduty.com/documentation/integration/events/trigger
|
||||||
alerts := types.Alerts(as...)
|
alerts := types.Alerts(as...)
|
||||||
|
|
||||||
|
@ -246,24 +246,56 @@ func (pd *PagerDuty) Notify(ctx context.Context, as ...*types.Alert) error {
|
||||||
eventType = pagerDutyEventResolve
|
eventType = pagerDutyEventResolve
|
||||||
}
|
}
|
||||||
|
|
||||||
|
key, ok := GroupKey(ctx)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("group key missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.With("incident", key).With("eventType", eventType).Debugln("notifying PagerDuty")
|
||||||
|
|
||||||
|
groupLabels, ok := GroupLabels(ctx)
|
||||||
|
if !ok {
|
||||||
|
log.Error("missing group labels")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Alerts model.Alerts
|
||||||
|
GroupLabels model.LabelSet
|
||||||
|
}{
|
||||||
|
Alerts: alerts,
|
||||||
|
GroupLabels: groupLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
tmpl := func(name string) (s string) {
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s, err = n.tmpl.ExecuteTextString(name, &data)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
msg := &pagerDutyMessage{
|
msg := &pagerDutyMessage{
|
||||||
ServiceKey: pd.conf.ServiceKey,
|
ServiceKey: n.conf.ServiceKey,
|
||||||
EventType: eventType,
|
EventType: eventType,
|
||||||
IncidentKey: 123,
|
IncidentKey: key,
|
||||||
Description: "",
|
Description: tmpl(n.conf.Templates.Description),
|
||||||
Details: nil,
|
Details: nil,
|
||||||
}
|
}
|
||||||
if eventType == pagerDutyEventTrigger {
|
if eventType == pagerDutyEventTrigger {
|
||||||
msg.Client = "Prometheus Alertmanager"
|
msg.Client = "Prometheus Alertmanager"
|
||||||
msg.ClientURL = ""
|
msg.ClientURL = ""
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
|
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := ctxhttp.Post(ctx, http.DefaultClient, pd.conf.URL, contentTypeJSON, &buf)
|
resp, err := ctxhttp.Post(ctx, http.DefaultClient, n.conf.URL, contentTypeJSON, &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ const (
|
||||||
keyRepeatInterval
|
keyRepeatInterval
|
||||||
keySendResolved
|
keySendResolved
|
||||||
keyGroupLabels
|
keyGroupLabels
|
||||||
|
keyGroupKey
|
||||||
keyNow
|
keyNow
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,6 +54,10 @@ func WithSendResolved(ctx context.Context, b bool) context.Context {
|
||||||
return context.WithValue(ctx, keySendResolved, b)
|
return context.WithValue(ctx, keySendResolved, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithGroupKey(ctx context.Context, fp model.Fingerprint) context.Context {
|
||||||
|
return context.WithValue(ctx, keyGroupKey, fp)
|
||||||
|
}
|
||||||
|
|
||||||
func WithGroupLabels(ctx context.Context, lset model.LabelSet) context.Context {
|
func WithGroupLabels(ctx context.Context, lset model.LabelSet) context.Context {
|
||||||
return context.WithValue(ctx, keyGroupLabels, lset)
|
return context.WithValue(ctx, keyGroupLabels, lset)
|
||||||
}
|
}
|
||||||
|
@ -76,6 +81,11 @@ func SendResolved(ctx context.Context) (bool, bool) {
|
||||||
return v, ok
|
return v, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GroupKey(ctx context.Context) (model.Fingerprint, bool) {
|
||||||
|
v, ok := ctx.Value(keyGroupKey).(model.Fingerprint)
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
func GroupLabels(ctx context.Context) (model.LabelSet, bool) {
|
func GroupLabels(ctx context.Context) (model.LabelSet, bool) {
|
||||||
v, ok := ctx.Value(keyGroupLabels).(model.LabelSet)
|
v, ok := ctx.Value(keyGroupLabels).(model.LabelSet)
|
||||||
return v, ok
|
return v, ok
|
||||||
|
|
45
route.go
45
route.go
|
@ -35,6 +35,8 @@ var DefaultRouteOpts = RouteOpts{
|
||||||
|
|
||||||
// A Route is a node that contains definitions of how to handle alerts.
|
// A Route is a node that contains definitions of how to handle alerts.
|
||||||
type Route struct {
|
type Route struct {
|
||||||
|
parent *Route
|
||||||
|
|
||||||
// The configuration parameters for matches of this route.
|
// The configuration parameters for matches of this route.
|
||||||
RouteOpts RouteOpts
|
RouteOpts RouteOpts
|
||||||
|
|
||||||
|
@ -49,13 +51,12 @@ type Route struct {
|
||||||
Routes []*Route
|
Routes []*Route
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRoute(cr *config.Route, parent *RouteOpts) *Route {
|
func NewRoute(cr *config.Route, parent *Route) *Route {
|
||||||
if parent == nil {
|
|
||||||
parent = &DefaultRouteOpts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create default and overwrite with configured settings.
|
// Create default and overwrite with configured settings.
|
||||||
opts := *parent
|
opts := DefaultRouteOpts
|
||||||
|
if parent != nil {
|
||||||
|
opts = parent.RouteOpts
|
||||||
|
}
|
||||||
|
|
||||||
if cr.SendTo != "" {
|
if cr.SendTo != "" {
|
||||||
opts.SendTo = cr.SendTo
|
opts.SendTo = cr.SendTo
|
||||||
|
@ -95,16 +96,18 @@ func NewRoute(cr *config.Route, parent *RouteOpts) *Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
route := &Route{
|
route := &Route{
|
||||||
|
parent: parent,
|
||||||
RouteOpts: opts,
|
RouteOpts: opts,
|
||||||
Matchers: matchers,
|
Matchers: matchers,
|
||||||
Continue: cr.Continue,
|
Continue: cr.Continue,
|
||||||
Routes: NewRoutes(cr.Routes, &opts),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
route.Routes = NewRoutes(cr.Routes, route)
|
||||||
|
|
||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRoutes(croutes []*config.Route, parent *RouteOpts) []*Route {
|
func NewRoutes(croutes []*config.Route, parent *Route) []*Route {
|
||||||
res := []*Route{}
|
res := []*Route{}
|
||||||
for _, cr := range croutes {
|
for _, cr := range croutes {
|
||||||
res = append(res, NewRoute(cr, parent))
|
res = append(res, NewRoute(cr, parent))
|
||||||
|
@ -131,6 +134,8 @@ func (r *Route) Match(lset model.LabelSet) []*RouteOpts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no child nodes were matches, the current node itself is
|
||||||
|
// a match.
|
||||||
if len(all) == 0 {
|
if len(all) == 0 {
|
||||||
all = append(all, &r.RouteOpts)
|
all = append(all, &r.RouteOpts)
|
||||||
}
|
}
|
||||||
|
@ -138,6 +143,30 @@ func (r *Route) Match(lset model.LabelSet) []*RouteOpts {
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Route) SquashMatchers() types.Matchers {
|
||||||
|
var res types.Matchers
|
||||||
|
res = append(res, r.Matchers...)
|
||||||
|
|
||||||
|
if r.parent == nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
pm := r.parent.SquashMatchers()
|
||||||
|
res = append(pm, res...)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) Fingerprint() model.Fingerprint {
|
||||||
|
lset := make(model.LabelSet, len(r.RouteOpts.GroupBy))
|
||||||
|
|
||||||
|
for ln := range r.RouteOpts.GroupBy {
|
||||||
|
lset[ln] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.SquashMatchers().Fingerprint() ^ lset.Fingerprint()
|
||||||
|
}
|
||||||
|
|
||||||
type RouteOpts struct {
|
type RouteOpts struct {
|
||||||
// The identifier of the associated notification configuration
|
// The identifier of the associated notification configuration
|
||||||
SendTo string
|
SendTo string
|
||||||
|
|
|
@ -74,7 +74,7 @@ routes:
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
def = DefaultRouteOpts
|
def = DefaultRouteOpts
|
||||||
tree = NewRoute(&ctree, &def)
|
tree = NewRoute(&ctree, nil)
|
||||||
)
|
)
|
||||||
lset := func(labels ...string) map[model.LabelName]struct{} {
|
lset := func(labels ...string) map[model.LabelName]struct{} {
|
||||||
s := map[model.LabelName]struct{}{}
|
s := map[model.LabelName]struct{}{}
|
||||||
|
|
|
@ -20,6 +20,8 @@ my text
|
||||||
my text
|
my text
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "pagerduty.default.description" }}{{ .GroupLabels }} [{{ len .Alerts }} instance{{ if gt (len .Alerts) 1 }}s{{ end }}]{{ end }}
|
||||||
|
|
||||||
{{ define "email.default.header" }}From: "Prometheus Alertmanager" <{{ .From }}>
|
{{ define "email.default.header" }}From: "Prometheus Alertmanager" <{{ .From }}>
|
||||||
To: {{ .To }}
|
To: {{ .To }}
|
||||||
Date: {{ .Date }}
|
Date: {{ .Date }}
|
||||||
|
@ -47,7 +49,7 @@ table.outer {
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
color: #202020;
|
color: #202020 !important;
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -91,7 +93,7 @@ ul {
|
||||||
}
|
}
|
||||||
.labels.column {
|
.labels.column {
|
||||||
max-width: 295px;
|
max-width: 295px;
|
||||||
color: #fff;
|
color: #fff !important;
|
||||||
background: #4f4f4f;
|
background: #4f4f4f;
|
||||||
}
|
}
|
||||||
.annotations.column {
|
.annotations.column {
|
||||||
|
|
|
@ -91,3 +91,13 @@ func (ms Matchers) Match(lset model.LabelSet) bool {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ms Matchers) Fingerprint() model.Fingerprint {
|
||||||
|
lset := make(model.LabelSet, 3*len(ms))
|
||||||
|
|
||||||
|
for _, m := range ms {
|
||||||
|
lset[model.LabelName(fmt.Sprintf("%s-%s-%s", m.Name, m.Value, m.isRegex))] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return lset.Fingerprint()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue