// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package prober import ( "bytes" "encoding/json" "errors" "expvar" "fmt" "io" "net/http" "os" "strings" "time" "github.com/google/uuid" "tailscale.com/util/httpm" ) 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") } req, err := http.NewRequest(httpm.POST, webhookUrl, bytes.NewBuffer(sqBody)) 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 }