Logging & Observability
📡 Logging & Observability
console.log Doesn't Scale
console.log is fine for a script on your machine. In production, across dozens of instances, it falls apart — no structure to search on, no severity to filter by, no way to correlate lines from the same request:
Unstructured vs Structured Logging
console.log
Structured Log
A Typed Logger Interface
Just like Database and Mailer back in Chapter 1, a Logger should be an interface your classes depend on — not a hardcoded call to a specific library:
Interface, Not Library
Business logic depends on Logger, not pino or winston directly — swap the implementation without touching a single call site.
Event Names, Not Sentences
"order.placed" is grep-able and stable across languages/refactors; a full sentence isn't.
Redacting Secrets Automatically
Chapter 4 split PublicConfig from Secrets at the type level. The same discipline applies to logging — a typed logger can refuse to accept fields it doesn't trust:
A Redacting Console Logger
📊 Metrics Basics
Logs answer "what happened in this one request." Metrics answer "how is the system behaving overall" — cheap to store, fast to query, ideal for dashboards and alerts:
Counter
Only goes up: requests_total, orders_placed_total. Good for rates ("orders per minute").
Gauge
Goes up or down: active_connections, queue_depth. A snapshot of "right now."
Histogram
Distribution of values: request_duration_ms. Lets you ask "what's the p95 latency?"
Typed Metric Names
A literal-union type for metric names catches typos the same way LogLevel caught invalid levels.
🧵 Tracing Basics
A single user action can fan out across several services. Tracing ties every log line and span from that one action together with a shared identifier:
Correlation ID
One ID per request, passed through every log line and downstream call — the simplest form of tracing.
Spans
Full tracing tools (OpenTelemetry) go further: each unit of work is a timed "span," nested to show where time was actually spent.
💻 Coding Challenges
Challenge 1: Build a Typed Logger Interface
Define a Logger interface with debug/info/warn/error methods, then implement a ConsoleLogger that prints each call as a single JSON line including a level field.
Goal: Practice designing an interface that could later be swapped for a real logging library without touching any calling code.
Challenge 2: Redact Sensitive Fields
Write a redact(fields: LogFields): LogFields function that replaces any field whose key matches password, token, or secret (case-insensitive) with "[REDACTED]", then wire it into your logger from Challenge 1.
Goal: Practice enforcing a safety rule in code rather than relying on developers to remember it.
Challenge 3: Add Request Correlation
Write a logWithContext helper that takes a Logger, a RequestContext (with a requestId), an event name, and extra fields — and merges the requestId into every logged event automatically.
Goal: Practice the correlation-ID pattern that makes multi-line, multi-service logs traceable back to one request.
Structured logging makes it easy to log everything — which sounds safe until a log aggregator bill spikes, or a debug log at request-per-millisecond volume slows down the very service it's meant to help diagnose. Log events that matter for debugging or auditing, use debug level generously in development but sparingly in production, and always run new log fields through the redaction rule from Challenge 2 before they ship.
🎯 What's Next
With visibility into a running app established, the next chapter goes back to where that data usually lives: Database Integration — ORM patterns with TypeORM and Prisma, query builders, migrations, and writing genuinely type-safe queries.