package alerts import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" ) // AlwaysOnDisplay sends each alert to the session API used by the wall // display. A session is created for the alert and then given a status and log // entry so the display can show both the current incident and its details. type AlwaysOnDisplay struct { URL string Client *http.Client } func (a *AlwaysOnDisplay) Name() string { return "always-on-display" } type aodSession struct { ID string `json:"id"` } func (a *AlwaysOnDisplay) Send(ctx context.Context, alert Alert) error { client := a.Client if client == nil { client = &http.Client{Timeout: 15 * time.Second} } base := strings.TrimRight(a.URL, "/") var session aodSession name := fmt.Sprintf("%s alert: %s", strings.ToUpper(string(alert.Priority())), alert.Spike.Service) if alert.Test { name = "TEST alert: " + alert.Spike.Service } if err := postJSONDecode(ctx, client, base+"/api/sessions", nil, map[string]string{"name": name}, &session); err != nil { return err } if session.ID == "" { return fmt.Errorf("always-on-display: session response did not include an id") } status := firstLine(alert.Text("")) if err := postJSON(ctx, client, base+"/api/sessions/"+session.ID+"/status", nil, map[string]string{"status": status}); err != nil { return err } level := "info" if alert.Priority() == PriorityHigh { level = "error" } else if alert.Priority() == PriorityMedium { level = "warning" } return postJSON(ctx, client, base+"/api/sessions/"+session.ID+"/log", nil, map[string]string{ "level": level, "message": alert.Text(""), }) } func firstLine(s string) string { if i := strings.IndexByte(s, '\n'); i >= 0 { return s[:i] } return s } func postJSONDecode(ctx context.Context, client *http.Client, target string, headers map[string]string, body any, out any) error { raw, err := json.Marshal(body) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, strings.NewReader(string(raw))) if err != nil { return fmt.Errorf("always-on-display: invalid request") } req.Header.Set("Content-Type", "application/json") for k, v := range headers { req.Header.Set(k, v) } resp, err := client.Do(req) if err != nil { return fmt.Errorf("always-on-display: request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode/100 != 2 { return fmt.Errorf("always-on-display: HTTP %d", resp.StatusCode) } if err := json.NewDecoder(resp.Body).Decode(out); err != nil { return fmt.Errorf("always-on-display: invalid session response: %w", err) } return nil }