Engineering

Structured Logging in Node.js: From console.log to Production Observability

Learn about structured logging in Node.js for better debugging, monitoring, and production observability. Move beyond console.log for robust applications. #nodejs

· Founder & Engineer · · 7 min read
Code snippet showing structured logging implementation in Node.js with monitoring dashboard in the background.

Logging is a fundamental aspect of building robust and maintainable applications. Moving beyond simple console.log statements is crucial for effective debugging, monitoring, and troubleshooting, especially in production environments.

Structured Logging: Why and How?

As the MisuJob platform evolved to process 1M+ job listings and power our AI-powered job matching across Europe, we quickly realized that our initial reliance on basic console logging wasn’t scalable. Debugging issues across our distributed services became a nightmare, and gleaning meaningful insights from raw log data was nearly impossible. Structured logging became essential.

Structured logging involves formatting log messages into a consistent, machine-readable format, typically JSON. This allows for easy parsing, filtering, and analysis of logs by tools like Elasticsearch, Kibana, Grafana Loki, and Datadog. This is a significant improvement over grepping through unstructured text logs.

Benefits of Structured Logging

  • Improved Debugging: Quickly identify and correlate events across different parts of your application.
  • Enhanced Observability: Gain real-time insights into your application’s performance and health.
  • Simplified Analysis: Aggregate and analyze log data to identify trends and patterns.
  • Faster Root Cause Analysis: Pinpoint the root cause of issues more efficiently.
  • Compliance and Auditing: Meet regulatory requirements by maintaining a detailed audit trail.

Moving Beyond console.log

The humble console.log is a great starting point, but it lacks the structure and context needed for production-grade logging. Consider this:

console.log("User logged in", { userId: 123, username: "john.doe" });

While this provides some information, it’s still just a string. To truly leverage this data, we need to structure it properly.

Implementing Structured Logging in Node.js

Several excellent logging libraries are available for Node.js. We’ve found that pino and winston offer a good balance of performance, features, and ease of use. In this example, we’ll use pino due to its speed and minimal overhead, which is crucial for high-throughput applications like ours.

Example with Pino

First, install pino:

npm install pino

Then, you can use it like this:

const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  redact: ['password', 'credit_card'], // Redact sensitive data
});

logger.info({ userId: 123, username: "john.doe" }, "User logged in");
logger.warn({ transactionId: "abc-123" }, "Transaction failed");
logger.error({ err: new Error("Something went wrong") }, "An error occurred");

This code snippet demonstrates:

  • Creating a pino logger instance.
  • Setting the log level based on an environment variable (LOG_LEVEL). This allows you to control the verbosity of your logs in different environments (e.g., debug in development, info in production).
  • Using the redact option to prevent sensitive data (passwords, credit card numbers) from being logged. This is crucial for security and compliance.
  • Logging messages with associated data as key-value pairs. This is what makes the logs structured.

Customizing Log Output

pino allows for extensive customization. For example, you can configure the log format, add custom fields, and integrate with external logging services. One common pattern is to use pino-pretty for human-readable output during development:

npm install pino-pretty

Then, you can pipe your logs through pino-pretty:

node your-app.js | pino-pretty

This will format your JSON logs into a more readable format in your console. For production, you’d typically send your logs to a centralized logging system.

Correlation IDs

In a microservices architecture, tracking requests across multiple services can be challenging. Correlation IDs help solve this problem. A correlation ID is a unique identifier that is generated at the entry point of a request and propagated to all downstream services. This allows you to trace the entire request flow through your system.

Here’s an example of how to add a correlation ID to your logs using middleware:

const { v4: uuidv4 } = require('uuid');

function correlationIdMiddleware(req, res, next) {
  const correlationId = req.headers['x-correlation-id'] || uuidv4();
  req.correlationId = correlationId;
  res.setHeader('x-correlation-id', correlationId);
  next();
}

// In your Express app:
// app.use(correlationIdMiddleware);

// Then, in your logger:
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  mixin: () => ({ correlationId: req.correlationId }), // Accessing req requires context
});

(Note: the req.correlationId access requires the logger to be created within the request context or using pino.context for more complex setups).

Now, every log message will include the correlation ID, making it easier to trace requests across services.

Choosing the Right Log Level

Using the correct log level is crucial for effective logging. The most common log levels are:

  • debug: Detailed information for debugging purposes. Generally not enabled in production.
  • info: General information about the application’s state. Useful for monitoring.
  • warn: Potential issues that don’t necessarily cause errors but should be investigated.
  • error: Errors that prevent the application from functioning correctly.
  • fatal: Critical errors that may lead to application termination.

Setting the log level appropriately allows you to filter out noise and focus on the most important events. For example, in production, you might set the log level to info to capture general information and errors, while in development, you might set it to debug to see more detailed information.

Integrating with Centralized Logging Systems

In a production environment, you’ll typically want to send your logs to a centralized logging system like Elasticsearch, Grafana Loki, or Datadog. These systems provide powerful tools for searching, analyzing, and visualizing log data.

pino and other logging libraries offer integrations with these systems. For example, you can use pino-elasticsearch to stream your logs directly to Elasticsearch. Alternatively, you can use a log shipper like Fluentd or Logstash to collect logs from your application and forward them to your logging system.

Real-World Examples at MisuJob

At MisuJob, we use structured logging extensively throughout our platform. Here are a few examples:

  • Job Search: We log every search query, including the keywords, location, and filters used. This allows us to analyze search trends and identify areas for improvement in our AI-powered job matching algorithms. We can see, for example, that searches for “Data Scientist” with “Remote” in the UK have increased by 30% in the last quarter, allowing us to tailor our content and recommendations accordingly.
  • User Authentication: We log every login attempt, including the username, IP address, and authentication method used. This helps us detect and prevent fraudulent activity.
  • API Requests: We log every API request, including the endpoint, request parameters, and response status code. This allows us to monitor the performance of our APIs and identify potential bottlenecks. We noticed a spike in 500 errors on our /jobs endpoint last week, which led us to identify and fix a database connection issue.

Salary Insights: A Case Study with Structured Logging

By analyzing aggregated and anonymized data from our structured logs, we can gain valuable insights into salary trends across Europe. For example, consider the following salary ranges for Senior Software Engineers in different countries:

Country/RegionAverage Salary (EUR)Salary Range (EUR)
Germany85,00070,000 - 100,000
United Kingdom78,00065,000 - 90,000
Netherlands80,00068,000 - 95,000
Switzerland110,00095,000 - 130,000
France70,00060,000 - 80,000
Spain55,00045,000 - 65,000

This data, derived from our platform which processes 1M+ job listings, allows job seekers to understand salary expectations in different markets and negotiate effectively.

Performance Considerations

Logging can impact application performance, especially in high-throughput systems. It’s important to choose a logging library that is optimized for performance and to avoid logging excessive amounts of data. pino is a good choice because it is designed to be fast and efficient. Additionally, consider asynchronous logging to offload logging operations to a separate thread, minimizing the impact on your application’s main thread.

Security Best Practices

When logging data, it’s important to be mindful of security and privacy. Avoid logging sensitive data like passwords, credit card numbers, and personal information. If you need to log sensitive data, redact it or mask it before logging. Make sure your logging system is properly secured to prevent unauthorized access to your log data. We use the redact option in pino to ensure sensitive data is never written to disk.

Conclusion

Structured logging is a critical component of building robust, observable, and maintainable applications. By moving beyond simple console.log statements and adopting structured logging practices, you can significantly improve your ability to debug issues, monitor performance, and gain valuable insights into your application’s behavior. At MisuJob, we’ve seen firsthand the benefits of structured logging in enabling us to build a high-performance, scalable platform that processes 1M+ job listings and delivers AI-powered job matching across Europe.

Key Takeaways

  • Embrace Structured Logging: Move beyond console.log for production applications.
  • Choose the Right Library: pino is a performant and flexible option for Node.js.
  • Use Correlation IDs: Track requests across multiple services.
  • Set the Correct Log Level: Control the verbosity of your logs in different environments.
  • Integrate with Centralized Logging: Use tools like Elasticsearch, Grafana Loki, or Datadog for analysis.
  • Prioritize Performance: Choose a logging library that is optimized for speed.
  • Maintain Security: Protect sensitive data by redacting or masking it before logging.
  • Analyze Your Logs: Use aggregated log data to gain valuable insights into your application’s behavior and the job market.
nodejs logging observability debugging production
Share
P
Pablo Inigo

Founder & Engineer

Building MisuJob - an AI-powered job matching platform processing 1M+ job listings daily.

Engineering updates

Technical deep dives delivered to your inbox.

Find your next role with AI

Upload your CV. Get matched to 50,000+ jobs. Apply to the best fits effortlessly.

Get Started Free

User

Dashboard Profile Subscription