Skip to content

Latest commit

 

History

History
655 lines (509 loc) · 15.3 KB

advanced-usage.md

File metadata and controls

655 lines (509 loc) · 15.3 KB

Advanced Usage Patterns

This guide covers advanced patterns and best practices for MagicLogger.

Table of Contents

Architecture Details

Transport-Optimized Architecture

MagicLogger uses a multi-layered transport system designed for maximum flexibility and performance:

Core Architecture Components:

  • Logger Core: Lightweight, synchronous message formatting and routing
  • Transport Layer: Pluggable outputs with independent configuration
  • Async Workers: Optional worker threads for non-blocking I/O
  • Batch Processing: Intelligent batching for network and file I/O

Transport Types:

  • Synchronous: Console, File (immediate write)
  • Asynchronous: HTTP, WebSocket, Database (queued with retry)
  • Streaming: Real-time outputs with backpressure handling
  • Conditional: Route logs based on level, content, or metadata

Dispatch Architecture

The logging pipeline is optimized for different scenarios:

Synchronous Mode (SyncLogger):

  • Direct dispatch to transports
  • Guaranteed delivery order
  • Best for: Audit logs, debugging, small applications
  • Trade-off: Can block on slow I/O

Asynchronous Mode (AsyncLogger):

  • Worker thread pool for I/O operations
  • Non-blocking main thread
  • Smart batching and buffering
  • Best for: Production services, high-throughput applications
  • Trade-off: Possible log loss on crash (mitigated by flush strategies)

Log Delivery Guarantees

MagicLogger provides configurable delivery guarantees:

At-Most-Once (Default for Async):

  • Logs queued in memory
  • Fast, non-blocking
  • Possible loss on crash
  • Use for: Application logs, metrics

At-Least-Once (With Retry):

  • Automatic retry on failure
  • Exponential backoff
  • Duplicate possible on retry
  • Use for: Important events, alerts

Exactly-Once (With Sync):

  • Synchronous confirmation
  • Transaction support
  • Guaranteed single delivery
  • Use for: Audit logs, compliance

Custom Transports

Building a custom transport allows you to send logs to any destination:

Create specialized transports for your infrastructure:

import { Transport } from 'magiclogger/transports/base';
import { LogEntry } from 'magiclogger/types';

class KafkaTransport extends Transport {
  private producer: KafkaProducer;
  private topic: string;
  
  constructor(options: KafkaTransportOptions) {
    super(options);
    this.topic = options.topic;
    this.producer = new KafkaProducer(options.kafka);
  }
  
  async init(): Promise<void> {
    await this.producer.connect();
  }
  
  async log(entry: LogEntry): Promise<void> {
    const message = {
      key: entry.id,
      value: JSON.stringify(entry),
      timestamp: entry.timestampMs,
      headers: {
        'log-level': entry.level,
        'service': entry.metadata?.service || 'unknown'
      }
    };
    
    await this.producer.send({
      topic: this.topic,
      messages: [message]
    });
  }
  
  async logBatch(entries: LogEntry[]): Promise<void> {
    const messages = entries.map(entry => ({
      key: entry.id,
      value: JSON.stringify(entry),
      timestamp: entry.timestampMs
    }));
    
    await this.producer.sendBatch({
      topic: this.topic,
      messages
    });
  }
  
  async close(): Promise<void> {
    await this.producer.disconnect();
  }
}

Transport Composition

Combine multiple transports with different strategies:

class ConditionalTransport extends Transport {
  private transports: Map<string, Transport>;
  
  constructor(options: ConditionalTransportOptions) {
    super(options);
    this.transports = new Map(options.transports);
  }
  
  async log(entry: LogEntry): Promise<void> {
    // Route based on level
    if (entry.level === 'error' || entry.level === 'fatal') {
      await this.transports.get('critical')?.log(entry);
    } else if (entry.level === 'warn') {
      await this.transports.get('warning')?.log(entry);
    } else {
      await this.transports.get('default')?.log(entry);
    }
  }
}

const logger = new Logger({
  transports: [
    new ConditionalTransport({
      transports: [
        ['critical', new HTTPTransport({ url: '/api/critical-logs' })],
        ['warning', new FileTransport({ filepath: './warnings.log' })],
        ['default', new ConsoleTransport()]
      ]
    })
  ]
});

Middleware & Plugins

Creating Middleware

Process logs before they reach transports:

class TimestampEnricher {
  process(entry: LogEntry): LogEntry {
    return {
      ...entry,
      timestamps: {
        iso: entry.timestamp,
        unix: entry.timestampMs,
        local: new Date(entry.timestampMs).toLocaleString()
      }
    };
  }
}

class GeoIPEnricher {
  async process(entry: LogEntry): Promise<LogEntry> {
    if (entry.context?.ip) {
      const geo = await this.lookupGeoIP(entry.context.ip);
      return {
        ...entry,
        context: {
          ...entry.context,
          geo
        }
      };
    }
    return entry;
  }
}

Plugin System

Extend logger functionality with plugins:

interface LoggerPlugin {
  name: string;
  version: string;
  
  install(logger: Logger): void;
  uninstall(logger: Logger): void;
}

class MetricsPlugin implements LoggerPlugin {
  name = 'metrics';
  version = '1.0.0';
  
  private metrics: MetricsClient;
  
  install(logger: Logger): void {
    logger.on('log', (entry) => {
      this.metrics.increment(`logs.${entry.level}`);
    });
    
    logger.on('error', (error) => {
      this.metrics.increment('logs.errors');
    });
    
    logger.on('flush', (count) => {
      this.metrics.histogram('logs.flush.size', count);
    });
  }
  
  uninstall(logger: Logger): void {
    logger.removeAllListeners();
  }
}

Distributed Tracing

W3C Trace Context Integration

Automatic trace context propagation:

import { AsyncLocalStorage } from 'async_hooks';
import { extractTraceContext } from 'magiclogger/utils/trace-context';

const traceStorage = new AsyncLocalStorage();

// Middleware to extract and store trace context
app.use((req, res, next) => {
  const traceContext = extractTraceContext(req.headers);
  
  traceStorage.run(traceContext, () => {
    req.log = createContextualLogger(traceContext);
    next();
  });
});

function createContextualLogger(traceContext) {
  return new Logger({
    context: { trace: traceContext },
    
    // Auto-inject trace context into all logs
    middleware: [(entry) => ({
      ...entry,
      trace: traceStorage.getStore() || traceContext
    })]
  });
}

// Logs automatically include trace context
app.get('/api/users', (req, res) => {
  req.log.info('Fetching users');  // Includes traceId and spanId
});

OpenTelemetry Integration

import { OTLPTransport } from 'magiclogger/transports/otlp';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

const logger = new Logger({
  transports: [
    new OTLPTransport({
      endpoint: 'http://localhost:4318/v1/logs',
      resource: new Resource({
        [SemanticResourceAttributes.SERVICE_NAME]: 'api-service',
        [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
        [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: 'production'
      }),
      
      // Map MagicLogger fields to OTLP
      fieldMapping: {
        'level': 'severityText',
        'message': 'body',
        'context': 'attributes',
        'trace.traceId': 'traceId',
        'trace.spanId': 'spanId'
      }
    })
  ]
});

Security & Compliance

PII Redaction

Automatically redact sensitive information:

import { Redactor } from 'magiclogger/extensions';

const redactor = new Redactor({
  preset: 'strict',
  
  // Custom patterns
  patterns: [
    {
      name: 'email',
      pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
      replacement: '[EMAIL]'
    },
    {
      name: 'ssn',
      pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
      replacement: '[SSN]'
    }
  ],
  
  // Keys to always redact
  keys: ['password', 'token', 'apiKey', 'secret'],
  
  // Deep scanning
  deepScan: true,
  maxDepth: 10
});

const logger = new Logger({ redactor });

Audit Logging

Create tamper-proof audit logs:

import crypto from 'crypto';

class AuditLogger extends SyncLogger {
  private hashChain: string = '';
  
  log(level: LogLevel, message: string, context?: any): void {
    const entry = this.createEntry(level, message, context);
    
    // Add hash chain
    const entryHash = this.hash(JSON.stringify(entry));
    entry.audit = {
      hash: entryHash,
      previousHash: this.hashChain,
      signature: this.sign(entryHash)
    };
    
    this.hashChain = entryHash;
    
    // Write with fsync for durability
    super.log(entry);
  }
  
  private hash(data: string): string {
    return crypto.createHash('sha256').update(data).digest('hex');
  }
  
  private sign(data: string): string {
    // Sign with private key
    return crypto.sign('sha256', Buffer.from(data), this.privateKey);
  }
}

Testing Strategies

Mock Logger for Tests

class MockLogger extends Logger {
  public logs: LogEntry[] = [];
  
  protected dispatch(entry: LogEntry): void {
    this.logs.push(entry);
  }
  
  hasLogged(level: LogLevel, message: string): boolean {
    return this.logs.some(
      log => log.level === level && log.message.includes(message)
    );
  }
  
  clear(): void {
    this.logs = [];
  }
}

// In tests
describe('UserService', () => {
  let logger: MockLogger;
  let service: UserService;
  
  beforeEach(() => {
    logger = new MockLogger();
    service = new UserService(logger);
  });
  
  it('should log user creation', async () => {
    await service.createUser({ name: 'John' });
    
    expect(logger.hasLogged('info', 'User created')).toBe(true);
    expect(logger.logs[0].context).toHaveProperty('userId');
  });
});

Snapshot Testing

import { stripAnsi } from 'magiclogger/utils';

test('log formatting', () => {
  const logger = new Logger({ useColors: true });
  const output = logger.format('info', 'Test message', { id: 123 });
  
  // Strip ANSI codes for snapshot
  expect(stripAnsi(output)).toMatchSnapshot();
});

Production Best Practices

1. Graceful Shutdown

class Application {
  private logger: AsyncLogger;
  
  async shutdown(): Promise<void> {
    this.logger.info('Shutting down gracefully');
    
    // Stop accepting new logs
    this.logger.pause();
    
    // Flush remaining logs
    await this.logger.flush();
    
    // Close transports
    await this.logger.close();
    
    process.exit(0);
  }
}

process.on('SIGTERM', () => app.shutdown());
process.on('SIGINT', () => app.shutdown());

2. Health Checks

app.get('/health', (req, res) => {
  const loggerStats = logger.getStats();
  
  const health = {
    status: 'healthy',
    logger: {
      buffered: loggerStats.bufferSize,
      dropped: loggerStats.droppedCount,
      transports: logger.getTransports().map(t => ({
        name: t.name,
        status: t.isHealthy() ? 'healthy' : 'unhealthy'
      }))
    }
  };
  
  res.json(health);
});

3. Dynamic Configuration

class ConfigurableLogger extends Logger {
  updateLevel(level: LogLevel): void {
    this.options.level = level;
    this.transports.forEach(t => t.setLevel?.(level));
  }
  
  updateSampling(rate: number): void {
    this.sampler?.setRate(rate);
  }
  
  addTransport(transport: Transport): void {
    super.addTransport(transport);
    transport.init();
  }
}

// API endpoint for runtime configuration
app.post('/admin/logging', (req, res) => {
  const { level, samplingRate, enableDebug } = req.body;
  
  if (level) logger.updateLevel(level);
  if (samplingRate) logger.updateSampling(samplingRate);
  if (enableDebug !== undefined) logger.setVerbose(enableDebug);
  
  res.json({ message: 'Logging configuration updated' });
});

4. Correlation IDs

import { v4 as uuidv4 } from 'uuid';

app.use((req, res, next) => {
  // Get or create correlation ID
  const correlationId = req.headers['x-correlation-id'] || uuidv4();
  
  // Attach to request
  req.correlationId = correlationId;
  
  // Create child logger with correlation ID
  req.logger = logger.child({
    correlationId,
    requestId: uuidv4()
  });
  
  // Pass downstream
  res.setHeader('X-Correlation-ID', correlationId);
  
  next();
});

5. Structured Error Handling

class ErrorHandler {
  private logger: Logger;
  
  handleError(error: Error, context?: any): void {
    const errorEntry = {
      message: error.message,
      stack: error.stack,
      name: error.name,
      code: error.code,
      context,
      
      // Add categorization
      category: this.categorizeError(error),
      severity: this.calculateSeverity(error),
      userImpact: this.assessUserImpact(error)
    };
    
    // Log with appropriate level
    if (errorEntry.severity === 'critical') {
      this.logger.fatal('Critical error occurred', errorEntry);
      this.alertOncall(errorEntry);
    } else if (errorEntry.severity === 'high') {
      this.logger.error('Error occurred', errorEntry);
    } else {
      this.logger.warn('Warning condition', errorEntry);
    }
  }
  
  private categorizeError(error: Error): string {
    if (error.name === 'ValidationError') return 'validation';
    if (error.name === 'DatabaseError') return 'database';
    if (error.code === 'ECONNREFUSED') return 'network';
    return 'unknown';
  }
}

Performance Benchmarks

Compare different configurations:

import { performance } from 'perf_hooks';

async function benchmark() {
  const configs = [
    { name: 'Sync', logger: new SyncLogger() },
    { name: 'Async Small Buffer', logger: new AsyncLogger({ buffer: { size: 1000 } }) },
    { name: 'Async Large Buffer', logger: new AsyncLogger({ buffer: { size: 100000 } }) }
  ];
  
  for (const config of configs) {
    const start = performance.now();
    
    for (let i = 0; i < 100000; i++) {
      config.logger.info(`Log message ${i}`, { index: i });
    }
    
    await config.logger.flush();
    const duration = performance.now() - start;
    
    console.log(`${config.name}: ${duration.toFixed(2)}ms`);
    console.log(`  Throughput: ${(100000 / (duration / 1000)).toFixed(0)} logs/sec`);
  }
}

Troubleshooting

Debug Logging

Enable verbose output for troubleshooting:

const logger = new Logger({
  verbose: true,
  debug: {
    showTransportErrors: true,
    showDroppedLogs: true,
    showBufferStats: true,
    statsInterval: 5000
  }
});

logger.on('error', (error, context) => {
  console.error('Logger error:', error, context);
});

logger.on('transport:error', (transport, error) => {
  console.error(`Transport ${transport.name} error:`, error);
});

Common Issues

  1. High memory usage: Reduce buffer size or implement sampling
  2. Dropped logs: Increase buffer size or reduce log volume
  3. Slow performance: Use AsyncLogger, batch transports
  4. Missing logs: Check transport errors, ensure proper flushing
  5. Network timeouts: Implement retry logic, use local buffering