When something goes wrong in production the only trace is console.error inside the handler's catch in src/server.ts — a 403 that is not same-origin, a 413, a 415, a 400 on a bad form, a 429 from the rate limiter and every 404 leave no line at all. An operator watching the forge's log cannot tell a quiet afternoon from a shop refusing every POST, and cannot see a page that got slow.
Done looks like
[ ] One line per response that is not a 2xx or a 3xx, and one per request slower than 200 ms, written from the one place every response already leaves through (finish in src/server.ts) so nothing can miss it.
[ ] The line is fixed fields, easy to read and to grep: method, route pattern (/orders/:number, never the filled-in path), status, duration in ms, and the role — anon, user, admin — and nothing else.
[ ] Nothing a caller typed reaches the log: no query string, no form field, no cookie, no header, no order number, no email, no token. The route pattern is ours; the path is theirs.
[ ] The handler's catch keeps its stack trace but loses the raw path, and says the same route pattern.
[ ] Logging is off in tests unless asked for, so bun test stays readable — an env flag or a module-level switch, set once in src/fresh.ts if that is the tidier seam.
[ ] A test asserts: a 404 writes a line; a 200 does not; a 413 line contains no part of the body; a request to /orders/BZ-ABC?token=… logs /orders/:number and neither the number nor the token.
[ ] The README says what the log lines mean and how to read them beside FORGE_APP_ENV.
Filessrc/server.ts, a small src/log.ts if the formatting wants a home of its own, src/routes.ts only if the pattern has to travel with the match, src/server.test.ts, README.md.
Risks A log line per request is a write on the fast path — keep it to one console.log of a prepared string, after the response is built, and never JSON.stringify a whole object. Do not log 2xx pages: the point is refusals, failures and slowness. The rate-limited path must not become a way to fill the log faster than it is capped.
Suggested mason, with sentinel reading the pull — a log is where secrets leak.
When something goes wrong in production the only trace is
console.errorinside the handler'scatchinsrc/server.ts— a 403 that is not same-origin, a 413, a 415, a 400 on a bad form, a 429 from the rate limiter and every 404 leave no line at all. An operator watching the forge's log cannot tell a quiet afternoon from a shop refusing every POST, and cannot see a page that got slow.Done looks like
finishinsrc/server.ts) so nothing can miss it./orders/:number, never the filled-in path), status, duration in ms, and the role —anon,user,admin— and nothing else.catchkeeps its stack trace but loses the raw path, and says the same route pattern.bun teststays readable — an env flag or a module-level switch, set once insrc/fresh.tsif that is the tidier seam./orders/BZ-ABC?token=…logs/orders/:numberand neither the number nor the token.FORGE_APP_ENV.Files
src/server.ts, a smallsrc/log.tsif the formatting wants a home of its own,src/routes.tsonly if the pattern has to travel with the match,src/server.test.ts,README.md.Risks A log line per request is a write on the fast path — keep it to one
console.logof a prepared string, after the response is built, and never JSON.stringify a whole object. Do not log 2xx pages: the point is refusals, failures and slowness. The rate-limited path must not become a way to fill the log faster than it is capped.Suggested mason, with sentinel reading the pull — a log is where secrets leak.