68 lines
2.0 KiB
Go
68 lines
2.0 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// NtfyNotifier posts alerts to an ntfy topic URL. Used to surface a
|
|
// degraded audit state (records buffered locally during a loki outage).
|
|
type NtfyNotifier struct {
|
|
topicURL string
|
|
token string
|
|
http *http.Client
|
|
}
|
|
|
|
// NewNtfyNotifier constructs a notifier for the given ntfy topic URL
|
|
// (e.g. https://ntfy.sh/my-topic). token is an optional bearer for
|
|
// protected ntfy instances; it is held here and only sent in the
|
|
// Authorization header, never logged. Returns nil when topicURL is empty.
|
|
func NewNtfyNotifier(topicURL, token string) *NtfyNotifier {
|
|
if topicURL == "" {
|
|
return nil
|
|
}
|
|
return &NtfyNotifier{
|
|
topicURL: strings.TrimRight(topicURL, "/"),
|
|
token: token,
|
|
http: &http.Client{Timeout: 10 * time.Second},
|
|
}
|
|
}
|
|
|
|
// Notify posts a message to the ntfy topic.
|
|
func (n *NtfyNotifier) Notify(ctx context.Context, msg string) error {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, n.topicURL, strings.NewReader(msg))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Title", "brain-capture audit degraded")
|
|
req.Header.Set("Priority", "high")
|
|
req.Header.Set("Tags", "warning,brain")
|
|
if n.token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+n.token)
|
|
}
|
|
resp, err := n.http.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("ntfy notify: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return &RejectedError{Status: resp.StatusCode}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RejectedError is a non-2xx ntfy response.
|
|
type RejectedError struct{ Status int }
|
|
|
|
func (e *RejectedError) Error() string { return fmt.Sprintf("ntfy notify: status %d", e.Status) }
|
|
|
|
// Permanent reports whether resending the same message cannot succeed: a 4xx
|
|
// other than 429 (rate limited). Callers may drop such a message; anything
|
|
// else (5xx, 429, transport errors) is an outage and must be retried.
|
|
func (e *RejectedError) Permanent() bool {
|
|
return e.Status >= 400 && e.Status < 500 && e.Status != http.StatusTooManyRequests
|
|
}
|