CodingNic

Logging & Error Monitoring

Structured Logging with Pino

Logging & Error Monitoring 15 min read

Structured Logging with Pino

Objectives

By the end of this lesson, you should be able to:

  • Create a Pino logger and log at different levels
  • Read Pino’s structured JSON output
  • Attach extra structured fields to a log line, including an error object

💡 Why this matters: The last lesson named the gaps in console.log, this lesson closes them, with a real, widely-used logging library, producing real structured output.

⚠️ A note on verification: every snippet and every output in this lesson was actually run.

Installing Pino

bash
npm install pino

Logging at Different Levels

javascript
const pino = require('pino');
const logger = pino();

logger.info('Server started');
logger.warn('Disk space running low');
logger.error({ err: new Error('Database connection failed') }, 'Failed to connect to database');
logger.debug('This will not show at the default log level');
text
{"level":30,"time":1785866583110,"pid":5,"hostname":"claude","msg":"Server started"}
{"level":40,"time":1785866583110,"pid":5,"hostname":"claude","msg":"Disk space running low"}
{"level":50,"time":1785866583110,"pid":5,"hostname":"claude","err":{"type":"Error","message":"Database connection failed","stack":"Error: Database connection failed\n    at ..."},"msg":"Failed to connect to database"}

Every line is JSON, with a consistent shape: level, time, pid, hostname, msg, exactly the “structured, not free-text” property the last lesson was missing. Notice logger.debug(...) produced no output at all, Pino’s default level is info, debug is below that threshold and is silently skipped, intentional, debug-level detail is useful while developing, noisy in production, adjustable via configuration without changing a single log call in the code.

Reading Levels as Numbers

Pino’s numeric level values, 30 for info, 40 for warn, 50 for error (and 20 for debug, 60 for fatal), correspond directly to level names, a log aggregation tool can filter level >= 50 to see only errors and above, across every log line an application ever produces, structured data makes that kind of filtering possible, a free-text console.error(...) line never could.

Attaching Structured Fields

javascript
logger.error({ err: new Error('Database connection failed') }, 'Failed to connect to database');

The first argument to a Pino log call, an object, becomes additional structured fields on that log line, err here, the second argument is the human-readable msg. Any relevant data goes in that first object, userId, status, requestId, whatever matters for that specific event, all of it becomes a real, filterable field, not buried inside a sentence.

javascript
logger.warn({ userId: 42, attempt: 3 }, 'Repeated failed login attempts');
text
{"level":40,"time":1785866600000,"pid":5,"hostname":"claude","userId":42,"attempt":3,"msg":"Repeated failed login attempts"}

userId and attempt are both real fields on this line now, a log search for userId:42 finds this line directly, no text-matching required.

Try It

  1. Create a logger and log one message at each of info, warn, and error, and confirm debug is silently skipped by default.
  2. Log an error with logger.error({ err }, 'message'), and confirm the resulting line includes the error’s type, message, and stack as structured fields.
  3. Log an event with at least two extra structured fields beyond msg (for example, a simulated failed-login event with email and attempt), and explain how a real log search tool would use those fields differently than it would use the msg text.
  4. Explain, in one or two sentences, why a numeric level (30, 40, 50) is more useful for filtering than a free-text label would be.

Recap

  • Pino produces structured, consistent JSON log lines, level, time, msg, and whatever extra fields are attached.
  • logger.info/warn/error/debug map to numeric levels, debug is silently skipped at the default configuration, adjustable without changing individual log calls.
  • Extra data belongs as structured fields, the first argument to a log call, not folded into the message text, that’s what makes it searchable later.

Next lesson: wiring Pino into Express as request-logging middleware, and distinguishing operational errors from programmer errors in the error handler.