Skip to content

Logs

The API logs through Pino: readable lines on your machine, one JSON object per line in production.

While you develop:

[10:02:14.454] INFO: [Bootstrap] API listening on port 3000, under /api
[10:02:18.004] WARN: GET /api/me 401 {"responseTime":5}
[10:03:27.096] WARN: [Auth] User not found

The name in brackets is who logged it; a line without one is a request. In production:

{"level":"warn","time":1790517813195,"reqId":"3c15d9bd-…","req":{"method":"GET","url":"/api/me"},"res":{"statusCode":401},"responseTime":4,"msg":"GET /api/me 401"}

Filter on level error and fatal for failures, on reqId to follow one request, on context for one part of the API.

Use NestJS’s Logger; it goes through Pino:

private readonly logger = new Logger(ReportsService.name);
this.logger.log(`Report sent to workspace ${workspaceId}`);

Never log a secret, token, password, cookie or email body. Don’t log an error and rethrow it. Don’t use console.log in the API: it bypasses the logger and isn’t JSON in production.

Set LOG_LEVEL. Left empty it is debug while you develop, info in production and silent in tests.

Every response carries an x-request-id header, and the same id is on the request’s log line and on what your code logs while handling it. Ask the customer for it. An id from a proxy in front of the API is kept.

Better Auth’s lines (marked [Auth]) carry no id: find them by time.

With traces on, lines written during a request or a job also carry trace_id and span_id in production.

With error reporting on, the report of a failed request carries the same id.

Request lines have no headers and no query string, on purpose: those hold session cookies and sign-in tokens. Keep it that way if you change createHttpLogger in apps/api/src/logging/logger.ts; its test checks that secrets stay out.

/api/health is not logged, since hosts ask for it every few seconds.

When the AI assistant fails, the reason is printed by the AI SDK itself, outside Pino, so in production that line is not JSON.