Files
alertmanager-discord/pkg/alertforwarder/alertforwarder.go
T
Iain Sproat f2872d6dea Configurable logging levels (#16)
* Update screenshot to match current output
* Log level is configurable
* Alert name is provided in log line, where available.
* Capitalise Discord, check err in integration test, and other syntax issues
* Go formatting syntax fixes
* Log the common alert name or group alert name, in that precedence
2022-11-21 21:04:05 +00:00

222 lines
6.9 KiB
Go

package alertforwarder
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/specklesystems/alertmanager-discord/pkg/alertmanager"
"github.com/specklesystems/alertmanager-discord/pkg/discord"
"github.com/specklesystems/alertmanager-discord/pkg/logging"
"github.com/specklesystems/alertmanager-discord/pkg/prometheus"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
const (
maxLogLength = 1024
)
type AlertForwarderHandler struct {
af AlertForwarder
}
func NewAlertForwarderHandler(client *http.Client, webhookURL string, maximumBackoffElapsedTime time.Duration) *AlertForwarderHandler {
return &AlertForwarderHandler{
af: NewAlertForwarder(client, webhookURL, maximumBackoffElapsedTime),
}
}
func (h *AlertForwarderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.af.TransformAndForward(w, r)
}
type AlertForwarder struct {
client *discord.Client
}
func NewAlertForwarder(client *http.Client, webhookURL string, maximumBackoffElapsedTime time.Duration) AlertForwarder {
return AlertForwarder{
client: discord.NewClient(client, webhookURL, maximumBackoffElapsedTime),
}
}
func (af *AlertForwarder) groupAlerts(amo *alertmanager.Out) map[string][]alertmanager.Alert {
groupedAlerts := make(map[string][]alertmanager.Alert)
for _, alert := range amo.Alerts {
groupedAlerts[alert.Status] = append(groupedAlerts[alert.Status], alert)
}
return groupedAlerts
}
func (af *AlertForwarder) sendWebhook(correlationId string, amo *alertmanager.Out, w http.ResponseWriter) {
if len(amo.Alerts) < 1 {
log.Debug().
Str(logging.FieldKeyCorrelationId, correlationId).
Msg("There are no alerts within this notification. There is nothing to forward to Discord. Returning early...")
w.WriteHeader(http.StatusOK)
return
}
logger := zerolog.New(os.Stderr).With().
Timestamp().
Str(logging.FieldKeyCorrelationId, correlationId).Logger()
if amo.CommonLabels.Alertname != "" {
logger = logger.With().Str(logging.FieldKeyAlertName, amo.CommonLabels.Alertname).Logger()
} else if amo.GroupLabels.Alertname != "" {
logger = logger.With().Str(logging.FieldKeyAlertName, amo.GroupLabels.Alertname).Logger()
}
failedToPublishAtLeastOne := false
for status, alerts := range af.groupAlerts(amo) {
DO := TranslateAlertManagerToDiscord(status, amo, alerts)
logger.Info().
Str(logging.FieldKeyEventType, logging.EventTypeRequestSending).
Str(logging.FieldKeyCorrelationId, correlationId).
Msg("Sending HTTP request to Discord.")
res, err := af.client.PublishMessage(DO)
if err != nil {
err = fmt.Errorf("failed to publish message to Discord: %w", err)
logger.Error().
Str(logging.FieldKeyCorrelationId, correlationId).
Err(err).
Msg("Error when attempting to publish message to Discord.")
failedToPublishAtLeastOne = true
continue
}
logger.Info().
Str(logging.FieldKeyEventType, logging.EventTypeResponseReceived).
Str(logging.FieldKeyCorrelationId, correlationId).
Msg("HTTP response received from Discord")
if res.StatusCode < 200 || res.StatusCode > 399 {
failedToPublishAtLeastOne = true
continue
}
}
if failedToPublishAtLeastOne {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (af *AlertForwarder) sendRawPromAlertWarn(correlationId string) (*http.Response, error) {
warningMessage := `You have probably misconfigured this software.
We detected input in Prometheus Alert format but are expecting AlertManager format.
This program is intended to ingest alerts from alertmanager.
It is not a replacement for alertmanager, it is a
webhook target for it. Please read the README.md
for guidance on how to configure it for alertmanager
or https://prometheus.io/docs/alerting/latest/configuration/#webhook_config`
log.Warn().Msg(warningMessage)
DO := discord.Out{
Content: "",
Embeds: []discord.Embed{
{
Title: "You have misconfigured this software",
Description: warningMessage,
Color: discord.ColorGrey,
Fields: []discord.EmbedField{},
},
},
}
log.Info().
Str(logging.FieldKeyEventType, logging.EventTypeRequestSending).
Str(logging.FieldKeyCorrelationId, correlationId).
Msg("Sending HTTP request to Discord.")
res, err := af.client.PublishMessage(DO)
if err != nil {
return nil, fmt.Errorf("error encountered when publishing message to Discord: %w", err)
}
log.Info().
Str(logging.FieldKeyEventType, logging.EventTypeResponseReceived).
Str(logging.FieldKeyCorrelationId, correlationId).
Msg("HTTP response received from Discord")
return res, nil
}
func (af *AlertForwarder) TransformAndForward(w http.ResponseWriter, r *http.Request) {
correlationId := uuid.New().String()
log.Info().
Str(logging.FieldKeyHttpHost, r.Host).
Str(logging.FieldKeyHttpMethod, r.Method).
Str(logging.FieldKeyHttpPath, r.URL.Path).
Str(logging.FieldKeyEventType, logging.EventTypeRequestReceived).
Str(logging.FieldKeyCorrelationId, correlationId).
Msg("HTTP request received from AlertManager.")
defer log.Info().
Str(logging.FieldKeyEventType, logging.EventTypeResponseSending).
Str(logging.FieldKeyCorrelationId, correlationId).
Msg("Sending HTTP response to AlertManager.")
b, err := io.ReadAll(r.Body)
if err != nil {
log.Error().
Str(logging.FieldKeyCorrelationId, correlationId).
Err(err).
Msg("Unable to read request body.")
w.WriteHeader(http.StatusInternalServerError)
return
}
amo := alertmanager.Out{}
err = json.Unmarshal(b, &amo)
if err != nil {
af.handleInvalidInput(correlationId, b, w)
return
}
af.sendWebhook(correlationId, &amo, w)
}
func (af *AlertForwarder) handleInvalidInput(correlationId string, b []byte, w http.ResponseWriter) {
if prometheus.IsAlert(b) {
log.Info().
Str(logging.FieldKeyCorrelationId, correlationId).
Msg("Detected a Prometheus Alert, and not an AlertManager alert, has been sent within the http request. This indicates a misconfiguration. Attempting to send a message to notify the Discord channel of the misconfiguration.")
res, err := af.sendRawPromAlertWarn(correlationId)
if err != nil || (res != nil && res.StatusCode < 200 || res.StatusCode > 399) {
statusCode := 0
if res != nil {
statusCode = res.StatusCode
}
log.Error().
Err(err).
Str(logging.FieldKeyCorrelationId, correlationId).
Int(logging.FieldKeyStatusCode, statusCode).
Msg("Error when attempting to send a warning message to Discord.")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusUnprocessableEntity)
return
}
if len(b) > maxLogLength-3 {
log.Info().
Str(logging.FieldKeyCorrelationId, correlationId).
Msgf("Failed to unpack inbound alert request - %s...", string(b[:maxLogLength-3]))
} else {
log.Info().
Str(logging.FieldKeyCorrelationId, correlationId).
Msgf("Failed to unpack inbound alert request - %s", string(b))
}
w.WriteHeader(http.StatusBadRequest)
}