The Problem with Microservice Logging

Traditional logging approaches fail in microservices. Learn how to implement effective distributed logging.

Distributed logging seems simple until your system grows. Here's why traditional logging approaches fail in microservices, and how to do it right.

Common Anti-Patterns

1. Inconsistent Log Formats

// ❌ BAD: Different formats across services
// Service A
logger.info('User created', { userId: 123 });

// Service B
console.log(`Order processed: ${orderId}`);

// Service C
winston.log('info', 'Payment received', {
  amount: 100,
  currency: 'USD'
});

2. Missing Context

// ❌ BAD: Insufficient context
class PaymentService {
  async processPayment(payment: Payment) {
    try {
      await this.stripe.charge(payment.amount);
      logger.info('Payment processed');  // Which payment? What amount?
    } catch (error) {
      logger.error('Payment failed');    // Why? For whom?
    }
  }
}

Better Approaches

1. Structured Logging

// ✅ BETTER: Consistent structured logs
interface LogEntry {
  timestamp: string;
  level: LogLevel;
  service: string;
  traceId: string;
  spanId: string;
  message: string;
  context: Record<string, any>;
  metadata: {
    host: string;
    environment: string;
    version: string;
  };
}

class StructuredLogger {
  constructor(
    private service: string,
    private options: LoggerOptions
  ) {}

  info(
    message: string,
    context?: Record<string, any>
  ): void {
    this.log('info', message, context);
  }

  private log(
    level: LogLevel,
    message: string,
    context?: Record<string, any>
  ): void {
    const entry: LogEntry = {
      timestamp: new Date().toISOString(),
      level,
      service: this.service,
      traceId: this.getTraceId(),
      spanId: this.getSpanId(),
      message,
      context: context || {},
      metadata: {
        host: os.hostname(),
        environment: process.env.NODE_ENV!,
        version: process.env.APP_VERSION!
      }
    };

    this.output(entry);
  }
}

2. Correlation Context

// ✅ BETTER: Request tracing
class RequestContext {
  private static store = new AsyncLocalStorage<Context>();

  static middleware() {
    return (req: Request, res: Response, next: NextFunction) => {
      const context = {
        traceId: req.headers['x-trace-id'] || uuid(),
        spanId: uuid(),
        userId: req.user?.id,
        sessionId: req.session?.id
      };

      RequestContext.store.run(context, () => next());
    };
  }

  static getContext(): Context {
    return this.store.getStore() || {};
  }
}

class CorrelatedLogger {
  info(message: string, context?: Record<string, any>) {
    const requestContext = RequestContext.getContext();
    
    this.log('info', message, {
      ...requestContext,
      ...context
    });
  }
}

3. Log Aggregation

// ✅ BETTER: Centralized logging
class LogAggregator {
  private readonly buffer: LogEntry[] = [];
  private readonly flushInterval: number = 5000; // 5s

  constructor(
    private readonly logstash: LogstashClient,
    private readonly options: AggregatorOptions
  ) {
    this.startFlushInterval();
  }

  append(entry: LogEntry): void {
    this.buffer.push(entry);
    
    if (this.buffer.length >= this.options.batchSize) {
      this.flush();
    }
  }

  private async flush(): Promise<void> {
    if (this.buffer.length === 0) return;

    const batch = this.buffer.splice(0, this.buffer.length);
    
    try {
      await this.logstash.bulk(batch);
    } catch (error) {
      // Handle failed delivery
      this.handleFailedDelivery(batch, error);
    }
  }
}

Advanced Patterns

1. Semantic Logging

// ✅ BETTER: Event-based logging
class OrderLogger {
  logOrderCreated(order: Order): void {
    this.logger.event('order_created', {
      orderId: order.id,
      userId: order.userId,
      items: order.items.length,
      total: order.total,
      currency: order.currency
    });
  }

  logOrderFulfillment(order: Order): void {
    this.logger.event('order_fulfilled', {
      orderId: order.id,
      warehouse: order.fulfillment.warehouse,
      shippingMethod: order.fulfillment.method,
      estimatedDelivery: order.fulfillment.estimatedDate
    });
  }
}

class EventLogger {
  event(
    name: string,
    data: Record<string, any>
  ): void {
    this.logger.info(name, {
      type: 'event',
      event: name,
      data,
      timestamp: new Date().toISOString()
    });
  }
}

2. Log Sampling

// ✅ BETTER: Smart log sampling
class SamplingLogger {
  private samplingRates = {
    error: 1.0,      // Log all errors
    warn: 1.0,       // Log all warnings
    info: 0.1,       // Log 10% of info
    debug: 0.01      // Log 1% of debug
  };

  log(
    level: LogLevel,
    message: string,
    context?: Record<string, any>
  ): void {
    if (!this.shouldSample(level)) {
      return;
    }

    this.logger.log(level, message, context);
  }

  private shouldSample(level: LogLevel): boolean {
    const rate = this.samplingRates[level] || 1.0;
    return Math.random() < rate;
  }
}

3. Log Enrichment

// ✅ BETTER: Context enrichment
class LogEnricher {
  enrich(entry: LogEntry): EnrichedLogEntry {
    return {
      ...entry,
      environment: this.getEnvironmentInfo(),
      runtime: this.getRuntimeInfo(),
      metrics: this.getMetrics(),
      kubernetes: this.getK8sMetadata()
    };
  }

  private getMetrics() {
    return {
      memory: process.memoryUsage(),
      cpu: process.cpuUsage(),
      uptime: process.uptime()
    };
  }

  private getK8sMetadata() {
    return {
      namespace: process.env.K8S_NAMESPACE,
      pod: process.env.K8S_POD_NAME,
      node: process.env.K8S_NODE_NAME
    };
  }
}

Best Practices

1. Log Levels

// ✅ BETTER: Clear log level guidelines
enum LogLevel {
  ERROR = 'error',   // Service is unusable
  WARN = 'warn',     // Service degraded
  INFO = 'info',     // Important business events
  DEBUG = 'debug',   // Development information
  TRACE = 'trace'    // Detailed debugging
}

class ServiceLogger {
  error(message: string, error: Error): void {
    this.log(LogLevel.ERROR, message, {
      error: {
        message: error.message,
        stack: error.stack,
        name: error.name
      }
    });
  }
}

2. Performance Monitoring

// ✅ BETTER: Performance tracking
class PerformanceLogger {
  startOperation(name: string): Operation {
    const start = process.hrtime.bigint();
    
    return {
      end: () => {
        const end = process.hrtime.bigint();
        const duration = Number(end - start) / 1e6; // ms
        
        this.logger.info(`Operation ${name} completed`, {
          operation: name,
          durationMs: duration,
          timestamp: new Date().toISOString()
        });
      }
    };
  }
}

// Usage
async function processOrder(order: Order) {
  const operation = performanceLogger
    .startOperation('process_order');
  
  try {
    await processOrderSteps(order);
  } finally {
    operation.end();
  }
}

Conclusion

For better microservice logging:

  • Use structured, consistent log formats
  • Maintain request context across services
  • Implement proper log aggregation
  • Consider log sampling for high-volume services
  • Enrich logs with relevant metadata

Remember: Good logging is about finding the right balance between visibility and noise.