feat(receiver): storage layer and webhook payload compat

Dual-dialect store (SQLite via modernc.org, MySQL via go-sql-driver,
both pure Go) with order-tolerant start/complete upserts, filtered and
paginated listing, and aggregate queries (per-day, top domains, query
types, statuses, duration percentiles, top clients). Sender gains
optional EXPLOREDNS_WEBHOOK_TOKEN bearer auth; a round-trip test pins
receiver structs byte-compatible with the sender payloads.

Note: go directive moves to 1.25.0, required by modernc.org/sqlite.
CI reads the version from go.mod so GOTOOLCHAIN=local stays satisfied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gary Hansen
2026-07-08 02:43:50 +10:00
co-authored by Claude Fable 5
parent d96f25b1d6
commit e8f56a1ef4
11 changed files with 1468 additions and 23 deletions
+8 -2
View File
@@ -52,19 +52,22 @@ type webhookCompleteEvent struct {
// single retry, and failures are logged but never surface to callers.
type webhookReporter struct {
url string
token string
client *http.Client
timeout time.Duration
retryDelay time.Duration
}
// newWebhookReporter returns a reporter for url, or nil when url is empty
// (webhook reporting disabled). A nil reporter is safe to call.
func newWebhookReporter(url string) *webhookReporter {
// (webhook reporting disabled). A nil reporter is safe to call. A non-empty
// token is sent as an Authorization bearer token on every delivery.
func newWebhookReporter(url, token string) *webhookReporter {
if url == "" {
return nil
}
return &webhookReporter{
url: url,
token: token,
client: &http.Client{},
timeout: 5 * time.Second,
retryDelay: 2 * time.Second,
@@ -106,6 +109,9 @@ func (wr *webhookReporter) post(event string, body []byte) error {
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ExploreDNS-Event", event)
if wr.token != "" {
req.Header.Set("Authorization", "Bearer "+wr.token)
}
resp, err := wr.client.Do(req)
if err != nil {