Skip to main content
Logging That Actually Helps You Debug

Logging That Actually Helps You Debug

Jul 5, 2025

Youre on-call. Something breaks at 3am. You open the logs and see thousands of lines of "Starting server..." and "Request received". Nothing useful.

Good logging is the difference between finding the bug in 5 minutes and spending 2 hours guessing.

The Problem With Most Logs

Structured Logging

Stop concatenating strings. Use structured data:

// Bad - hard to parse and search
console.log(`User ${userId} placed order ${orderId} for $${total}`);

// Good - structured and searchable
logger.info('order_placed', {
  userId,
  orderId,
  total,
  itemCount: items.length,
  paymentMethod: 'card'
});

Output as JSON:

{
  "level": "info",
  "message": "order_placed",
  "userId": "user_123",
  "orderId": "order_456",
  "total": 99.50,
  "itemCount": 3,
  "timestamp": "2024-01-15T10:30:00Z"
}

Now you can query: "show me all orders over $100 from user_123".

Log Levels That Make Sense

// ERROR: Something failed that shouldn't
logger.error('payment_failed', { orderId, error: err.message, stack: err.stack });

// WARN: Something suspicious but handled
logger.warn('rate_limit_approached', { userId, current: 95, limit: 100 });

// INFO: Business event happened
logger.info('user_registered', { userId, source: 'google_oauth' });

// DEBUG: Details for development
logger.debug('cache_miss', { key, ttl: 300 });

Context Is Everything

Include enough context to debug without looking elsewhere:

// Bad - what user? what order? what card?
logger.error('Payment failed');

// Good - full context
logger.error('payment_failed', {
  orderId: order.id,
  userId: order.userId,
  amount: order.total,
  currency: 'USD',
  paymentMethod: 'card',
  cardLast4: card.last4,
  errorCode: 'insufficient_funds',
  errorMessage: err.message,
  processorResponse: processor.rawResponse
});

Request Context

Trace requests across your system:

// Generate ID at entry point
app.use((req, res, next) => {
  req.requestId = req.headers['x-request-id'] || uuid();
  req.logger = logger.child({ requestId: req.requestId });
  next();
});

// Use throughout the request
app.post('/orders', async (req, res) => {
  req.logger.info('order_request_received', {
    userId: req.user.id,
    items: req.body.items.length
  });

  const order = await createOrder(req.body, req.logger);

  req.logger.info('order_created', { orderId: order.id });
});

// Pass logger to services
async function createOrder(data, logger) {
  logger.debug('validating_order', { itemCount: data.items.length });
  // ... creates order
  logger.info('order_saved', { orderId: order.id });
}

All logs for one request share the same requestId. Easy to trace.

What To Log (And What Not To)

Always log:

  • User authentication (login, logout, failed attempts)
  • Important business events (order placed, payment processed)
  • Errors with full context
  • External service calls (API requests, database queries)
  • Performance data (response times, queue depths)

Never log:

  • Passwords, API keys, tokens
  • Full credit card numbers
  • Personal data you dont need
  • Health check endpoints

Sanitize Sensitive Data

function sanitize(obj: any): any {
  const sensitive = ['password', 'token', 'apiKey', 'cardNumber', 'ssn'];

  return Object.fromEntries(
    Object.entries(obj).map(([key, value]) => {
      if (sensitive.some(s => key.toLowerCase().includes(s))) {
        return [key, '[REDACTED]'];
      }
      if (typeof value === 'object') {
        return [key, sanitize(value)];
      }
      return [key, value];
    })
  );
}

logger.info('user_updated', sanitize(userData));
// { email: "user@example.com", password: "[REDACTED]" }

Performance Logging

Track how long things take:

async function withTiming<T>(
  name: string,
  fn: () => Promise<T>,
  logger: Logger
): Promise<T> {
  const start = Date.now();

  try {
    const result = await fn();
    logger.info(`${name}_completed`, {
      durationMs: Date.now() - start
    });
    return result;
  } catch (error) {
    logger.error(`${name}_failed`, {
      durationMs: Date.now() - start,
      error: error.message
    });
    throw error;
  }
}

// Usage
const user = await withTiming(
  'fetch_user',
  () => db.users.findById(id),
  logger
);

Quick Setup With Pino

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label })
  },
  redact: ['password', 'token', '*.password', '*.token']
});

// Child loggers for context
const requestLogger = logger.child({
  requestId: 'abc123',
  userId: 'user_456'
});

requestLogger.info('order_placed', { orderId: 'order_789' });

Further Reading

Good logs are like good documentation - you dont appreciate them until you desperately need them. Invest the time upfront, and future-you will be grateful at 3am.

© 2026 Tawan. All rights reserved.