notification.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package prober
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "errors"
  8. "expvar"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "os"
  13. "strings"
  14. "time"
  15. "github.com/google/uuid"
  16. "tailscale.com/util/httpm"
  17. )
  18. var (
  19. alertGenerated = expvar.NewInt("alert_generated")
  20. alertFailed = expvar.NewInt("alert_failed")
  21. warningGenerated = expvar.NewInt("warning_generated")
  22. warningFailed = expvar.NewInt("warning_failed")
  23. )
  24. // SendAlert sends an alert to the incident response system, to
  25. // page a human responder immediately.
  26. // summary should be short and state the nature of the emergency.
  27. // details can be longer, up to 29 KBytes.
  28. func SendAlert(summary, details string) error {
  29. type squadcastAlert struct {
  30. Message string `json:"message"`
  31. Description string `json:"description"`
  32. Tags map[string]string `json:"tags,omitempty"`
  33. Status string `json:"status"`
  34. EventId string `json:"event_id"`
  35. }
  36. sqa := squadcastAlert{
  37. Message: summary,
  38. Description: details,
  39. Tags: map[string]string{"severity": "critical"},
  40. Status: "trigger",
  41. EventId: uuid.New().String(),
  42. }
  43. sqBody, err := json.Marshal(sqa)
  44. if err != nil {
  45. alertFailed.Add(1)
  46. return fmt.Errorf("encoding alert payload: %w", err)
  47. }
  48. webhookUrl := os.Getenv("SQUADCAST_WEBHOOK")
  49. if webhookUrl == "" {
  50. warningFailed.Add(1)
  51. return errors.New("no SQUADCAST_WEBHOOK configured")
  52. }
  53. req, err := http.NewRequest(httpm.POST, webhookUrl, bytes.NewBuffer(sqBody))
  54. if err != nil {
  55. alertFailed.Add(1)
  56. return err
  57. }
  58. req.Header.Set("Content-Type", "application/json")
  59. client := &http.Client{Timeout: 10 * time.Second}
  60. resp, err := client.Do(req)
  61. if err != nil {
  62. alertFailed.Add(1)
  63. return err
  64. }
  65. defer resp.Body.Close()
  66. if resp.StatusCode != 200 {
  67. alertFailed.Add(1)
  68. return errors.New(resp.Status)
  69. }
  70. body, _ := io.ReadAll(resp.Body)
  71. if string(body) != "ok" {
  72. alertFailed.Add(1)
  73. return errors.New("non-ok response returned from Squadcast")
  74. }
  75. alertGenerated.Add(1)
  76. return nil
  77. }
  78. // SendWarning will post a message to Slack.
  79. // details should be a description of the issue.
  80. func SendWarning(details string) error {
  81. webhookUrl := os.Getenv("SLACK_WEBHOOK")
  82. if webhookUrl == "" {
  83. warningFailed.Add(1)
  84. return errors.New("no SLACK_WEBHOOK configured")
  85. }
  86. type slackRequestBody struct {
  87. Text string `json:"text"`
  88. }
  89. slackBody, err := json.Marshal(slackRequestBody{Text: details})
  90. if err != nil {
  91. warningFailed.Add(1)
  92. return err
  93. }
  94. req, err := http.NewRequest("POST", webhookUrl, bytes.NewReader(slackBody))
  95. if err != nil {
  96. warningFailed.Add(1)
  97. return err
  98. }
  99. req.Header.Set("Content-Type", "application/json")
  100. client := &http.Client{Timeout: 10 * time.Second}
  101. resp, err := client.Do(req)
  102. if err != nil {
  103. warningFailed.Add(1)
  104. return err
  105. }
  106. defer resp.Body.Close()
  107. if resp.StatusCode != http.StatusOK {
  108. warningFailed.Add(1)
  109. return errors.New(resp.Status)
  110. }
  111. body, _ := io.ReadAll(resp.Body)
  112. if s := strings.TrimSpace(string(body)); s != "ok" {
  113. warningFailed.Add(1)
  114. return errors.New("non-ok response returned from Slack")
  115. }
  116. warningGenerated.Add(1)
  117. return nil
  118. }