Logging with morgan & pino
Logging is how a running server tells you what it's doing: morgan automatically records every HTTP request, while pino writes fast, structured JSON logs with severity levels that your tools can search, filter, and alert on.
Learn Logging with morgan & pino in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll see why console.log isn't enough in production, write structured logs with pino, control verbosity with levels, tag requests with child loggers, and add morgan as drop-in HTTP request logging — the logging stack real Node services rely on.
What You'll Learn in This Lesson
1️⃣ console.log vs a Real Logger
console.log prints free text with no level, no structure, and no off switch. A real logger like pino writes structured JSON : each line is an object with a level , a time , your message, and any extra fields you attach. That JSON is what log tools ingest so you can search, filter, and alert on it.
Notice the shape below: logger.info(obj, msg) merges your data object into the line. And a child logger created with logger.child( { requestId } ) stamps that field onto everything it logs.
2️⃣ Levels: Controlling the Noise
Every log has a level ranking its importance, from trace (10) up to fatal (60). You set a minimum level, and the logger silently drops anything below it. That single setting is how you run chatty debug logs locally but only info and above in production.
3️⃣ morgan: Logging Every Request
You don't want to hand-write a log line in every route. morgan is middleware that does it for you: add app.use(morgan('tiny')) once and every incoming request is logged automatically with its method, URL, status, response size, and timing.
(The exact response time varies per run — yours will show a different millisecond value.)
In production you usually want one consistent format. The classic pattern pipes morgan's HTTP lines into pino, so your access logs and your application logs are all structured JSON — easy to ship to one place:
Your turn. Fill in the two ___ blanks so the child logger writes the expected JSON line, then run it.
Mini-Challenge: A Login Flow's Logs
No blanks this time — just a brief and an outline. Write it yourself, run it, and check your output. This ties together child loggers, data fields, and level filtering in one short program.
Common Errors (and the fix)
- My debug logs don't show — Your logger's level is higher than debug (default is info ). Set { level: 'debug' } or the LOG_LEVEL env var.
- The data object and message are swapped — pino's signature is logger.info(obj, msg) : the object comes first, the string second. Reversed, your fields won't appear as expected.
- morgan logs nothing — app.use(morgan(...)) must come before your routes, and it only logs requests that actually reach Express.
- Logging a whole error as a string — Pass the error as a field ( logger.error( { err } , 'failed') ) so the stack is captured, instead of String(err) which loses the trace.
- Pretty output crashes in production — pino-pretty is for local dev only. In production, emit raw JSON and let your platform format it.
Pro Tips
- 💡 One child logger per request: tag it with a requestId so you can trace a whole request through your logs by one id.
- 💡 Log objects, not concatenated strings: logger.info({ userId }, 'login') stays searchable; string interpolation does not.
- 💡 Set the level from an env var: pino({ level: process.env.LOG_LEVEL || 'info' }) lets you crank up detail without redeploying.
📋 Quick Reference — Logging
Task
Syntax
Create a logger
const logger = pino()
Info with data
logger.info({ userId }, 'msg')
Set min level
pino({ level: 'debug' })
Child logger
logger.child({ requestId })
HTTP logging
app.use(morgan('tiny'))
morgan → pino
morgan(fmt, { stream })
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ console.log is for quick debugging; production needs a real logger
- ✅ pino writes fast, structured JSON with a level, time, and your fields
- ✅ Levels (trace→fatal) plus a minimum level control how much you log
- ✅ logger.child( { requestId } ) tags every line with shared context
- ✅ morgan logs every HTTP request automatically as middleware
- ✅ Pipe morgan into pino to get one consistent JSON format everywhere
- ✅ Next lesson: MongoDB with Mongoose — model and query your data
Practice quiz
In what format does pino write its log lines by default?
- Structured JSON
- Plain English sentences
- XML
- CSV
Answer: Structured JSON. pino emits structured JSON, one object per line.
What does logger.child({ requestId: 'abc-123' }) create?
- A new process
- A logger that tags every line with that field
- A separate file
- A network connection
Answer: A logger that tags every line with that field. A child logger carries fixed fields (like requestId) onto every line it writes.
With a logger configured at level 'warn', which call is NOT printed?
- logger.error('x')
- logger.fatal('x')
- logger.info('x')
- logger.warn('x')
Answer: logger.info('x'). info (30) is below warn (40), so it is filtered out.
What is the numeric level pino uses for 'info'?
- 10
- 50
- 60
- 30
Answer: 30. pino levels: trace=10, debug=20, info=30, warn=40, error=50, fatal=60.
What is morgan used for?
- Logging an HTTP request line automatically per request
- Connecting to a database
- Parsing JSON bodies
- Hashing passwords
Answer: Logging an HTTP request line automatically per request. morgan is HTTP-request logging middleware for Express.
How is morgan added to an Express app?
- app.get(morgan)
- app.use(morgan('tiny'))
- morgan.listen(app)
- app.morgan()
Answer: app.use(morgan('tiny')). You register it as middleware: app.use(morgan('tiny')).
Which pino level is the most severe?
- info
- warn
- fatal
- error
Answer: fatal. fatal (60) is the highest, most severe level.
Why prefer a real logger like pino over console.log in production?
- console.log is unavailable in Node
- It is slower
- It changes the language
- It produces structured, level-aware, machine-parsable logs
Answer: It produces structured, level-aware, machine-parsable logs. Structured JSON with levels is far easier to filter and search than plain console output.
What is the 'tiny' value passed to morgan?
- A compact predefined log format preset
- A file path
- A port number
- A log level
Answer: A compact predefined log format preset. 'tiny' is one of morgan's built-in format presets (method url status size - response-time).
Which rule decides whether a pino message is printed?
- Only error and above ever print
- Only messages at or above the configured level print
- Every message always prints
- Only the first message prints
Answer: Only messages at or above the configured level print. pino prints messages whose level is at or above the configured level.
Continue this course
- Previous: File Uploads with Multer
- Next: MongoDB with Mongoose