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
npm install pino
Logging at Different Levels
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');
{"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
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.
logger.warn({ userId: 42, attempt: 3 }, 'Repeated failed login attempts');
{"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
- Create a logger and log one message at each of
info,warn, anderror, and confirmdebugis silently skipped by default. - Log an error with
logger.error({ err }, 'message'), and confirm the resulting line includes the error’stype,message, andstackas structured fields. - Log an event with at least two extra structured fields beyond
msg(for example, a simulated failed-login event withemailandattempt), and explain how a real log search tool would use those fields differently than it would use themsgtext. - 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/debugmap to numeric levels,debugis 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.