From ed81db137e0a455291057f7f806904d71ca3dd51 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 7 Aug 2026 19:57:16 +0700 Subject: [PATCH] Implement the log delivery target (closes #70) --- internal/delivery/engine.go | 3 ++ internal/delivery/engine_test.go | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/internal/delivery/engine.go b/internal/delivery/engine.go index 674f33b..bb82b19 100644 --- a/internal/delivery/engine.go +++ b/internal/delivery/engine.go @@ -1006,8 +1006,11 @@ func (e *Engine) deliverLog( "webhook event delivered to log target", "delivery_id", d.ID, "event_id", d.EventID, + "webhook_id", d.Event.WebhookID, + "entrypoint_id", d.Event.EntrypointID, "target_id", d.TargetID, "target_name", d.Target.Name, + "outcome", database.DeliveryStatusDelivered, "method", d.Event.Method, "content_type", d.Event.ContentType, "body_length", len(d.Event.Body), diff --git a/internal/delivery/engine_test.go b/internal/delivery/engine_test.go index 00ce06b..13fb9d6 100644 --- a/internal/delivery/engine_test.go +++ b/internal/delivery/engine_test.go @@ -1,6 +1,7 @@ package delivery_test import ( + "bytes" "context" "database/sql" "encoding/json" @@ -435,6 +436,91 @@ func TestDeliverLog_ImmediateSuccess(t *testing.T) { assert.True(t, result.Success) } +func TestDeliverLog_StructuredLogFields(t *testing.T) { + t.Parallel() + + db := testWebhookDB(t) + + var logBuf bytes.Buffer + + e := delivery.NewTestEngine( + slog.New(slog.NewTextHandler( + &logBuf, + &slog.HandlerOptions{Level: slog.LevelDebug}, + )), + &http.Client{Timeout: 5 * time.Second}, + 1, + ) + + event := seedEvent(t, db, `{"log":"structured"}`) + + dlv := seedDelivery( + t, db, event.ID, uuid.New().String(), + database.DeliveryStatusPending, + ) + + d := &database.Delivery{ + EventID: event.ID, + TargetID: dlv.TargetID, + Status: database.DeliveryStatusPending, + Event: event, + Target: database.Target{ + Name: "structured-log", + Type: database.TargetTypeLog, + }, + } + d.ID = dlv.ID + + e.ExportDeliverLog(db, d) + + // The delivery is marked delivered and a success + // DeliveryResult with no HTTP status is recorded, + // mirroring the other target types' bookkeeping. + var updated database.Delivery + + require.NoError(t, db.First( + &updated, "id = ?", dlv.ID, + ).Error) + + assert.Equal(t, + database.DeliveryStatusDelivered, updated.Status, + "log target should immediately succeed", + ) + + var result database.DeliveryResult + + require.NoError(t, db.Where( + "delivery_id = ?", dlv.ID, + ).First(&result).Error) + + assert.True(t, result.Success) + assert.Equal(t, 0, result.StatusCode, + "log target should not have an HTTP status", + ) + + assertLogFields(t, logBuf.String(), event, "structured-log") +} + +// assertLogFields checks that a log target's structured +// log line carries the required fields: event id, +// webhook/entrypoint, target name, and outcome. +func assertLogFields( + t *testing.T, + logged string, + event database.Event, + targetName string, +) { + t.Helper() + + assert.Contains(t, logged, "event_id="+event.ID) + assert.Contains(t, logged, "webhook_id="+event.WebhookID) + assert.Contains(t, + logged, "entrypoint_id="+event.EntrypointID, + ) + assert.Contains(t, logged, "target_name="+targetName) + assert.Contains(t, logged, "outcome=delivered") +} + func TestDeliverHTTP_WithRetries_Success(t *testing.T) { t.Parallel() -- 2.49.1