Back to Blog
·4 min read

Why I stopped using console.log and started using structured logging

Structured logging with proper levels and context makes debugging production issues infinitely easier than scattered console.log statements.

AI Dev
logging
debugging
observability
production

Why I stopped using console.log and started using structured logging

I used to debug everything with console.log. Quick, dirty, and it worked -- until I had to track down a production bug that was happening once every few hours across thousands of requests. Scrolling through walls of unstructured log output, trying to piece together which logs belonged to which user session, was like looking for a needle in a haystack made of needles.

The breaking point came during a payment processing bug where transactions were failing silently. I had console.log statements scattered throughout the codebase: "payment started", "validating card", "calling stripe", "payment complete". But when I needed to trace a specific failed transaction, I could not connect the dots. Which validation belonged to which payment? What was the user ID? What was the request ID that tied everything together?

That's when I learned the difference between logging and structured logging. It is not just about what you log -- it's about how you log it.

The Console.log Problem

Here's what my old logging looked like:

async function processPayment(userId: string, amount: number) {
  console.log('Starting payment processing');
  
  try {
    console.log('Validating user:', userId);
    const user = await getUser(userId);
    
    console.log('Creating charge for amount:', amount);
    const charge = await stripe.charges.create({
      amount: amount * 100,
      currency: 'usd',
      customer: user.stripeCustomerId,
    });
    
    console.log('Payment successful:', charge.id);
    return { success: true, chargeId: charge.id };
  } catch (error) {
    console.log('Payment failed:', error.message);
    throw error;
  }
}

In development, this felt fine. But in production with hundreds of concurrent payments, these logs became useless noise. I could not filter, search, or correlate them effectively.

Structured Logging Changes Everything

Now I use structured logging with consistent context:

import { logger } from './logger';
 
async function processPayment(userId: string, amount: number) {
  const requestId = generateRequestId();
  const context = { userId, amount, requestId };
  
  logger.info('Payment processing started', context);
  
  try {
    logger.debug('Validating user', { ...context, step: 'validation' });
    const user = await getUser(userId);
    
    logger.info('Creating stripe charge', { 
      ...context, 
      step: 'stripe_charge',
      customerId: user.stripeCustomerId 
    });
    
    const charge = await stripe.charges.create({
      amount: amount * 100,
      currency: 'usd',
      customer: user.stripeCustomerId,
    });
    
    logger.info('Payment completed successfully', {
      ...context,
      chargeId: charge.id,
      status: 'success'
    });
    
    return { success: true, chargeId: charge.id };
  } catch (error) {
    logger.error('Payment processing failed', {
      ...context,
      error: error.message,
      stack: error.stack,
      status: 'failed'
    });
    throw error;
  }
}

The difference is night and day. Every log entry has the same structural context, making it trivial to filter logs by user, request, or transaction type.

My Structured Logging Setup

I use a simple wrapper around a proper logging library:

import winston from 'winston';
 
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'app.log' })
  ]
});
 
export { logger };

The key principles I follow:

  • Consistent context: Every related log shares the same identifiers (requestId, userId, etc.)
  • Proper log levels: info for business events, debug for technical details, error for actual problems
  • Structured data: Objects instead of string concatenation
  • Meaningful messages: Describe what happened, not just variable values

Real Production Benefits

Since switching to structured logging, I can answer questions like:

  • "Show me all logs for user 12345 from the last hour"
  • "What were the last 10 failed payment attempts?"
  • "Which API endpoints are taking longer than 2 seconds?"

With console.log, these queries were impossible. With structured logs and a tool like Elasticsearch or even just jq, they're one command away:

# Find all failed payments for a specific user
cat app.log | jq 'select(.userId == "12345" and .status == "failed")'
 
# Get all slow requests
cat app.log | jq 'select(.duration > 2000)'

The Performance Question

"But is not structured logging slower?" Not meaningfully. The bottleneck in most applications is I/O, not logging. And the debugging time I save when things go wrong more than makes up for any microscopic performance cost.

The real cost of console.log is not performance -- it's the hours you'll spend trying to debug production issues with inadequate information.

Start with structured logging from day one. Your future self will thank you when you're tracking down that critical bug at 2 AM and you can actually follow the trail of what went wrong.