2023-01-27 21:37:20 +00:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2022-10-07 07:13:49 +01:00
|
|
|
|
|
|
|
package prober
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"expvar"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
2023-01-27 03:35:26 +00:00
|
|
|
"tailscale.com/util/httpm"
|
2022-10-07 07:13:49 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
alertGenerated = expvar.NewInt("alert_generated")
|
|
|
|
alertFailed = expvar.NewInt("alert_failed")
|
|
|
|
warningGenerated = expvar.NewInt("warning_generated")
|
|
|
|
warningFailed = expvar.NewInt("warning_failed")
|
|
|
|
)
|
|
|
|
|
|
|
|
// SendAlert sends an alert to the incident response system, to
|
|
|
|
// page a human responder immediately.
|
|
|
|
// summary should be short and state the nature of the emergency.
|
|
|
|
// details can be longer, up to 29 KBytes.
|
|
|
|
func SendAlert(summary, details string) error {
|
|
|
|
type squadcastAlert struct {
|
|
|
|
Message string `json:"message"`
|
|
|
|
Description string `json:"description"`
|
|
|
|
Tags map[string]string `json:"tags,omitempty"`
|
|
|
|
Status string `json:"status"`
|
|
|
|
EventId string `json:"event_id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
sqa := squadcastAlert{
|
|
|
|
Message: summary,
|
|
|
|
Description: details,
|
|
|
|
Tags: map[string]string{"severity": "critical"},
|
|
|
|
Status: "trigger",
|
|
|
|
EventId: uuid.New().String(),
|
|
|
|
}
|
|
|
|
|
|
|
|
sqBody, err := json.Marshal(sqa)
|
|
|
|
if err != nil {
|
|
|
|
alertFailed.Add(1)
|
|
|
|
return fmt.Errorf("encoding alert payload: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
webhookUrl := os.Getenv("SQUADCAST_WEBHOOK")
|
|
|
|
if webhookUrl == "" {
|
|
|
|
warningFailed.Add(1)
|
|
|
|
return errors.New("no SQUADCAST_WEBHOOK configured")
|
|
|
|
}
|
|
|
|
|
2023-01-27 03:35:26 +00:00
|
|
|
req, err := http.NewRequest(httpm.POST, webhookUrl, bytes.NewBuffer(sqBody))
|
2022-10-07 07:13:49 +01:00
|
|
|
if err != nil {
|
|
|
|
alertFailed.Add(1)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
alertFailed.Add(1)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
alertFailed.Add(1)
|
|
|
|
return errors.New(resp.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
if string(body) != "ok" {
|
|
|
|
alertFailed.Add(1)
|
|
|
|
return errors.New("non-ok response returned from Squadcast")
|
|
|
|
}
|
|
|
|
|
|
|
|
alertGenerated.Add(1)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SendWarning will post a message to Slack.
|
|
|
|
// details should be a description of the issue.
|
|
|
|
func SendWarning(details string) error {
|
|
|
|
webhookUrl := os.Getenv("SLACK_WEBHOOK")
|
|
|
|
if webhookUrl == "" {
|
|
|
|
warningFailed.Add(1)
|
|
|
|
return errors.New("no SLACK_WEBHOOK configured")
|
|
|
|
}
|
|
|
|
|
|
|
|
type slackRequestBody struct {
|
|
|
|
Text string `json:"text"`
|
|
|
|
}
|
|
|
|
|
|
|
|
slackBody, err := json.Marshal(slackRequestBody{Text: details})
|
|
|
|
if err != nil {
|
|
|
|
warningFailed.Add(1)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("POST", webhookUrl, bytes.NewReader(slackBody))
|
|
|
|
if err != nil {
|
|
|
|
warningFailed.Add(1)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
warningFailed.Add(1)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
warningFailed.Add(1)
|
|
|
|
return errors.New(resp.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
if s := strings.TrimSpace(string(body)); s != "ok" {
|
|
|
|
warningFailed.Add(1)
|
|
|
|
return errors.New("non-ok response returned from Slack")
|
|
|
|
}
|
|
|
|
warningGenerated.Add(1)
|
|
|
|
return nil
|
|
|
|
}
|