Build Elevate

Logging

Structured, high-performance logging with Pino for the API and other Node services

Logger Package

The @workspace/logger package provides a shared, structured logger built on Pino. It emits machine-readable JSON in production (for log collectors on Kubernetes) and pretty, colorized output in local development — all from a single configured instance imported across Node apps.

Overview

  • Package: @workspace/logger
  • Location: packages/logger
  • Engine: Pino — fast, low-overhead JSON logging
  • Dev output: pino-pretty (colorized, single-line)
  • Request logging: pino-http (wired into the API)
  • Type Safety: Full TypeScript support

Why Pino

The logger follows the twelve-factor principle: the app writes a single JSON stream to stdout and lets the platform (Kubernetes, Docker) collect and route it. Pino is built for exactly this — structured JSON by default, very low overhead, and per-request child loggers. A single logger handles both request logs and application/error logs, instead of running separate systems.

Architecture

The logger package:

  • Emits structured JSON - Queryable fields (level, time, request, status) instead of flat text
  • Redacts sensitive data - Strips auth headers, cookies, passwords, and tokens from every log
  • Pretty-prints in dev - Human-readable, colorized, single-line output locally
  • Raw JSON in prod/test - No transport overhead; the platform collects stdout
  • Level controlled by env - LOG_LEVEL overrides the default per environment

Features

Structured JSON Logging

Every log is a JSON object with consistent fields, so backends like Loki, Datadog, or the ELK stack can filter and aggregate (e.g. status >= 500) instead of regex-matching text.

Automatic Redaction

Sensitive keys are stripped from every log object — including the req/res objects serialized by pino-http:

  • req.headers.authorization
  • req.headers.cookie
  • res.headers["set-cookie"]
  • *.password, *.token, *.secret

Environment-aware Output

  • Development - pino-pretty renders a colorized, single-line log; bulky request fields are hidden for readability.
  • Production / Test - Raw JSON to stdout with the full req/res objects intact for querying.

Per-request Child Loggers

When used with pino-http (see the API application), each request gets a child logger on req.log carrying a request id, so every line within a request is correlated.

Project Structure

packages/logger/
├── src/
│   └── index.ts                 # Configured Pino instance + exports
├── package.json                 # Dependencies and scripts
├── tsconfig.json                # TypeScript configuration
└── eslint.config.js             # ESLint configuration

Environment Variables

VariableDefaultPurpose
LOG_LEVELinfo (prod) / debug (dev)Minimum level to emit (tracefatal, silent)
NODE_ENVproduction/test → JSON; otherwise pretty output
# Quiet a noisy environment
LOG_LEVEL=warn

# Verbose debugging
LOG_LEVEL=debug

Usage

Basic Logging

import { logger } from "@workspace/logger";

logger.info("Server started");
logger.warn({ retries: 3 }, "Retrying request");
logger.error({ err }, "Failed to process job");

Pass an object as the first argument to attach structured fields; the message is the second argument.

Child Loggers

Attach persistent context to a scope so every line from it carries the same fields:

import { logger } from "@workspace/logger";

const jobLogger = logger.child({ module: "billing", jobId });

jobLogger.info("starting"); // includes module + jobId
jobLogger.error({ err }, "failed");

In Request Handlers (API)

The API wires pino-http, so handlers and middleware should log through req.log to inherit the request id:

app.get("/api/users/:id", async (req, res) => {
  req.log.info({ userId: req.params.id }, "fetching user");
  // ...
});

Log Levels

LevelWhen to use
fatalThe process is about to exit
errorA request or operation failed
warnUnexpected but recoverable (e.g. fail-open)
infoNormal lifecycle events (startup, requests)
debugDetailed diagnostics for development
traceVery fine-grained tracing

Example Output

Development (pino-pretty, single line):

[2026-06-06 13:47:25.410 +0530] INFO: API server running on http://localhost:4000
[2026-06-06 13:47:25.595 +0530] INFO: GET /api/users/me 200 (4ms) ip=::1 userId=usr_abc

Production (raw JSON to stdout — emitted as a single line, shown formatted here):

{
  "level": 30,
  "time": 1780729721693,
  "req": {
    "method": "GET",
    "url": "/api/users/me"
  },
  "res": {
    "statusCode": 200
  },
  "responseTime": 40,
  "ip": "204.0.113.10",
  "userId": "usr_abc",
  "msg": "GET /api/users/me 200 (40ms)"
}

Key Scripts

CommandPurpose
pnpm devWatch mode for development
pnpm buildBuild package for production
pnpm check-typesCheck TypeScript types
pnpm lintCheck code quality with ESLint

Pino is a Node.js logger and must not be imported into browser/client code (e.g. React Client Components). Use it in the API, server-side handlers, and other Node services only.

On this page