Stop Using console.log: Switch to Structured Logging

Stop Using console.log: Switch to Structured Logging

Debugging plain text logs in production is a nightmare. Learn why you should switch from console.log to JSON-based structured logging for better observability.

We have all been there: trying to debug a critical production issue at 2 AM, frantically scrolling through a wall of plain text generated by console.log(). You are looking for one specific error among thousands of generic messages, which is exhausting, inefficient, and entirely avoidable. If you are building backend services, it is time to stop using raw text logs and switch to structured logging.

The Nightmare of Plain Text Logs

When you start a new project, writing console.log("User successfully created: " + userId) feels perfectly sufficient. It works flawlessly on your local machine where you are the only user triggering requests. But in a production environment, this approach completely falls apart.

The Concurrency Trap

If User A and User B hit your API at the exact same millisecond, your text logs interleave. You might see a stream like this:

Checking auth...
Checking auth...
Database connection opened
Auth success
Database connection opened
Auth failed

Which user failed authentication? Which database transaction belongs to which request? You have absolutely no idea. Plain text lacks the inherent context needed to group asynchronous operations together.

The Searchability Problem

Try searching for all failed payment transactions above $100 in plain text logs. You would need to write a complex, fragile Regex that breaks the moment another developer slightly changes the log message format. Text logs are designed for humans to read sequentially, but at scale, machines need to read and index your logs first so they can help you filter them.

Enter Structured Logging

Searching through structured log data

Structured logging means writing your logs in a machine-readable format—almost always JSON. Instead of concatenating strings to form a human-readable sentence, you log a standardized event message alongside a structured object containing the exact context.

Plain Text: [INFO] 2026-07-15 Payment processed for user 12345 amount 50

Structured Log: {"level": "info", "time": "2026-07-15T15:00:25Z", "msg": "Payment processed", "userId": 12345, "amount": 50}

Why You Will Love It

Securing sensitive information in logs

Once you make the switch, you unlock capabilities that are impossible with console.log().

Instant Search and Filtering

Modern log aggregators (like Datadog, Elasticsearch, AWS CloudWatch, or Grafana Loki) automatically parse JSON logs. You can instantly run a query like level="error" AND userId="12345" and get exact results in milliseconds. You treat your logs like a queryable database of events rather than a text dump.

Richer Context

You can attach full JavaScript objects to your logs without worrying about formatting or stringification breaking. Need to log the incoming HTTP request headers or a complex response payload? Just pass the object directly to the logger.

Automated Alerting

It is incredibly simple to set up reliable automated alerts based on specific JSON fields. For example, you can trigger a Slack alert if paymentStatus="failed" occurs more than 10 times in 5 minutes, without relying on error-prone string matching.

Practical Implementation in Node.js

In the Node.js and TypeScript ecosystem, I highly recommend using Pino. It is blazing fast, avoids high memory overhead, and generates JSON logs by default. Here is how you can set it up to immediately improve your system:

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
});

// Usage in your service
logger.info({ 
  userId: 12345, 
  amount: 50, 
  gateway: 'stripe' 
}, 'Payment processed successfully');

Preserving Developer Experience (DX)

One common pushback from developers is: "JSON is unreadable in my local terminal!" This is entirely true. Nobody wants to read raw JSON blocks while developing a feature.

The solution is to use a transport formatter. In Pino, you install pino-pretty. In production, your app outputs raw JSON for the machines. In your local environment, pino-pretty parses that JSON and prints it as beautiful, colorized text. You get the best of both worlds.

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: process.env.NODE_ENV === 'development' ? {
    target: 'pino-pretty',
    options: { colorize: true }
  } : undefined
});

Watch Out for Sensitive Data (PII)

Because structured logging makes it so easy to pass entire objects (logger.info({ user })), you must be careful not to accidentally log passwords, credit card numbers, or Personally Identifiable Information (PII).

Log aggregators index everything, meaning a leaked token in your logs becomes a security incident. Thankfully, production-grade loggers have built-in redaction. You can configure them to automatically mask specific keys before the JSON is written:

const logger = pino({
  redact: ['password', 'token', 'creditCard', 'user.socialSecurityNumber'],
});

Leveling Up: Correlation IDs

To truly master observability, combine structured logging with Correlation IDs (often called Request IDs). When a request hits your server, generate a unique UUID. Attach this UUID to every single log generated during that request's lifecycle.

In Node.js, you can use AsyncLocalStorage to inject this ID silently across your entire application without passing it down through every function signature. When a user reports an error, they give you the Request ID, and you instantly pull up the exact trace of everything that happened for them.

Moving to structured logging takes maybe ten minutes of configuration, but it shifts your mindset from "dumping text" to "recording events." Do it today, and you will save countless hours of stress during your next production incident.

NT

written by

Nguyên Tech

0

Responses

Loading comments…