Logging
This document describes how API logs work today with Fastify and a shared Pino logger.
Implementation lives under apps/api/src/shared/kernel/ and request/response logging hooks are in apps/api/src/index.ts.
Core logger (logger.ts)
The app exports a single shared logger instance built with Pino.
- Levels (in order):
fatal,error,warn,info,debug,trace. - Active level comes from
LOG_LEVEL(defaults toinfo). - Format: JSON lines with
level,msg,time, plus base metadata:type: "log",hostname,pid.
Transports
- Files — logs are written under
logs/relative to the current working directory. A separate file stream exists for each level (fatal.log,error.log,warn.log,info.log,debug.log,trace.log). - Axiom (optional) — if
AXIOM_LOGS_ENABLED=trueand bothAXIOM_EVENTS_DATASET+AXIOM_EVENTS_TOKENare present, logs are also sent using@axiomhq/pino. If enabled but credentials are missing, the process prints a warning and continues without Axiom transport.
Other logger usage
The same shared logger is also used in:
env.tsfor env validation success/failure logs.sqs.tsfor SQS configuration warnings.cache.tsas the default logger when no custom logger is injected.- startup/shutdown paths in
index.ts(server start, graceful shutdown lifecycle).
Fastify HTTP logging (index.ts)
Fastify's built-in logger is disabled (logger: false), and the app performs structured request logging via hooks.
Request correlation (onRequest)
For each request:
requestIdis read fromx-request-idif present, otherwise generated viacrypto.randomUUID().- The response header
x-request-idis set immediately. requestStartedAtis stored usingperformance.now()for duration calculation.- The current OpenTelemetry span gets
request.id.
Request completion (onResponse)
After response completion, one http.request.completed line is emitted, except OPTIONS requests are skipped.
Metadata includes:
requestIdmethodpath(route pattern when available, otherwise raw URL)statusdurationMstrace_idspan_id
Level selection is status-based:
| HTTP status | Log level |
|---|---|
>= 500 | error |
>= 400 | warn |
| else | info |
The same onResponse hook also records http.server.request.duration metrics through OpenTelemetry.
Error handling (setErrorHandler)
Unhandled route errors are logged with http.request.error and include:
requestIdmethodpathstatusCode(normalized to at least500if missing/invalid)err(full Fastify/JS error object)trace_idspan_id
Current behavior logs error-handler events at error level. The client response body is:
{ "message": "Internal server error" }for>= 500{ "message": error.message }for4xx
If a request fails, you will usually see both:
http.request.error(error details)http.request.completed(final status + timing)
Environment variables (logging)
| Variable | Role |
|---|---|
LOG_LEVEL | Minimum Pino level (default info). |
AXIOM_LOGS_ENABLED | Set to true to enable Axiom transport when credentials exist. |
AXIOM_EVENTS_DATASET | Axiom dataset for log shipping. |
AXIOM_EVENTS_TOKEN | Axiom API token for log shipping. |
Related docs
- Error handling — how HTTP and domain errors are represented and returned.