Keep the entrypoint UUID out of webhooker's own logs (closes #531)
check / check (push) Waiting to run

On the receiver route the access log and the per-entrypoint 429 line
name the route pattern whatever the status. A request chi never routed,
such as a dev-mode CORS preflight to the receiver, logs (unmatched).
The access log keeps only the scheme and host of a Referer. The
received line names the entrypoint and webhook IDs, and the
unknown-entrypoint line logs no path. The pattern is
middleware.ReceiverRoute, which routes.go registers the receiver with.

The README says the proxy's access log holds the UUID and must be
protected like the URL, and that a log target writes the request's
headers as received. Its access-log, rate-limit, Sentry and log-size
passages now match the code.

Model: opus-5-5
This commit is contained in:
2026-10-15 12:23:17 +00:00
parent c0f7cc29fd
commit ebc8ce2463
10 changed files with 380 additions and 170 deletions
+84
View File
@@ -12,12 +12,15 @@ import (
"testing"
"github.com/go-chi/chi"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm/clause"
"sneak.berlin/go/webhooker/internal/config"
"sneak.berlin/go/webhooker/internal/database"
"sneak.berlin/go/webhooker/internal/handlers"
"sneak.berlin/go/webhooker/internal/middleware"
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
)
// TestHandleWebhook_LogsClientNextToThePeer checks that the
@@ -99,6 +102,87 @@ func TestHandleWebhook_LogsClientNextToThePeer(t *testing.T) {
}
}
// TestHandleWebhook_LogsNoEntrypointUUID sends an accepted event, a
// request over the receiver rate limit and a request for an unknown
// UUID through the receiver as routes.go mounts it, and checks that
// nothing logged, at any level, carries either UUID.
func TestHandleWebhook_LogsNoEntrypointUUID(t *testing.T) {
t.Parallel()
var (
h *handlers.Handlers
db *database.Database
)
cfg := &config.Config{DataDir: t.TempDir(), ReceiverRateLimit: 1}
app := newTestAppWithConfig(t, cfg, &h, &db)
app.RequireStart()
t.Cleanup(app.RequireStop)
buf := new(bytes.Buffer)
log := slog.New(slog.NewJSONHandler(
buf, &slog.HandlerOptions{Level: slog.LevelDebug},
))
h.SetLogForTest(log)
mw := middlewaretest.New(t, log, cfg, nil)
webhook := seedWebhook(t, db)
entrypoint := &database.Entrypoint{
WebhookID: webhook.ID,
Path: uuid.New().String(),
Active: true,
}
require.NoError(t, db.DB().Omit(clause.Associations).
Create(entrypoint).Error)
unknownUUID := uuid.New().String()
router := chi.NewRouter()
router.Use(mw.Logging())
router.With(mw.ReceiverRateLimit()).HandleFunc(
middleware.ReceiverRoute, h.HandleWebhook(),
)
// In order: the limit is one request per minute per path, so the
// second request for the known UUID is the rejected one.
for _, step := range []struct {
entrypointUUID string
wantStatus int
}{
{entrypoint.Path, http.StatusOK},
{entrypoint.Path, http.StatusTooManyRequests},
{unknownUUID, http.StatusNotFound},
} {
req := httptest.NewRequestWithContext(
context.Background(), http.MethodPost,
"/h/"+step.entrypointUUID, strings.NewReader("{}"),
)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, step.wantStatus, w.Code)
}
// The lines this test is about were all written, so the checks
// below are not passing on an empty log.
for _, msg := range []string{
"webhook request received",
"webhook receiver rate limit exceeded",
"entrypoint not found",
} {
assert.Contains(t, buf.String(), `"msg":"`+msg+`"`)
}
assert.NotContains(t, buf.String(), entrypoint.Path)
assert.NotContains(t, buf.String(), unknownUUID)
line := receivedLine(t, buf)
assert.Equal(t, entrypoint.ID, line["entrypoint_id"])
assert.Equal(t, webhook.ID, line["webhook_id"])
}
// receivedLine returns the one "webhook request received" line in the
// captured JSON log.
func receivedLine(t *testing.T, buf *bytes.Buffer) map[string]any {