Skip to main content

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 to info).
  • Format: JSON lines with level, msg, time, plus base metadata: type: "log", hostname, pid.

Transports

  1. 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).
  2. Axiom (optional) — if AXIOM_LOGS_ENABLED=true and both AXIOM_EVENTS_DATASET + AXIOM_EVENTS_TOKEN are 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.ts for env validation success/failure logs.
  • sqs.ts for SQS configuration warnings.
  • cache.ts as 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:

  • requestId is read from x-request-id if present, otherwise generated via crypto.randomUUID().
  • The response header x-request-id is set immediately.
  • requestStartedAt is stored using performance.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:

  • requestId
  • method
  • path (route pattern when available, otherwise raw URL)
  • status
  • durationMs
  • trace_id
  • span_id

Level selection is status-based:

HTTP statusLog level
>= 500error
>= 400warn
elseinfo

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:

  • requestId
  • method
  • path
  • statusCode (normalized to at least 500 if missing/invalid)
  • err (full Fastify/JS error object)
  • trace_id
  • span_id

Current behavior logs error-handler events at error level. The client response body is:

  • { "message": "Internal server error" } for >= 500
  • { "message": error.message } for 4xx

If a request fails, you will usually see both:

  • http.request.error (error details)
  • http.request.completed (final status + timing)

Environment variables (logging)

VariableRole
LOG_LEVELMinimum Pino level (default info).
AXIOM_LOGS_ENABLEDSet to true to enable Axiom transport when credentials exist.
AXIOM_EVENTS_DATASETAxiom dataset for log shipping.
AXIOM_EVENTS_TOKENAxiom API token for log shipping.
  • Error handling — how HTTP and domain errors are represented and returned.