Structured logging with pino

Production has been live for a week. At 2 AM, orders start failing. You SSH in and find ten thousand lines of console.log output. Which are errors? When did it start? Is the database slow or is Redis on fire? You cannot tell, because the logs are unstructured text. Welcome to the moment every backend engineer converts to structured logging.

The three pillars of observability

Logs tell you what happened. Metrics tell you how much and how fast. Traces tell you how one request flowed through the system. All three are built from the same instrumentation in your handlers.

console.log gives you a string. Structured logging gives you a JSON object with fields you can search, filter, and aggregate. 'User logged in' becomes { level: 'info', userId: 42, action: 'login', timestamp: ... }. Now your log tool can answer questions like 'how many logins in the last hour' without grep.

after/logger.ts
typescript
import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
});

// Usage
logger.info({ userId: 42, action: 'login' }, 'user logged in');
logger.error({ err, requestId: 'abc-123' }, 'order creation failed');

Pino is the fastest Node logger. One import, one instance, JSON by default.

Pino outputs one JSON object per log line and is written to be fast enough that you do not pay a performance tax for logging. That matters in hot loops. Winston is fine too. Bunyan is fine. The important thing is that you move from plain text to JSON and never look back.

Quiz: Quiz

Loading practice…