AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 16 — Logging — Winston, Morgan, and Why Structure Matters

16 — Logging — Winston, Morgan, and Why Structure Matters

August 13, 20265 min read
Download as Markdown

"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 ms

That 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!