Skip to main content

Command Palette

Search for a command to run...

Logging Strategy: Structure Logs for Production

Learn: Logging Strategy: Structure Logs for Production

Updated
5 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Logging Strategy: Structure Logs for Production - Winston vs Pino

Problem

Unstructured logging in production environments creates critical challenges:

  • Debugging Difficulty: Plain text logs are hard to parse and correlate across services
  • Performance Impact: Synchronous logging blocks event loops; poor buffering causes memory issues
  • Scalability Issues: Centralized log aggregation systems struggle with unstructured data
  • Compliance Gaps: Audit trails lack proper context and traceability
  • Operational Blind Spots: No structured metadata for filtering, alerting, or analytics

Modern applications need structured, JSON-based logging with minimal performance overhead.

Solution

Implement structured logging using either Winston (feature-rich, flexible) or Pino (ultra-fast, minimal overhead). Both serialize logs to JSON, enabling:

  • Structured Metadata: Consistent key-value pairs for filtering and analysis
  • Performance: Asynchronous, non-blocking operations
  • Correlation: Request IDs and trace context across services
  • Integration: Native support for log aggregation platforms (ELK, Datadog, CloudWatch)
  • Levels & Filtering: Environment-aware log verbosity

Code Examples

1. Winston Setup (Feature-Rich Approach)

// logger-winston.js
const winston = require('winston');
const path = require('path');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    winston.format.splat(),
    winston.format.json()
  ),
  defaultMeta: { service: 'api-service', environment: process.env.NODE_ENV },
  transports: [
    // Error logs
    new winston.transports.File({
      filename: path.join('logs', 'error.log'),
      level: 'error',
      maxsize: 5242880, // 5MB
      maxFiles: 5,
    }),
    // Combined logs
    new winston.transports.File({
      filename: path.join('logs', 'combined.log'),
      maxsize: 5242880,
      maxFiles: 10,
    }),
  ],
});

// Console output in development
if (process.env.NODE_ENV !== 'production') {
  logger.add(
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.printf(
          ({ timestamp, level, message, ...meta }) =>
            `${timestamp} [${level}]: ${message} ${
              Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
            }`
        )
      ),
    })
  );
}

module.exports = logger;

2. Pino Setup (Performance-Optimized)

// logger-pino.js
const pino = require('pino');
const path = require('path');

const transport = pino.transport({
  targets: [
    {
      level: 'info',
      target: 'pino/file',
      options: { destination: path.join('logs', 'combined.log') },
    },
    {
      level: 'error',
      target: 'pino/file',
      options: { destination: path.join('logs', 'error.log') },
    },
    ...(process.env.NODE_ENV !== 'production'
      ? [
          {
            level: 'debug',
            target: 'pino-pretty',
            options: {
              colorize: true,
              translateTime: 'SYS:standard',
              ignore: 'pid,hostname',
            },
          },
        ]
      : []),
  ],
});

const logger = pino(
  {
    level: process.env.LOG_LEVEL || 'info',
    base: {
      service: 'api-service',
      environment: process.env.NODE_ENV,
    },
    timestamp: pino.stdTimeFunctions.isoTime,
  },
  transport
);

module.exports = logger;

3. Express Middleware Integration (Winston)

// middleware-winston.js
const logger = require('./logger-winston');
const expressWinston = require('express-winston');

const requestLogger = expressWinston.logger({
  transports: logger.transports,
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  meta: true,
  msg: 'HTTP {{req.method}} {{req.url}}',
  expressFormat: true,
  colorize: false,
  ignoreRoute: (req) => req.url === '/health',
});

const errorLogger = expressWinston.errorLogger({
  transports: logger.transports,
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
});

module.exports = { requestLogger, errorLogger };

4. Express Middleware Integration (Pino)

// middleware-pino.js
const logger = require('./logger-pino');
const pinoHttp = require('pino-http');

const httpLogger = pinoHttp(
  {
    logger,
    customLogLevel: (req, res, err) => {
      if (res.statusCode >= 500 || err) return 'error';
      if (res.statusCode >= 400) return 'warn';
      return 'info';
    },
    customSuccessMessage: (req, res) =>
      `${req.method} ${req.url} - ${res.statusCode}`,
    customErrorMessage: (req, res, err) =>
      `${req.method} ${req.url} - ${res.statusCode} - ${err.message}`,
    skip: (req) => req.url === '/health',
  },
  logger
);

module.exports = { httpLogger };

5. Application Usage (Winston)

// app-winston.js
const express = require('express');
const logger = require('./logger-winston');
const { requestLogger, errorLogger } = require('./middleware-winston');

const app = express();

app.use(requestLogger);

app.get('/api/users/:id', (req, res) => {
  const userId = req.params.id;

  logger.info('Fetching user', {
    userId,
    requestId: req.id,
    timestamp: new Date().toISOString(),
  });

  try {
    if (!userId || isNaN(userId)) {
      logger.warn('Invalid user ID provided', {
        userId,
        requestId: req.id,
        source: 'validation',
      });
      return res.status(400).json({ error: 'Invalid user ID' });
    }

    const user = { id: userId, name: 'John Doe', email: 'john@example.com' };

    logger.info('User fetched successfully', {
      userId,
      requestId: req.id,
      duration: '45ms',
    });

    res.json(user);
  } catch (error) {
    logger.error('Failed to fetch user', {
      userId,
      requestId: req.id,
      error: error.message,
      stack: error.stack,
    });
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.use(errorLogger);

app.listen(3000, () => {
  logger.info('Server started', { port: 3000, environment: process.env.NODE_ENV });
});

6. Application Usage (Pino)

// app-pino.js
const express = require('express');
const logger = require('./logger-pino');
const { httpLogger } = require('./middleware-pino');

const app = express();

app.use(httpLogger);

app.get('/api/users/:id', (req, res) => {
  const userId = req.params.id;
  const childLogger = req.log.child({ userId, requestId: req.id });

  childLogger.info('Fetching user');

  try {
    if (!userId || isNaN(userId)) {
      childLogger.warn({ userId }, 'Invalid user ID provided');
      return res.status(400).json({ error: 'Invalid user ID' });
    }

    const user = { id: userId, name: 'John Doe', email: 'john@example.com' };

    childLogger.info({ duration: '45ms' }, 'User fetched successfully');

    res.json(user);
  } catch (error) {
    childLogger.error(
      { error: error.message, stack: error.stack },
      'Failed to fetch user'
    );
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.listen(3000, () => {
  logger.info({ port: 3000 }, 'Server started');
});

7. Correlation & Request Tracing

// middleware-correlation.js
const { v4: uuidv4 } = require('uuid');

const correlationMiddleware = (req, res, next) => {
  req.id = req.headers['x-request-id'] || uuidv4();
  req.traceId = req.headers['x-trace-id'] || uuidv4();

  res.setHeader('x-request-id', req.id);
  res.setHeader('x-trace-id', req.traceId);

  next();
};

module.exports = correlationMiddleware;

8. Structured Error Logging

// error-handler.js
const logger = require('./logger-winston'); // or pino

class AppError extends Error {
  constructor(message, statusCode, context = {}) {
    super(message);
    this.statusCode = statusCode;
    this.context = context;
    this.timestamp = new Date().toISOString();
  }
}

const errorHandler = (err, req, res, next) => {
  const errorLog = {
    message: err.message,
    statusCode: err.statusCode || 500,
    requestId: req.id,
    traceId: req.traceId,
    method: req.method,
    url: req.url,
    userAgent: req.get('user-agent'),
    ip: req.ip,
    context: err.context || {},
    stack: err.stack,
  };

  if (err.statusCode >= 500) {
    logger.error(errorLog);
  } else {
    logger.warn(errorLog);
  }

  res.status(err.statusCode || 500).json({
    error: err.message,
    requestId: req.id,
  });
};

module.exports = { AppError, errorHandler };

Comparison Matrix

FeatureWinstonPino
PerformanceGoodExcellent (10x faster)
Bundle Size~1.2MB~200KB
Learning CurveModerateGentle
CustomizationExtensiveLimited but sufficient
EcosystemLarge (many transports)Growing
Child LoggersYesYes (optimized)
JSON OutputYesNative
Production ReadyYesYes

Best Practices

  1. Always use structured metadata - Include request IDs, user IDs, timestamps
  2. Async logging - Never block event loop; use transports with buffering
  3. Log levels appropriately - Error, warn, info, debug, trace
  4. Correlation IDs - Track requests across microservices
  5. Sensitive data - Never log passwords, tokens, or PII
  6. Log rotation - Implement file rotation to manage disk space
  7. Centralized aggregation - Send logs to ELK, Datadog, or CloudWatch
  8. Performance monitoring - Track logging overhead in production

Choose Pino for high-throughput APIs; choose Winston for complex, multi-transport scenarios.

Logging Strategy: Structure Logs for Production