173 lines
4.6 KiB
Go
173 lines
4.6 KiB
Go
// Copyright 2019 Prometheus Team
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package victorops
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
commoncfg "github.com/prometheus/common/config"
|
|
"github.com/prometheus/common/model"
|
|
|
|
"github.com/prometheus/alertmanager/config"
|
|
"github.com/prometheus/alertmanager/notify"
|
|
"github.com/prometheus/alertmanager/template"
|
|
"github.com/prometheus/alertmanager/types"
|
|
)
|
|
|
|
// https://help.victorops.com/knowledge-base/incident-fields-glossary/ - 20480 characters.
|
|
const maxMessageLenRunes = 20480
|
|
|
|
// Notifier implements a Notifier for VictorOps notifications.
|
|
type Notifier struct {
|
|
conf *config.VictorOpsConfig
|
|
tmpl *template.Template
|
|
logger *slog.Logger
|
|
client *http.Client
|
|
retrier *notify.Retrier
|
|
}
|
|
|
|
// New returns a new VictorOps notifier.
|
|
func New(c *config.VictorOpsConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
|
|
client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "victorops", httpOpts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Notifier{
|
|
conf: c,
|
|
tmpl: t,
|
|
logger: l,
|
|
client: client,
|
|
// Missing documentation therefore assuming only 5xx response codes are
|
|
// recoverable.
|
|
retrier: ¬ify.Retrier{},
|
|
}, nil
|
|
}
|
|
|
|
const (
|
|
victorOpsEventTrigger = "CRITICAL"
|
|
victorOpsEventResolve = "RECOVERY"
|
|
)
|
|
|
|
// Notify implements the Notifier interface.
|
|
func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
|
|
var err error
|
|
var (
|
|
data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
|
|
tmpl = notify.TmplText(n.tmpl, data, &err)
|
|
apiURL = n.conf.APIURL.Copy()
|
|
)
|
|
|
|
var apiKey string
|
|
if n.conf.APIKey != "" {
|
|
apiKey = string(n.conf.APIKey)
|
|
} else {
|
|
content, fileErr := os.ReadFile(n.conf.APIKeyFile)
|
|
if fileErr != nil {
|
|
return false, fmt.Errorf("failed to read API key from file: %w", fileErr)
|
|
}
|
|
apiKey = strings.TrimSpace(string(content))
|
|
}
|
|
|
|
apiURL.Path += fmt.Sprintf("%s/%s", apiKey, tmpl(n.conf.RoutingKey))
|
|
if err != nil {
|
|
return false, fmt.Errorf("templating error: %w", err)
|
|
}
|
|
|
|
buf, err := n.createVictorOpsPayload(ctx, as...)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
|
|
resp, err := notify.PostJSON(ctx, n.client, apiURL.String(), buf)
|
|
if err != nil {
|
|
return true, notify.RedactURL(err)
|
|
}
|
|
defer notify.Drain(resp)
|
|
|
|
shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
|
|
if err != nil {
|
|
return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
|
|
}
|
|
return shouldRetry, err
|
|
}
|
|
|
|
// Create the JSON payload to be sent to the VictorOps API.
|
|
func (n *Notifier) createVictorOpsPayload(ctx context.Context, as ...*types.Alert) (*bytes.Buffer, error) {
|
|
victorOpsAllowedEvents := map[string]bool{
|
|
"INFO": true,
|
|
"WARNING": true,
|
|
"CRITICAL": true,
|
|
}
|
|
|
|
key, err := notify.ExtractGroupKey(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
alerts = types.Alerts(as...)
|
|
data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
|
|
tmpl = notify.TmplText(n.tmpl, data, &err)
|
|
|
|
messageType = tmpl(n.conf.MessageType)
|
|
stateMessage = tmpl(n.conf.StateMessage)
|
|
)
|
|
|
|
if alerts.Status() == model.AlertFiring && !victorOpsAllowedEvents[messageType] {
|
|
messageType = victorOpsEventTrigger
|
|
}
|
|
|
|
if alerts.Status() == model.AlertResolved {
|
|
messageType = victorOpsEventResolve
|
|
}
|
|
|
|
stateMessage, truncated := notify.TruncateInRunes(stateMessage, maxMessageLenRunes)
|
|
if truncated {
|
|
n.logger.Warn("Truncated state_message", "incident", key, "max_runes", maxMessageLenRunes)
|
|
}
|
|
|
|
msg := map[string]string{
|
|
"message_type": messageType,
|
|
"entity_id": key.Hash(),
|
|
"entity_display_name": tmpl(n.conf.EntityDisplayName),
|
|
"state_message": stateMessage,
|
|
"monitoring_tool": tmpl(n.conf.MonitoringTool),
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("templating error: %w", err)
|
|
}
|
|
|
|
// Add custom fields to the payload.
|
|
for k, v := range n.conf.CustomFields {
|
|
msg[k] = tmpl(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("templating error: %w", err)
|
|
}
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &buf, nil
|
|
}
|