Structured Data
JSON-formatted logs with consistent fields
Strategic logging transforms applications from black boxes into transparent, observable systems. With AI assistance, you can implement sophisticated logging patterns that provide the right information at the right time, making debugging faster and operations smoother. This guide covers battle-tested logging patterns for real-world applications.
Modern logging is about creating a narrative of your application’s behavior:
Structured Data
JSON-formatted logs with consistent fields
Contextual Information
Request IDs, user context, and trace data
Performance Awareness
Minimal overhead with maximum insight
Security First
Never log sensitive data
Transform unstructured logs into queryable, analyzable data.
// Traditional logging (avoid this)console.log('User ' + userId + ' logged in from ' + ipAddress);
// AI-recommended structured loggingimport winston from 'winston';
const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), defaultMeta: { service: 'api-gateway', version: process.env.APP_VERSION }, transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }) ]});
// Usage with structured datalogger.info('User login successful', { userId: user.id, email: user.email, ipAddress: req.ip, userAgent: req.get('User-Agent'), loginMethod: 'oauth', provider: 'google'});
// Output:{ "timestamp": "2024-01-15T10:30:45.123Z", "level": "info", "message": "User login successful", "service": "api-gateway", "version": "1.2.3", "userId": "usr_123", "email": "user@example.com", "ipAddress": "192.168.1.1", "userAgent": "Mozilla/5.0...", "loginMethod": "oauth", "provider": "google"}
import structlogimport loggingfrom datetime import datetime
# Configure structured loggingstructlog.configure( processors=[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.processors.JSONRenderer() ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True,)
# Create logger with contextlogger = structlog.get_logger()logger = logger.bind( service="api-gateway", environment="production", region="us-east-1")
# Log with structured datalogger.info( "user_login_successful", user_id=user.id, email=user.email, ip_address=request.remote_addr, login_method="oauth", provider="github", duration_ms=152)
Track requests across your entire system with correlation IDs.
// Express middleware for request contextimport { v4 as uuidv4 } from 'uuid';
class ContextualLogger { constructor(baseLogger) { this.baseLogger = baseLogger; }
createRequestLogger(req) { const requestId = req.headers['x-request-id'] || uuidv4(); const userId = req.user?.id || 'anonymous';
// Store in request for downstream use req.requestId = requestId; req.logger = this.baseLogger.child({ requestId, userId, method: req.method, path: req.path, ip: req.ip });
return req.logger; }}
// Middleware implementationconst loggingMiddleware = (logger) => (req, res, next) => { const contextLogger = new ContextualLogger(logger); req.logger = contextLogger.createRequestLogger(req);
// Log request start req.logger.info('Request started');
// Capture response details const startTime = Date.now(); const originalSend = res.send;
res.send = function(data) { res.send = originalSend;
req.logger.info('Request completed', { statusCode: res.statusCode, duration: Date.now() - startTime, responseSize: Buffer.byteLength(data) });
return res.send(data); };
next();};
// Usage in routesapp.get('/api/users/:id', (req, res) => { req.logger.info('Fetching user', { targetUserId: req.params.id });
// Your logic here...});
Monitor application performance through strategic logging.
// Performance logging utilityclass PerformanceLogger { constructor(logger) { this.logger = logger; this.timers = new Map(); }
startTimer(operation, metadata = {}) { const id = `${operation}-${Date.now()}-${Math.random()}`; this.timers.set(id, { operation, startTime: process.hrtime.bigint(), metadata });
this.logger.debug('Operation started', { operation, timerId: id, ...metadata });
return id; }
endTimer(timerId, additionalData = {}) { const timer = this.timers.get(timerId); if (!timer) { this.logger.warn('Timer not found', { timerId }); return; }
const endTime = process.hrtime.bigint(); const durationMs = Number(endTime - timer.startTime) / 1e6;
this.timers.delete(timerId);
const logData = { operation: timer.operation, durationMs, ...timer.metadata, ...additionalData };
// Log based on duration if (durationMs > 1000) { this.logger.warn('Slow operation detected', logData); } else { this.logger.info('Operation completed', logData); }
// Emit metrics if configured if (this.metricsClient) { this.metricsClient.histogram( `operation.duration.${timer.operation}`, durationMs ); }
return durationMs; }
async measureAsync(operation, fn, metadata = {}) { const timerId = this.startTimer(operation, metadata); try { const result = await fn(); this.endTimer(timerId, { status: 'success' }); return result; } catch (error) { this.endTimer(timerId, { status: 'error', error: error.message }); throw error; } }}
// Usageconst perfLogger = new PerformanceLogger(logger);
// Manual timingconst timerId = perfLogger.startTimer('database_query', { query: 'SELECT * FROM users', table: 'users'});const results = await db.query('SELECT * FROM users');perfLogger.endTimer(timerId, { rowCount: results.length });
// Automatic timingconst user = await perfLogger.measureAsync( 'fetch_user', () => userService.getUser(userId), { userId });
Capture comprehensive error information for faster debugging.
AI-Enhanced Error Logging
class ErrorLogger { constructor(logger) { this.logger = logger; }
logError(error, context = {}) { // Extract useful error information const errorInfo = { message: error.message, name: error.name, stack: error.stack, code: error.code, statusCode: error.statusCode || 500 };
// Add request context if available if (context.req) { errorInfo.request = { method: context.req.method, url: context.req.url, headers: this.sanitizeHeaders(context.req.headers), body: this.sanitizeBody(context.req.body), user: context.req.user?.id }; }
// Add custom error properties if (error.details) { errorInfo.details = error.details; }
// Log with appropriate level if (error.statusCode >= 500 || !error.statusCode) { this.logger.error('Application error', { ...errorInfo, ...context }); } else if (error.statusCode >= 400) { this.logger.warn('Client error', { ...errorInfo, ...context }); }
// Log to error tracking service if (this.errorTracker) { this.errorTracker.captureException(error, { extra: context, user: context.req?.user }); } }
sanitizeHeaders(headers) { const sensitive = ['authorization', 'cookie', 'x-api-key']; const sanitized = { ...headers };
sensitive.forEach(key => { if (sanitized[key]) { sanitized[key] = '[REDACTED]'; } });
return sanitized; }
sanitizeBody(body) { if (!body) return body;
const sensitive = ['password', 'token', 'secret', 'creditCard']; const sanitized = { ...body };
const recursiveSanitize = (obj) => { Object.keys(obj).forEach(key => { if (sensitive.some(s => key.toLowerCase().includes(s))) { obj[key] = '[REDACTED]'; } else if (typeof obj[key] === 'object' && obj[key] !== null) { recursiveSanitize(obj[key]); } }); };
recursiveSanitize(sanitized); return sanitized; }}
Combine logging with distributed tracing for complete observability.
// OpenTelemetry integrationimport { trace, context } from '@opentelemetry/api';
class TracingLogger { constructor(logger) { this.logger = logger; this.tracer = trace.getTracer('application'); }
log(level, message, attributes = {}) { const span = trace.getActiveSpan();
if (span) { // Add trace context to logs const spanContext = span.spanContext(); this.logger[level](message, { traceId: spanContext.traceId, spanId: spanContext.spanId, traceFlags: spanContext.traceFlags, ...attributes });
// Add log event to span span.addEvent(message, attributes); } else { // No active span, log normally this.logger[level](message, attributes); } }
async traceOperation(name, operation, attributes = {}) { return this.tracer.startActiveSpan(name, async (span) => { try { this.log('info', `Starting ${name}`, attributes);
const result = await operation();
span.setStatus({ code: 1 }); // OK this.log('info', `Completed ${name}`, { ...attributes, duration: span.duration });
return result; } catch (error) { span.setStatus({ code: 2, // ERROR message: error.message }); span.recordException(error);
this.log('error', `Failed ${name}`, { ...attributes, error: error.message, stack: error.stack });
throw error; } finally { span.end(); } }); }}
// Usageconst tracingLogger = new TracingLogger(logger);
await tracingLogger.traceOperation( 'process_payment', async () => { // Your payment logic return await paymentService.process(payment); }, { paymentId: payment.id, amount: payment.amount, currency: payment.currency });
Track security-relevant events for compliance and forensics.
class AuditLogger { constructor(logger, options = {}) { this.logger = logger; this.options = { includeIp: true, includeUserAgent: true, includeTimestamp: true, ...options }; }
logAuditEvent(event, context = {}) { const auditEntry = { eventType: event.type, eventCategory: event.category, timestamp: new Date().toISOString(), actor: { userId: context.user?.id, username: context.user?.username, role: context.user?.role, ip: context.req?.ip, userAgent: context.req?.get('User-Agent') }, target: event.target, action: event.action, result: event.result, reason: event.reason, metadata: event.metadata };
// Sign the audit entry for tamper detection if (this.options.sign) { auditEntry.signature = this.signEntry(auditEntry); }
// Log to separate audit log this.logger.info('AUDIT_EVENT', auditEntry);
// Store in audit database if configured if (this.auditStore) { this.auditStore.save(auditEntry); } }
// Common audit events logLogin(user, req, success = true) { this.logAuditEvent({ type: 'AUTH_LOGIN', category: 'AUTHENTICATION', target: { type: 'USER', id: user.id }, action: 'LOGIN', result: success ? 'SUCCESS' : 'FAILURE', metadata: { loginMethod: req.body.method, mfaUsed: user.mfaEnabled } }, { user, req }); }
logDataAccess(user, resource, action, req) { this.logAuditEvent({ type: 'DATA_ACCESS', category: 'AUTHORIZATION', target: { type: resource.type, id: resource.id, name: resource.name }, action: action.toUpperCase(), result: 'SUCCESS', metadata: { fields: resource.fields, filters: resource.filters } }, { user, req }); }
logConfigChange(user, config, req) { this.logAuditEvent({ type: 'CONFIG_CHANGE', category: 'SYSTEM', target: { type: 'CONFIGURATION', id: config.key }, action: 'UPDATE', result: 'SUCCESS', metadata: { oldValue: config.oldValue, newValue: config.newValue, changeReason: config.reason } }, { user, req }); }}
Prevent log flooding while maintaining visibility.
class SamplingLogger { constructor(logger, config = {}) { this.logger = logger; this.config = { defaultSampleRate: 0.1, // 10% rules: [], windowSize: 60000, // 1 minute ...config }; this.counters = new Map(); }
shouldLog(level, message, metadata = {}) { // Always log errors if (level === 'error' || level === 'fatal') { return true; }
// Check custom rules for (const rule of this.config.rules) { if (rule.matches(level, message, metadata)) { return this.applySampling(rule.sampleRate, rule.key); } }
// Apply default sampling return Math.random() < this.config.defaultSampleRate; }
applySampling(rate, key) { if (rate === 1) return true; if (rate === 0) return false;
// Ensure minimum representation const counter = this.getCounter(key); if (counter.total === 0) { counter.sampled++; counter.total++; return true; }
const currentRate = counter.sampled / counter.total; if (currentRate < rate) { counter.sampled++; counter.total++; return true; }
counter.total++; return false; }
getCounter(key) { if (!this.counters.has(key)) { this.counters.set(key, { total: 0, sampled: 0, windowStart: Date.now() }); }
const counter = this.counters.get(key);
// Reset counter if window expired if (Date.now() - counter.windowStart > this.config.windowSize) { counter.total = 0; counter.sampled = 0; counter.windowStart = Date.now(); }
return counter; }
log(level, message, metadata = {}) { if (this.shouldLog(level, message, metadata)) { this.logger[level](message, metadata); } else { // Track dropped logs this.logger.debug('Log dropped due to sampling', { originalLevel: level, originalMessage: message }); } }}
// Configurationconst samplingLogger = new SamplingLogger(logger, { defaultSampleRate: 0.1, rules: [ { key: 'health-check', matches: (level, msg) => msg.includes('health check'), sampleRate: 0.01 // 1% for health checks }, { key: 'debug-verbose', matches: (level) => level === 'debug', sampleRate: 0.05 // 5% for debug logs } ]});
Send logs to multiple destinations based on criteria.
class MultiTransportLogger { constructor() { this.transports = []; }
addTransport(transport, options = {}) { this.transports.push({ transport, levels: options.levels || ['error', 'warn', 'info', 'debug'], filter: options.filter || (() => true), transform: options.transform || (log => log) }); }
log(level, message, metadata = {}) { const logEntry = { timestamp: new Date().toISOString(), level, message, ...metadata };
this.transports.forEach(({ transport, levels, filter, transform }) => { if (levels.includes(level) && filter(logEntry)) { const transformed = transform(logEntry); transport.log(level, transformed); } }); }
// Convenience methods error(message, metadata) { this.log('error', message, metadata); } warn(message, metadata) { this.log('warn', message, metadata); } info(message, metadata) { this.log('info', message, metadata); } debug(message, metadata) { this.log('debug', message, metadata); }}
// Configure multi-transportconst logger = new MultiTransportLogger();
// Console for developmentlogger.addTransport(new ConsoleTransport(), { levels: ['error', 'warn', 'info'], transform: (log) => ({ ...log, // Add color coding message: colorize(log.level, log.message) })});
// File for persistencelogger.addTransport(new FileTransport('app.log'), { levels: ['error', 'warn', 'info', 'debug']});
// CloudWatch for productionlogger.addTransport(new CloudWatchTransport(), { levels: ['error', 'warn', 'info'], filter: (log) => process.env.NODE_ENV === 'production', transform: (log) => ({ ...log, environment: process.env.NODE_ENV, service: process.env.SERVICE_NAME })});
// Sentry for errorslogger.addTransport(new SentryTransport(), { levels: ['error'], transform: (log) => ({ ...log, fingerprint: generateFingerprint(log) })});
Log Levels
What to Log
What NOT to Log
Performance Tips
Use consistent, searchable log messages:
// AI-recommended log message patternsconst LogMessages = { // Operation lifecycle OPERATION_START: (op) => `Starting ${op}`, OPERATION_SUCCESS: (op) => `Completed ${op}`, OPERATION_FAILURE: (op) => `Failed to ${op}`, OPERATION_RETRY: (op, attempt) => `Retrying ${op} (attempt ${attempt})`,
// API patterns API_REQUEST: (method, path) => `${method} ${path}`, API_RESPONSE: (method, path, status) => `${method} ${path} ${status}`, API_ERROR: (method, path, error) => `${method} ${path} failed: ${error}`,
// Database patterns DB_QUERY: (operation, table) => `Database ${operation} on ${table}`, DB_CONNECTION: (action) => `Database connection ${action}`, DB_TRANSACTION: (action) => `Transaction ${action}`,
// Authentication AUTH_LOGIN: (method) => `User login via ${method}`, AUTH_LOGOUT: () => 'User logout', AUTH_FAILURE: (reason) => `Authentication failed: ${reason}`,
// Performance SLOW_OPERATION: (op, duration) => `Slow operation detected: ${op} took ${duration}ms`, MEMORY_WARNING: (usage) => `High memory usage: ${usage}MB`,
// Business events ORDER_PLACED: () => 'Order placed', PAYMENT_PROCESSED: () => 'Payment processed', USER_REGISTERED: () => 'New user registered'};
// Usagelogger.info(LogMessages.OPERATION_START('payment_processing'), { paymentId: payment.id, amount: payment.amount});
// Jest test exampledescribe('Logging Tests', () => { let logger; let mockTransport;
beforeEach(() => { mockTransport = { logs: [], log: jest.fn((level, entry) => { mockTransport.logs.push({ level, ...entry }); }) };
logger = new Logger(); logger.addTransport(mockTransport); });
test('should log structured data', () => { logger.info('User action', { userId: '123', action: 'login' });
expect(mockTransport.logs).toHaveLength(1); expect(mockTransport.logs[0]).toMatchObject({ level: 'info', message: 'User action', userId: '123', action: 'login' }); });
test('should sanitize sensitive data', () => { logger.info('User update', { userId: '123', password: 'secret123', creditCard: '4111111111111111' });
expect(mockTransport.logs[0].password).toBe('[REDACTED]'); expect(mockTransport.logs[0].creditCard).toBe('[REDACTED]'); });
test('should respect sampling rules', () => { const samplingLogger = new SamplingLogger(logger, { defaultSampleRate: 0 });
samplingLogger.info('Should be dropped'); expect(mockTransport.logs).toHaveLength(0);
samplingLogger.error('Should be logged'); expect(mockTransport.logs).toHaveLength(1); });});
Master logging with: