diff --git a/webhook/webhook.go b/webhook/webhook.go index 300e81c..324f3c7 100644 --- a/webhook/webhook.go +++ b/webhook/webhook.go @@ -51,9 +51,10 @@ type dispatcher struct { secret string // HMAC-SHA256 pre-shared secret (empty = no sig) // Dead letter queue: stores last N failed events for retry/inspection - dlqMu sync.Mutex - dlqItems []*Event - dlqMax int + dlqMu sync.Mutex + dlqItems []*Event + dlqMax int + dlqDropped atomic.Uint64 // events evicted from DLQ ring } const ( @@ -199,6 +200,8 @@ func (d *dispatcher) addToDLQ(ev *Event) { defer d.dlqMu.Unlock() if len(d.dlqItems) >= d.dlqMax { d.dlqItems = d.dlqItems[1:] // drop oldest + d.dlqDropped.Add(1) + slog.Warn("registry webhook DLQ full, dropping oldest event", "dlq_max", d.dlqMax) } d.dlqItems = append(d.dlqItems, ev) } @@ -360,21 +363,23 @@ func (st *Store) HandleGetWebhook() map[string]interface{} { st.mu.RUnlock() url := "" - var delivered, failed, dropped uint64 + var delivered, failed, dropped, dlqDropped uint64 dlqLen := 0 if d != nil { url = d.url delivered, failed, dropped = d.stats() + dlqDropped = d.dlqDropped.Load() dlqLen = len(d.getDLQ()) } return map[string]interface{}{ - "type": "get_webhook_ok", - "enabled": d != nil, - "url": url, - "delivered": delivered, - "failed": failed, - "dropped": dropped, - "dlq_size": dlqLen, + "type": "get_webhook_ok", + "enabled": d != nil, + "url": url, + "delivered": delivered, + "failed": failed, + "dropped": dropped, + "dlq_size": dlqLen, + "dlq_dropped": dlqDropped, } } diff --git a/webhook/zz_more_test.go b/webhook/zz_more_test.go index 9469f3b..430d330 100644 --- a/webhook/zz_more_test.go +++ b/webhook/zz_more_test.go @@ -139,6 +139,10 @@ func TestDispatcher_AddToDLQ_DropsOldestWhenFull(t *testing.T) { if got[0].EventID != 3 || got[1].EventID != 4 { t.Errorf("DLQ contents = %v, want oldest dropped", got) } + // Verify the dlqDropped counter tracks evictions (5 pushes into cap 2 = 3 drops). + if n := d.dlqDropped.Load(); n != 3 { + t.Errorf("dlqDropped = %d, want 3", n) + } } func TestDispatcher_EmitAfterCloseIsNoop(t *testing.T) {