Compare commits
1
Commits
next
...
e3c632c793
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3c632c793 |
@@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-02: a Mattermost webhook that answers an HTTP error is logged as
|
||||||
|
`mattermost notification failed`, not as a Slack failure (closes #227).
|
||||||
- 2026-10-02: durations in the log are written as text such as `2m0s`, not as a
|
- 2026-10-02: durations in the log are written as text such as `2m0s`, not as a
|
||||||
bare count of nanoseconds (closes #228).
|
bare count of nanoseconds (closes #228).
|
||||||
- 2026-10-02: a watched name whose nameservers answer with a CNAME and no
|
- 2026-10-02: a watched name whose nameservers answer with a CNAME and no
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -413,7 +415,8 @@ func sendSlackInfo(
|
|||||||
svc *notify.Service, target *url.URL,
|
svc *notify.Service, target *url.URL,
|
||||||
) error {
|
) error {
|
||||||
return svc.SendSlack(
|
return svc.SendSlack(
|
||||||
context.Background(), target, "t", "m", prioInfo,
|
context.Background(), target, notify.ErrSlackFailed,
|
||||||
|
"t", "m", prioInfo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,6 +509,7 @@ func TestSendSlackPayloadFields(t *testing.T) {
|
|||||||
err := svc.SendSlack(
|
err := svc.SendSlack(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
webhookURL,
|
webhookURL,
|
||||||
|
notify.ErrSlackFailed,
|
||||||
"Alert Title",
|
"Alert Title",
|
||||||
"Alert body text",
|
"Alert body text",
|
||||||
"warning",
|
"warning",
|
||||||
@@ -608,7 +612,8 @@ func TestSendSlackAllColors(t *testing.T) {
|
|||||||
|
|
||||||
err := svc.SendSlack(
|
err := svc.SendSlack(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
webhookURL, "t", "m", tc.priority,
|
webhookURL, notify.ErrSlackFailed,
|
||||||
|
"t", "m", tc.priority,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SendSlack error: %v", err)
|
t.Fatalf("SendSlack error: %v", err)
|
||||||
@@ -659,7 +664,8 @@ func TestSendSlackNetworkError(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
err := svc.SendSlack(
|
err := svc.SendSlack(
|
||||||
context.Background(), webhookURL, "t", "m", "info",
|
context.Background(), webhookURL, notify.ErrSlackFailed,
|
||||||
|
"t", "m", "info",
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for network failure")
|
t.Fatal("expected error for network failure")
|
||||||
@@ -1028,6 +1034,66 @@ func TestSendNotificationMattermostError(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSendNotificationErrorNamesEndpoint verifies that, with both
|
||||||
|
// Slack and Mattermost set, a failed delivery's logged error names
|
||||||
|
// the endpoint that failed. Both are sent by the Slack sender.
|
||||||
|
func TestSendNotificationErrorNamesEndpoint(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
srv := httptest.NewServer(
|
||||||
|
http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
target, _ := url.Parse(srv.URL)
|
||||||
|
|
||||||
|
svc, logs := newLoggingService(http.DefaultTransport)
|
||||||
|
svc.SetSlackWebhookURL(target)
|
||||||
|
svc.SetMattermostWebhookURL(target)
|
||||||
|
svc.SetSleepFunc(instantSleep)
|
||||||
|
svc.SetRetryConfig(notify.RetryConfig{
|
||||||
|
MaxRetries: 1,
|
||||||
|
BaseDelay: time.Millisecond,
|
||||||
|
MaxDelay: time.Millisecond,
|
||||||
|
})
|
||||||
|
|
||||||
|
svc.SendNotification(
|
||||||
|
context.Background(), "t", "m", prioError,
|
||||||
|
)
|
||||||
|
|
||||||
|
waitForCondition(t, func() bool {
|
||||||
|
return svc.OutstandingDeliveries() == 0
|
||||||
|
})
|
||||||
|
|
||||||
|
got := map[string]string{}
|
||||||
|
|
||||||
|
for line := range strings.Lines(logs.String()) {
|
||||||
|
var record struct {
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = json.Unmarshal([]byte(line), &record)
|
||||||
|
|
||||||
|
if record.Msg == "failed to send notification after retries" {
|
||||||
|
got[record.Endpoint] = record.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
want := map[string]string{
|
||||||
|
"slack": "slack notification failed: status 503",
|
||||||
|
"mattermost": "mattermost notification failed: status 503",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !maps.Equal(got, want) {
|
||||||
|
t.Errorf("logged errors = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── SlackPayload JSON marshaling ──────────────────────────
|
// ── SlackPayload JSON marshaling ──────────────────────────
|
||||||
|
|
||||||
func TestSlackPayloadJSON(t *testing.T) {
|
func TestSlackPayloadJSON(t *testing.T) {
|
||||||
|
|||||||
@@ -85,10 +85,11 @@ func (svc *Service) SendNtfy(
|
|||||||
func (svc *Service) SendSlack(
|
func (svc *Service) SendSlack(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
webhookURL *url.URL,
|
webhookURL *url.URL,
|
||||||
|
failed error,
|
||||||
title, message, priority string,
|
title, message, priority string,
|
||||||
) error {
|
) error {
|
||||||
return svc.sendSlack(
|
return svc.sendSlack(
|
||||||
ctx, webhookURL, title, message, priority,
|
ctx, webhookURL, failed, title, message, priority,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,7 +277,8 @@ func (svc *Service) dispatchSlack(
|
|||||||
|
|
||||||
svc.dispatch(ctx, "slack", func(c context.Context) error {
|
svc.dispatch(ctx, "slack", func(c context.Context) error {
|
||||||
return svc.sendSlack(
|
return svc.sendSlack(
|
||||||
c, svc.slackWebhookURL, title, message, priority,
|
c, svc.slackWebhookURL, ErrSlackFailed,
|
||||||
|
title, message, priority,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -294,7 +295,7 @@ func (svc *Service) dispatchMattermost(
|
|||||||
ctx, "mattermost",
|
ctx, "mattermost",
|
||||||
func(c context.Context) error {
|
func(c context.Context) error {
|
||||||
return svc.sendSlack(
|
return svc.sendSlack(
|
||||||
c, svc.mattermostWebhookURL,
|
c, svc.mattermostWebhookURL, ErrMattermostFailed,
|
||||||
title, message, priority,
|
title, message, priority,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -370,9 +371,14 @@ type SlackAttachment struct {
|
|||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendSlack posts to a Slack or Mattermost incoming webhook, which
|
||||||
|
// take the same payload. An HTTP error status is returned wrapped in
|
||||||
|
// failed, ErrSlackFailed or ErrMattermostFailed, so the error names
|
||||||
|
// the endpoint.
|
||||||
func (svc *Service) sendSlack(
|
func (svc *Service) sendSlack(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
webhookURL *url.URL,
|
webhookURL *url.URL,
|
||||||
|
failed error,
|
||||||
title, message, priority string,
|
title, message, priority string,
|
||||||
) error {
|
) error {
|
||||||
ctx, cancel := context.WithTimeout(
|
ctx, cancel := context.WithTimeout(
|
||||||
@@ -420,7 +426,7 @@ func (svc *Service) sendSlack(
|
|||||||
if resp.StatusCode >= httpStatusClientError {
|
if resp.StatusCode >= httpStatusClientError {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"%w: status %d",
|
"%w: status %d",
|
||||||
ErrSlackFailed, resp.StatusCode,
|
failed, resp.StatusCode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user