Route GORM's logger through slog and bound it (closes #178)
All checks were successful
check / check (push) Successful in 2m57s
All checks were successful
check / check (push) Successful in 2m57s
This commit was merged in pull request #182.
This commit is contained in:
83
README.md
83
README.md
@@ -1141,10 +1141,11 @@ the figure has headroom. `internal/middleware/accesslog_test.go`
|
||||
asserts it against 8 KB of client-chosen text in the path, in the
|
||||
query, and in each of `User-Agent`, `Referer` and `X-Request-Id`,
|
||||
including cases built from the characters the handlers escape, and
|
||||
against the widest line the service can be made to write: a 5xx that
|
||||
keeps its concrete path while all three header fields are also at their
|
||||
budget. Every case runs through both handlers `internal/logger` can
|
||||
select — the JSON one and the text one it installs on a tty — since the
|
||||
against the widest access log line the service can be made to write: a
|
||||
5xx that keeps its concrete path while all three header fields are also
|
||||
at their budget. Every case runs through both handlers
|
||||
`internal/logger` can select — the JSON one and the text one it
|
||||
installs on a tty — since the
|
||||
two do not escape alike and the ceiling is quoted unqualified. Measured
|
||||
over a real connection, the widest line is 1,972 bytes.
|
||||
|
||||
@@ -1213,6 +1214,33 @@ against what the handlers really emit, over roughly 3,000 code points on
|
||||
each, so an undercharged rune fails a test rather than quietly
|
||||
falsifying the ceiling.
|
||||
|
||||
**It covers GORM's statement logging as well.** GORM's own default
|
||||
logger printed the fully interpolated SQL — parameters and all — to
|
||||
standard output on every statement that returned an error, including a
|
||||
plain record-not-found, at a level no operator setting reached. Two of
|
||||
this service's lookups miss by design on unauthenticated routes: the
|
||||
entrypoint lookup behind `/webhook/{uuid}` and the user lookup behind
|
||||
the login form, whose path segment and submitted username the client
|
||||
picks outright. Every
|
||||
`gorm.Open` in the service now installs the adapter in
|
||||
`internal/gormlog` instead. It writes through the same `slog` logger as
|
||||
everything else, so its lines take the level the operator set and the
|
||||
handler `internal/logger` selected, and every value it emits is spent
|
||||
through the same `internal/logfield` budget. A record-not-found is not
|
||||
logged as an error: it is the expected outcome on both of those paths,
|
||||
and each handler already records its own miss at `DEBUG` — bounded, per
|
||||
the table above — without the SQL. Slow statements are kept, at `WARN`,
|
||||
above the same 200 ms threshold GORM used and with the statement
|
||||
bounded, because that report is the one thing GORM's logger gave an
|
||||
operator that nothing else here does. The adapter orders its cases
|
||||
exactly as GORM's own `Trace` orders them, so a statement that both
|
||||
missed and ran slow is still reported as slow, and dropping the miss
|
||||
costs an operator no report `IgnoreRecordNotFoundError` would have
|
||||
kept. A GORM line spends at most two of those budgets — the statement
|
||||
and the driver error — against a smaller fixed portion than the access
|
||||
log's, and `internal/gormlog/gormlog_test.go` asserts each line against
|
||||
`MaxAccessLogLineBytes` directly rather than leaving it as arithmetic.
|
||||
|
||||
What that ceiling does **not** cover, stated here so the figure is not
|
||||
read as more than it is:
|
||||
|
||||
@@ -1236,16 +1264,39 @@ read as more than it is:
|
||||
that type on a specific webhook, and each line it writes is bounded
|
||||
per event by the 1 MB receiver body cap. Adding one is a decision to
|
||||
spend log volume on that webhook's payloads.
|
||||
- **GORM's default logger**, which prints the fully interpolated SQL to
|
||||
stdout on every record-not-found — including the client-chosen path
|
||||
on `/webhook/{uuid}` and the submitted username on the login form.
|
||||
This one is not deliberate and not yet fixed; it does not go through
|
||||
`internal/logger` at all, so no level the operator sets and no budget
|
||||
above applies to it. Tracked at
|
||||
<https://git.eeqj.de/sneak/webhooker/issues/178>. Until it is fixed,
|
||||
an unauthenticated flood can still write text of its own choosing and
|
||||
its own length to the operator's stdout, and the ceiling above
|
||||
describes only the `slog` half of the picture.
|
||||
- **Two writers that do not go through `internal/logger` at all**, both
|
||||
on standard error. `fx` prints the dependency graph and the lifecycle
|
||||
hooks through its default console logger at startup and shutdown —
|
||||
nothing calls `fx.WithLogger`, and `fx.New` builds that logger over
|
||||
`os.Stderr`. The Go runtime writes a panic or a fatal error itself; a
|
||||
panic in a background worker rather than in a request handler is the
|
||||
case that reaches it, since nothing recovers those. Neither carries a
|
||||
client-chosen value at a client-chosen length: the five `panic` calls
|
||||
in this service are invariant guards over constants and over
|
||||
`crypto/rand`.
|
||||
- **`net/http`'s own faults**, which are _not_ a separate writer.
|
||||
`internal/server/http.go` builds its server with a nil `ErrorLog`, so
|
||||
`net/http` falls back to the `log` package's default logger — and
|
||||
`internal/logger` calls `slog.SetDefault`, which redirects that logger
|
||||
into whichever handler it installed. Those lines therefore arrive on
|
||||
standard output, shaped like every other line, at `INFO`. They are not
|
||||
truncated and they are not bounded by the ceiling: a handler panic
|
||||
arrives as one record carrying a whole goroutine stack, above the
|
||||
ceiling's 2,560 bytes — measured at roughly 2,770 in one checkout. The
|
||||
exact width is not an invariant, since it moves with the goroutine
|
||||
number and with the source paths baked into the stack; that it exceeds
|
||||
the ceiling does not move. The value is the runtime's, not a client's.
|
||||
- **A handler panic reaches that path** rather than the one it looks
|
||||
like it should. `internal/server/routes.go` installs chi's
|
||||
`middleware.Recoverer` in front of every route, which is meant to
|
||||
print the panic and its stack to standard error and answer 500. On the
|
||||
Go version this service builds against it does neither: chi v1.5.5's
|
||||
stack pretty-printer looks for a `panic(0x` frame that the runtime no
|
||||
longer emits, walks past the end of its own slice, and panics before
|
||||
writing a byte. That second panic escapes to `net/http`, which drops
|
||||
the connection and reports it through the nil `ErrorLog` above.
|
||||
Tracked separately in
|
||||
<https://git.eeqj.de/sneak/webhooker/issues/187>.
|
||||
|
||||
Every limiter here — receiver, login, and password change — identifies
|
||||
the client the same way, through one shared key function: the
|
||||
@@ -1485,6 +1536,10 @@ webhooker/
|
||||
│ │ └── webhook_db_manager.go # Per-webhook DB lifecycle manager
|
||||
│ ├── globals/
|
||||
│ │ └── globals.go # Build-time variables (appname, version, arch)
|
||||
│ ├── gormlog/
|
||||
│ │ └── gormlog.go # GORM's logger.Interface on top of slog, bounded
|
||||
│ ├── logfield/
|
||||
│ │ └── logfield.go # Encoded-byte budget for client-supplied log values
|
||||
│ ├── delivery/
|
||||
│ │ ├── engine.go # Event-driven delivery engine (channel + timer based)
|
||||
│ │ ├── circuit_breaker.go # Per-target circuit breaker for http/slack targets with retries
|
||||
|
||||
Reference in New Issue
Block a user