---
title: "16 — Logging — Winston, Morgan, and Why Structure Matters"
uid: logging
tags: ["observability", "winston", "production", "logging", "nodejs", "morgan", "roadmap:nodejs"]
excerpt: "console.log is fine for development; production needs levels, transports, and structure. Winston covers app logs, Morgan covers HTTP request logs."
date: 2026-08-13T03:27:54+0000
source: https://www.aveshina.my.id/en/blog/logging
---

"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](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](https://github.com/winstonjs/winston?tab=readme-ov-file#readme)

[3] "morgan," npm. [Online]. Available: [https://www.npmjs.com/package/morgan](https://www.npmjs.com/package/morgan)

```quiz
Q: Why is console.log insufficient for production logging?
- It is too slow to use in production
- It has no severity levels, no multiple destinations (transports), and produces free-text that is hard to search at scale
correct: 1
explain: console.log writes a string to stdout with no level, no routing, no structure. Production needs levels to filter, transports to persist to multiple destinations, and JSON fields so a log aggregator can search and alert.

Q: In the standard severity ordering, which is correct (highest to lowest)?
- error, warn, info, debug
- info, error, debug, warn
correct: 0
explain: From most to least severe: error, warn, info, debug, (silly/trace). Configuring a logger's level means "write this severity and above."

Q: What is a Winston "transport"?
- A severity level
- A destination for log entries — the console, a file, a remote HTTP service — each configurable at its own level
correct: 1
explain: A transport is where logs go. A logger can have many: errors to a remote monitoring service, everything to a combined file, info to the console in dev. Each transport can filter by level independently.

Q: Morgan's role versus Winston is…
- Morgan replaces Winston
- Morgan logs HTTP requests (method, path, status, timing) as Express middleware; Winston logs application events
correct: 1
explain: Morgan is deliberately narrow — it logs the request/response cycle. Winston handles application-level events. Using both together gives request-layer visibility (Morgan) plus event visibility (Winston).

Q: Why log structured fields (JSON objects) instead of free-text strings?
- JSON is smaller on disk
- Structured fields are searchable and filterable by a log aggregator — you can answer "show me all events for this requestId/userId/orderId"
correct: 1
explain: A log aggregator indexes named fields. "Show me every event with requestId=abc" is a query against structured data; against free-text it is a fragile grep. Structure is what makes logs useful at scale.
```
