Day 16: Basic Logging with Winston. Success: Log API requests to console.
Welcome back, engineers! Today, we're diving into a topic that might seem mundane at first glance but is absolutely foundational to building resilient, observable, and maintainable systems: logging. Specifically, we'll implement basic API request logging using Winston, a powerful and highly flexible logging library for Node.js.
You might be thinking, "Just console.log() it, right?" In a small script, sure. But for a hyperscale CRM handling 100 million requests per second, console.log() is like trying to find a needle in a haystack while blindfolded and deaf. We need structure, levels, and destinations.
Agenda for Day 16
Core Concept: Understanding the critical role of structured logging in distributed systems.
Why Winston?: A brief overview of Winston's capabilities and why it's a good fit.
Component Architecture: Where our logger fits within our CRM API.
Hands-on Implementation: Setting up Winston and creating an Express middleware to log API requests.
Verification: Confirming our logs are captured correctly.
Core Concept: Beyond console.log() – The Power of Observability
In the world of ultra-high-scale systems, the ability to understand what's happening inside your black boxes is paramount. This is what we call observability. Logging is one of its three pillars (the others being metrics and tracing).
Imagine our CRM platform processing millions of customer interactions. A user reports an issue: "My lead isn't showing up in the sales pipeline." How do you debug this?
Was the request even received?
Did it hit the right endpoint?
Was the authentication successful?
Did an internal service fail?
What data was sent, and what was returned?
Without robust logging, you're flying blind.
Why Structured Logging is a Game Changer
Traditional console.log("User logged in: " + userId) generates unstructured text. While human-readable, it's terrible for machines. When you have terabytes of logs daily, you need to parse, filter, and analyze them programmatically.
Structured logging means outputting logs as machine-readable data, typically JSON.
Instead of:[2023-10-27T10:30:00.000Z] INFO User logged in: user123
You get:{"timestamp":"2023-10-27T10:30:00.000Z", "level":"info", "message":"User logged in", "userId":"user123", "operation":"login"}
This JSON object can be easily ingested by log aggregation tools (like Elasticsearch, Splunk, Datadog) and queried. You can quickly filter for all "login" operations for userId="user123" that occurred within a specific time window. This is indispensable for debugging, security auditing, and operational insights.
Logging Levels: Managing the Noise
Not all log messages are equally important. Winston, like most professional loggers, supports various logging levels:
error: Critical issues that prevent functionality.warn: Potentially problematic situations.info: General application flow, important events.http: HTTP request/response details (often used in web frameworks).verbose: Detailed information.debug: Debugging information, often too noisy for production.silly: Extremely detailed, for deep debugging.
In production, you might only log info, warn, and error to reduce volume and noise. In development, you'd use debug or verbose. This flexibility is crucial for performance and cost management at scale.
Architecture: Fitting Logging into our CRM API
Our CRM API is built on Express. The most effective way to log incoming API requests is via middleware. Middleware functions in Express sit between the request being received and the route handler being executed. This allows us to intercept every request, log its details, and then pass it along.
Our winston.js will configure the logger, and logger.js (or a specific requestLogger.js) will implement the middleware using that configured logger.
Hands-on Implementation: Setting Up Winston
Let's get our hands dirty. We'll configure Winston to log to the console initially. This is perfect for development. Later, we'll explore file transports, daily rotation, and remote log aggregation.
1. Install Winston
First, ensure you're in your project directory (crm-api from previous lessons).
2. Configure the Winston Logger (src/config/winston.js)
We'll create a central logger instance. This ensures all parts of our application use the same logging configuration.
Insight: Notice process.env.NODE_ENV. This is a critical pattern. We log debug in development (more verbose) and info in production (less verbose). This prevents our production logs from becoming an unmanageable torrent of data. At 100M RPS, every byte logged matters for performance and cost.
3. Create Request Logging Middleware (src/middleware/logger.js)
Now, let's create an Express middleware that uses our logger instance to log incoming HTTP requests.
Insight: We're logging req.method, req.originalUrl, and req.ip. For a real production system, you'd often add a correlation ID (also known as a request ID) to each log entry. This ID would be generated at the edge of your system and passed down through all services involved in processing that request. This allows you to trace a single request's journey across multiple microservices, which is invaluable for debugging complex distributed systems. We'll explore correlation IDs in a future lesson.
4. Integrate Middleware into src/server.js
Finally, we need to tell our Express app to use this middleware. We'll place it early in the middleware chain so it logs all incoming requests before they hit specific route handlers.
Verification: See it in Action!
Now, when you start your server and make requests, you should see structured log messages in your console.
Start your server:
node src/server.jsOpen another terminal and make a
curlrequest:
Observe your server console. You should see output similar to this:
(Your timestamp, IP, and exact message might vary slightly based on your system and Winston configuration.)
Success Criteria: You can see [HTTP] POST /api/auth/signup logged to your console when you hit the signup endpoint.
Real-world Production Application at 100 Million RPS
At 100 million requests per second, logging becomes an engineering challenge in itself.
Volume: You're generating an astronomical amount of log data. You cannot log everything. Sampling or extremely selective logging becomes crucial.
Performance: Synchronous logging is a no-go. Every I/O operation blocks your request thread. Asynchronous logging and buffering are essential.
Storage: You need a highly scalable, cost-effective log aggregation system (like an ELK stack - Elasticsearch, Logstash, Kibana, or cloud-native solutions like AWS CloudWatch Logs, Google Cloud Logging, Azure Monitor).
Cost: Storing and processing massive log volumes is expensive. This drives decisions on what to log, how long to retain it, and what level of detail is truly necessary.
Security & Compliance: Logging sensitive data is a major breach risk. Strict rules, data masking, and redaction must be in place. Audit trails for critical actions (e.g., data access, configuration changes) are mandatory for compliance.
Today, we laid the groundwork for robust logging. This seemingly small step is a giant leap towards building a truly observable and resilient CRM system that can scale to any demand.
Assignment: Enhance Logging with HTTP Status Codes
Your task is to modify the requestLogger middleware to also log the HTTP status code of the response after the request has been processed. This will give you a complete picture of each API interaction.
Steps:
In
src/middleware/logger.js, modify therequestLoggermiddleware.Use
res.on('finish', ...)to capture the response status code.Log the status code along with the request details.
Example Output (Expected):
Solution Hints
The
resobject in Express middleware emits afinishevent when the response has been sent.You can access the status code via
res.statusCodewithin thefinishevent listener.Remember to use your
loggerinstance for all log messages.