Skip to content

@tetsujs/request-log

@tetsujs/request-log writes a record for every request as its response goes out, and optionally another as it arrives. Each is a hook that hands the record to a write function you give it, so it fits any logger.

Terminal window
bun add @tetsujs/request-log
import { requestId } from "@tetsujs/request-id";
import { accessLog } from "@tetsujs/request-log";
const id = requestId();
const finished = accessLog({
write?: ((record: AccessRecord) => void) | undefined

Where the line goes. console.log by default.

Takes the record rather than a string so a structured logger can be handed the fields as they are.

write
: (
record: AccessRecord
record
) =>
const logger: pino.Logger<never, boolean>
logger
.
info: pino.LogFn
<AccessRecord, "request finished">(obj: AccessRecord & pino.LogFnFields, msg?: "request finished" | undefined) => void (+2 overloads)
info
(
record: AccessRecord
record
, "request finished") });
createApp({
hooks: { beforeParse: [id], afterResponse: [finished] },
routes,
});

accessLog() is one afterResponse hook. Every request, a 404 and a failure included, produces one record:

{ method: "GET", path: "/items/42", route: "/items/:id", status: 200, durationMs: 12.418, requestId: "…" }
Field
method the request method
path the pathname the client asked for; never the query
route the route as declared, /items/:id where path is /items/42; absent when no route matched
status the status of the response
durationMs milliseconds from ctx.startedAt to the response, rounded to the microsecond
thrown what was thrown, if anything was: an error’s own name, its class when it sets none, or the typeof of a value that is not an Error
aborted true when the connection closed before the response was ready; absent otherwise
requestId the id from requestId(), when it ran before this hook

Group a dashboard by route: it stays one value however many ids are requested. path is what to read on a 404, where there is no route.

A request whose handler hangs, or whose process dies, never gets an access record. arrivalLog() writes one as the request comes in, so there is a trace that it came at all:

import { arrivalLog } from "@tetsujs/request-log";
const arrived = arrivalLog({
write?: ((record: ArrivalRecord) => void) | undefined

Where the line goes. console.log by default.

Takes the record rather than a string so a structured logger can be handed the fields as they are.

write
: (
record: ArrivalRecord
record
) =>
const logger: pino.Logger<never, boolean>
logger
.
info: pino.LogFn
<ArrivalRecord, "request received">(obj: ArrivalRecord & pino.LogFnFields, msg?: "request received" | undefined) => void (+2 overloads)
info
(
record: ArrivalRecord
record
, "request received") });
createApp({
hooks: { beforeParse: [id, arrived], afterResponse: [finished] },
routes,
});
{ method: "GET", path: "/items/42", requestId: "…" }

It is one beforeParse hook and doubles the log lines, so mount it only where that trace is wanted. Its place in beforeParse matters: after requestId(), the record carries the id; after cors(), preflights get no record.

accessLog() and arrivalLog() take one option:

Option Default
write console.log receives each record

write receives the record object, not a string, so a structured logger gets the fields as they are. The package exports the types AccessRecord, ArrivalRecord, AccessLogOptions, ArrivalLogOptions, LogOptions, AccessLogHook and ArrivalLogHook.

  • No headers, bodies or query strings go into a record, and there is no option to add them. thrown names the error, never quotes its message; a minified build renames classes, and thrown with them. path carries what the client sent, so keep secrets out of URLs.
  • For the full error, pass reportError to createApp and join its reports to the records by ctx?.requestId. See Logging.
  • Records are written on the request’s path, so a write that blocks delays the response. Use a logger that buffers; see Logging.
  • durationMs does not include writing the response to the socket. For a stream, the record is written when the stream starts, so a long feed shows as a fast 200. onEnd of @tetsujs/sse reports how a stream actually ended.
  • aborted: true means the client left, or a forced stop cut the connection. The record keeps the status the server answered. For nginx’s view, use record.aborted ? 499 : record.status.

A record has what request metrics need (method, route, status and duration), so a metrics registry can be fed from the same write. Label by route, never by path: path creates a new series for every id, and for every path a scanner tries. The Metrics guide feeds a Prometheus histogram this way and serves it on a port of its own.