From ae7eaa8e97342e6fa4078659b76942d978b8cac2 Mon Sep 17 00:00:00 2001 From: marcusDenslow Date: Fri, 1 May 2026 13:55:54 +0200 Subject: [PATCH] feat: route audit + recovery through slog levels Audit events now emit at Info/Warn/Error so the log-level counter has variety instead of being pinned to info. Panic recovery uses structured slog.Error with method/path/stack attrs. Co-Authored-By: Claude Opus 4.7 --- api/middleware/recovery.go | 9 +++- pkg/audit/audit.go | 94 +++++++++++++++++++++----------------- 2 files changed, 60 insertions(+), 43 deletions(-) diff --git a/api/middleware/recovery.go b/api/middleware/recovery.go index 69cc79e..cebf766 100644 --- a/api/middleware/recovery.go +++ b/api/middleware/recovery.go @@ -1,7 +1,7 @@ package middleware import ( - "log" + "log/slog" "net/http" "runtime/debug" @@ -13,7 +13,12 @@ func Recovery(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { - log.Printf("PANIC: %v\n%s", err, debug.Stack()) + slog.Error("panic recovered", + "error", err, + "method", r.Method, + "path", r.URL.Path, + "stack", string(debug.Stack()), + ) utils.RespondError( w, diff --git a/pkg/audit/audit.go b/pkg/audit/audit.go index bb687dd..d6f9753 100644 --- a/pkg/audit/audit.go +++ b/pkg/audit/audit.go @@ -1,12 +1,10 @@ -// Package audit writes structured JSON logs for financial events. +// Package audit writes structured logs for financial events. // Every card addition, order creation, payment, and refund is recorded here. // Pipe this output to your log aggregator (Datadog, Loki, CloudWatch, etc.). package audit import ( - "encoding/json" - "log" - "time" + "log/slog" ) type eventType string @@ -22,103 +20,117 @@ const ( ) type event struct { - Time time.Time `json:"time"` - Event eventType `json:"event"` - UserID uint `json:"user_id,omitempty"` - OrderID uint `json:"order_id,omitempty"` - CardID uint `json:"card_id,omitempty"` - Amount int `json:"amount_cents,omitempty"` - StripeID string `json:"stripe_id,omitempty"` - CardLast4 string `json:"card_last4,omitempty"` - CardBrand string `json:"card_brand,omitempty"` - Error string `json:"error,omitempty"` - Extra map[string]string `json:"extra,omitempty"` + Event eventType + UserID uint + OrderID uint + CardID uint + Amount int + StripeID string + CardLast4 string + CardBrand string + Error string } -func emit(e event) { - e.Time = time.Now().UTC() - b, err := json.Marshal(e) - if err != nil { - log.Printf("[audit] failed to marshal event: %v", err) - return +func (e event) attrs() []any { + a := []any{"event", string(e.Event)} + if e.UserID != 0 { + a = append(a, "user_id", e.UserID) } - log.Printf("[AUDIT] %s", b) + if e.OrderID != 0 { + a = append(a, "order_id", e.OrderID) + } + if e.CardID != 0 { + a = append(a, "card_id", e.CardID) + } + if e.Amount != 0 { + a = append(a, "amount_cents", e.Amount) + } + if e.StripeID != "" { + a = append(a, "stripe_id", e.StripeID) + } + if e.CardLast4 != "" { + a = append(a, "card_last4", e.CardLast4) + } + if e.CardBrand != "" { + a = append(a, "card_brand", e.CardBrand) + } + if e.Error != "" { + a = append(a, "error", e.Error) + } + return a } // CardAdded records that a user saved a new payment method. func CardAdded(userID, cardID uint, brand, last4 string) { - emit(event{ + slog.Info("audit", event{ Event: eventCardAdded, UserID: userID, CardID: cardID, CardBrand: brand, CardLast4: last4, - }) + }.attrs()...) } // CardDeleted records that a user removed a saved payment method. func CardDeleted(userID, cardID uint) { - emit(event{ + slog.Info("audit", event{ Event: eventCardDeleted, UserID: userID, CardID: cardID, - }) + }.attrs()...) } // OrderCreated records that an order record was created (before payment). func OrderCreated(userID, orderID uint, amountCents int) { - emit(event{ + slog.Info("audit", event{ Event: eventOrderCreated, UserID: userID, OrderID: orderID, Amount: amountCents, - }) + }.attrs()...) } // OrderPaid records a successful charge. func OrderPaid(userID, orderID uint, amountCents int, stripePaymentIntentID string) { - emit(event{ + slog.Info("audit", event{ Event: eventOrderPaid, UserID: userID, OrderID: orderID, Amount: amountCents, StripeID: stripePaymentIntentID, - }) + }.attrs()...) } -// OrderFailed records a failed charge attempt. +// OrderFailed records a failed charge attempt. Logged at WARN. func OrderFailed(userID, orderID uint, amountCents int, reason string) { - emit(event{ + slog.Warn("audit", event{ Event: eventOrderFailed, UserID: userID, OrderID: orderID, Amount: amountCents, Error: reason, - }) + }.attrs()...) } // OrderRefunded records a refund. func OrderRefunded(userID, orderID uint, amountCents int, stripeRefundID string) { - emit(event{ + slog.Info("audit", event{ Event: eventOrderRefunded, UserID: userID, OrderID: orderID, Amount: amountCents, StripeID: stripeRefundID, - }) + }.attrs()...) } // PaymentCritical records the dangerous state where Stripe charged the card // but the local database transaction failed. This MUST be investigated and -// manually reconciled. Emit a loud log line that monitoring can alert on. +// manually reconciled. Logged at ERROR so monitoring can alert on it. func PaymentCritical(orderID uint, stripePaymentIntentID string, err error) { - emit(event{ + slog.Error("audit", event{ Event: eventPaymentCritical, OrderID: orderID, StripeID: stripePaymentIntentID, Error: err.Error(), - }) - // Second line so it's impossible to miss in plain-text log tails. - log.Printf("[AUDIT][CRITICAL] order %d was charged (pi=%s) but DB commit failed: %v — REQUIRES MANUAL RECONCILIATION", - orderID, stripePaymentIntentID, err) + }.attrs()...) }