Node.js Logging: The Art of Digital Breadcrumbs

The Importance of Logging

In the labyrinth of modern applications, logs are our digital breadcrumbs - helping us trace the path of execution, debug issues, and understand system behavior. But like any tool, logging is only as good as its implementation. Let’s explore how to master the art of logging in Node.js applications.

Beyond console.log

While console.log is great for development, production applications need a more robust logging solution. Let’s explore how to build one:

const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Development logging
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

Structured Logging

Structured logging is like organizing your closet - everything has its place and is easy to find:

logger.info('User action', {
  userId: '123',
  action: 'login',
  timestamp: new Date().toISOString(),
  metadata: {
    ip: '192.168.1.1',
    userAgent: 'Mozilla/5.0...'
  }
});

Log Levels and When to Use Them

Think of log levels as different types of breadcrumbs:

  1. ERROR: Something’s broken and needs immediate attention
try {
  await processPayment(order);
} catch (error) {
  logger.error('Payment processing failed', {
    orderId: order.id,
    error: error.message,
    stack: error.stack
  });
}
  1. WARN: Something’s not quite right, but the system can handle it
if (retries > maxRetries) {
  logger.warn('Max retries reached for operation', {
    operation: 'fetchUserData',
    attempts: retries
  });
}
  1. INFO: Notable events in the application’s lifecycle
logger.info('Server started', {
  port: process.env.PORT,
  environment: process.env.NODE_ENV
});
  1. DEBUG: Detailed information for debugging
logger.debug('Cache miss', {
  key: cacheKey,
  timestamp: Date.now()
});

Best Practices

1. Context is King

Always include relevant context in your logs:

const requestLogger = async (ctx, next) => {
  const start = Date.now();
  try {
    await next();
  } finally {
    const ms = Date.now() - start;
    logger.info('Request completed', {
      method: ctx.method,
      path: ctx.path,
      status: ctx.status,
      duration: `${ms}ms`,
      requestId: ctx.requestId
    });
  }
};

2. Error Handling

Proper error logging can save hours of debugging:

class ApplicationError extends Error {
  constructor(message, context = {}) {
    super(message);
    this.name = this.constructor.name;
    this.context = context;
    Error.captureStackTrace(this, this.constructor);
  }
}

try {
  throw new ApplicationError('Invalid input', {
    userId: '123',
    inputData: data
  });
} catch (error) {
  logger.error('Operation failed', {
    error: {
      message: error.message,
      name: error.name,
      context: error.context,
      stack: error.stack
    }
  });
}

3. Performance Monitoring

Use logs to track performance metrics:

const performanceLogger = async (ctx, next) => {
  const start = process.hrtime();
  
  try {
    await next();
  } finally {
    const [seconds, nanoseconds] = process.hrtime(start);
    const duration = seconds * 1000 + nanoseconds / 1000000;
    
    logger.info('Performance metric', {
      endpoint: ctx.path,
      method: ctx.method,
      duration: `${duration.toFixed(2)}ms`,
      timestamp: new Date().toISOString()
    });
  }
};

Log Management and Analysis

Collecting logs is only half the battle. Here’s how to make them useful:

  1. Centralized Logging
const winston = require('winston');
require('winston-elasticsearch');

const esTransport = new winston.transports.Elasticsearch({
  level: 'info',
  clientOpts: { node: 'http://localhost:9200' },
  indexPrefix: 'logs'
});

logger.add(esTransport);
  1. Log Rotation
const { createLogger, transports } = require('winston');
require('winston-daily-rotate-file');

const fileRotateTransport = new transports.DailyRotateFile({
  filename: 'logs/app-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  maxSize: '20m',
  maxFiles: '14d'
});

const logger = createLogger({
  transports: [fileRotateTransport]
});

Security Considerations

Remember to protect sensitive information in your logs:

const sanitizeUser = (user) => ({
  id: user.id,
  username: user.username,
  // Exclude password, email, etc.
});

logger.info('User profile updated', {
  user: sanitizeUser(user),
  changes: sanitizeChanges(changes)
});

Conclusion

Effective logging is an art that balances detail with clarity, performance with thoroughness. By following these practices, you’ll create logs that are not just records of what happened, but valuable tools for understanding and improving your application.

Remember: Good logs are like good documentation - they tell a story. Make sure your logs tell the story you need to hear when things go wrong.