16 — Logging — Winston, Morgan, and Why Structure Matters
"Just console.log and move on" was my logging strategy, and it stopped being fine the first time I searched a production log. The model that finally stuck splits cleanly: console.log is fine for development, but production logging needs three things console does not give me — severity levels, multiple destinations (transports), and machine-readable structure. Winston and Morgan are the two libraries that fill those gaps, one for application logs and one for HTTP request logs [1].
The framing that landed for me is the dev-versus-production split, then the two libraries that map to it.
What console.log is and is not
console.log writes a formatted string to stdout; console.error writes to stderr. For development — "is this variable what I think it is?" — that is exactly the right tool: zero setup, immediate output, throwaway by intent. The problem is that production logging is a different job. In production I need to:
- Filter by severity — show me the errors at 3am, not the debug noise.
- Persist to multiple destinations — local files for the on-call, a remote log aggregator for search and alerting.
- Be machine-parseable — a JSON object with fields is searchable; a free-text string is not.
- Survive rotation and retention — log files grow, and they need to be rotated and aged out.
console.log does none of these. That is not a flaw — it was never meant to — but it is why production code reaches for a logging library.
Log levels: the severity axis
The first idea a logging library gives me is levels [1]. The standard set, in order of decreasing severity:
- error — something failed; a human should look.
- warn — something unexpected, but the app recovered.
- info — normal, significant events (a user logged in, a job completed).
- debug — diagnostic detail, useful during development.
- silly — extremely verbose trace output.
The value of levels is filtering. I configure the logger to write warn and above to a file always, info and above to the console in dev, and debug only when a flag is set. One log call, routed differently by environment — the kind of thing console.log cannot do because it has no level.
Winston: the application logger
Winston is the standard logging library for Node.js application logs, and its two defining ideas are transports and levels [2]. A transport is a destination — the console, a file, a remote HTTP service. Each logger can have multiple transports configured at different levels: errors to a remote monitoring service, everything to a local file, info to the console during development.
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json() // structured, machine-parseable
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({ format: winston.format.simple() }));
}
logger.info('user logged in', { userId: 42 });
logger.error('payment failed', { orderId: 99, reason: 'card declined' });The second argument — the metadata object — is the part that makes logs useful in production. Structured fields mean a log aggregator can answer "show me all payment failures for order 99" instead of grepping free text. The format.json() is what makes the line machine-readable; the timestamp is what makes events orderable.
Morgan: the HTTP request logger
Morgan is a focused, complementary tool — it is Express middleware that logs every HTTP request and its outcome [3]. Where Winston logs application events, Morgan logs the request/response cycle: method, path, status code, response time.
import morgan from 'morgan';
app.use(morgan('combined')); // Apache-style combined log format:method :url :status :response-time ms
GET /users/42 200 12 ms
POST /login 401 45 msThat output is what tells me, in production, which endpoints are slow, which are erroring, and what the traffic shape looks like. Morgan is deliberately narrow — it does one job (HTTP request logging) and leaves application logging to Winston. Using both together is the common, clean split: Morgan for the request layer, Winston for everything else.
Structured logging: the production discipline
The single habit that separates useful production logs from noise is structure [1]. A log entry should be a JSON object with named fields — timestamp, level, message, plus whatever domain context applies (userId, requestId, orderId). Free-text strings are for humans reading a terminal; structured fields are for machines searching, filtering, and alerting on logs at scale.
The discipline I keep:
- Every log call carries a metadata object, not just a message string.
- A requestId (correlating all logs from one request) makes tracing a single user's path through the system possible.
- Errors log the full stack, not just the message — the stack is the diagnostic.
- Logs never carry secrets (passwords, tokens, PII — personal details like names and emails) — log aggregators are a common leak vector.
How I use this
The model I keep is the dev-versus-production split, and it drives a few habits. In development I use console.log freely — it is the right tool for quick inspection, and removing it (or leaving it) costs nothing. In production, every application log goes through Winston, configured with at least a file transport for errors and a JSON format for searchability, and every HTTP request goes through Morgan. I treat log levels as a real filter, not decoration — error means a human should look, info is the default for significant events, debug is off by default and on only when diagnosing. And every log entry carries structured fields, because the question I will eventually need to answer is "show me every event related to this request/user/order," and free-text logs cannot answer it. The split — console for dev, Winston + Morgan for production — is what keeps logging deliberate.
References
[1] AppSignal, "Best practices for logging in Node.js," AppSignal Blog, 2021. [Online]. Available: https://blog.appsignal.com/2021/09/01/best-practices-for-logging-in-nodejs.html
[2] "winston," GitHub. [Online]. Available: https://github.com/winstonjs/winston?tab=readme-ov-file#readme
[3] "morgan," npm. [Online]. Available: https://www.npmjs.com/package/morgan
Knowledge check · Question 1 of 5
Why is console.log insufficient for production logging?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!