@beethovn/logging
v1.0.0
Published
Structured logging with correlation tracking for the Beethovn platform
Maintainers
Readme
@beethovn/logging
Structured logging with correlation tracking for the Beethovn monorepo.
Features
- Structured Logging: JSON-formatted logs for easy parsing and analysis
- Correlation Tracking: Automatic correlation ID propagation across async operations
- Context Management: Track user ID, tenant ID, and custom context fields
- Error Integration: Seamless integration with
@beethovn/errorspackage - Multiple Log Levels: DEBUG, INFO, WARN, ERROR with priority filtering
- Transport System: Flexible output to console, files, or custom destinations
- Child Loggers: Create context-specific loggers with shared configuration
Installation
pnpm add @beethovn/loggingQuick Start
import { Logger, LogLevel } from '@beethovn/logging';
const logger = new Logger({
level: LogLevel.INFO,
service: 'payment-service',
json: true,
});
logger.info('Payment processed', { paymentId: '123', amount: 100 });Core Concepts
Log Levels
enum LogLevel {
DEBUG = 'debug', // Detailed debugging information
INFO = 'info', // General informational messages
WARN = 'warn', // Warning messages
ERROR = 'error', // Error messages
}Structured Log Entry
Every log entry includes:
interface LogEntry {
timestamp: string; // ISO 8601 timestamp
level: LogLevel; // Log level
message: string; // Log message
service?: string; // Service name
correlationId?: string; // Request correlation ID
userId?: string; // User ID from context
tenantId?: string; // Tenant ID from context
metadata?: Record<string, unknown>; // Additional data
error?: { // Error details (if present)
name: string;
code: string;
message: string;
stack?: string;
metadata?: Record<string, unknown>;
};
}Usage Examples
Basic Logging
const logger = new Logger({
level: LogLevel.INFO,
service: 'user-service',
});
logger.debug('User lookup started', { userId: '123' });
logger.info('User authenticated', { userId: '123' });
logger.warn('Rate limit approaching', { remaining: 10 });
logger.error('Database connection failed');Error Logging
import { ErrorFactory } from '@beethovn/errors';
try {
await processPayment(paymentId);
} catch (error: unknown) {
const err = ErrorFactory.fromUnknown(error, 'processPayment');
logger.error('Payment processing failed', err, { paymentId });
}Correlation Tracking
import { CorrelationContextManager } from '@beethovn/logging';
// In your request handler
const correlationId = CorrelationContextManager.generateCorrelationId();
CorrelationContextManager.run(
{
correlationId,
userId: req.user.id,
tenantId: req.tenant.id,
},
async () => {
// All logs within this context automatically include correlation data
logger.info('Processing request');
await service.execute();
logger.info('Request completed');
}
);Child Loggers
const mainLogger = new Logger({ service: 'api-server' });
// Create specialized logger for payment operations
const paymentLogger = mainLogger.child({ service: 'payment-processor' });
paymentLogger.info('Payment started', { paymentId: '123' });
// Logs with service: 'payment-processor'Custom Transports
import { Logger, type LogTransport, type LogEntry } from '@beethovn/logging';
class CustomTransport implements LogTransport {
log(entry: LogEntry): void {
// Send to external logging service
externalService.send(entry);
}
}
const logger = new Logger();
logger.addTransport(new CustomTransport());Console Transport
import { Logger, ConsoleTransport } from '@beethovn/logging';
const logger = new Logger({ json: false }); // Disable default JSON output
logger.addTransport(new ConsoleTransport({
json: false, // Human-readable format
colorize: true, // ANSI color codes
}));
logger.info('User logged in', { userId: '123' });
// Output: 2024-01-01T12:00:00.000Z [INFO] user-service User logged in
// Metadata: { "userId": "123" }Correlation Context Manager
The CorrelationContextManager uses Node.js AsyncLocalStorage to maintain context across async operations without explicit parameter passing.
API
class CorrelationContextManager {
// Run code with correlation context
static run<T>(context: LogContext, fn: () => T): T;
// Get current context
static getContext(): LogContext | undefined;
// Get correlation ID from context
static getCorrelationId(): string | undefined;
// Get user ID from context
static getUserId(): string | undefined;
// Get tenant ID from context
static getTenantId(): string | undefined;
// Update current context
static updateContext(updates: Partial<LogContext>): void;
// Generate new correlation ID
static generateCorrelationId(): string;
}Express Middleware Example
import { CorrelationContextManager } from '@beethovn/logging';
app.use((req, res, next) => {
const correlationId =
req.headers['x-correlation-id'] as string ||
CorrelationContextManager.generateCorrelationId();
CorrelationContextManager.run(
{
correlationId,
userId: req.user?.id,
tenantId: req.tenant?.id,
},
() => next()
);
});Event Handler Example
eventBus.on('payment.created', async (event) => {
CorrelationContextManager.run(
{
correlationId: event.correlationId,
userId: event.userId,
},
async () => {
logger.info('Processing payment event', { paymentId: event.paymentId });
await processPayment(event);
logger.info('Payment event processed');
}
);
});Integration with @beethovn/errors
The logger automatically extracts and formats error details from BeethovnError instances:
import { ValidationError } from '@beethovn/errors';
const error = new ValidationError('Invalid email format', 'email');
logger.error('Validation failed', error, { userId: '123' });
// Log output includes:
// {
// "level": "error",
// "message": "Validation failed",
// "error": {
// "name": "ValidationError",
// "code": "VALIDATION_ERROR",
// "message": "Invalid email format",
// "stack": "...",
// "metadata": { "field": "email" }
// },
// "metadata": { "userId": "123" }
// }Configuration
Logger Config
interface LoggerConfig {
level: LogLevel; // Minimum log level (default: INFO)
service: string; // Service name (default: 'beethovn')
json: boolean; // JSON output format (default: true)
timestamps: boolean; // Include timestamps (default: true)
colorize: boolean; // ANSI color codes (default: false)
}Log Context
interface LogContext {
correlationId?: string; // Request correlation ID
userId?: string; // Authenticated user ID
tenantId?: string; // Multi-tenant ID
service?: string; // Service name override
[key: string]: unknown; // Custom fields
}Best Practices
1. Use Appropriate Log Levels
logger.debug('Cache hit', { key }); // Development debugging
logger.info('User registered', { userId }); // Business events
logger.warn('Cache miss', { key }); // Potential issues
logger.error('Database error', err); // Errors requiring attention2. Include Structured Metadata
// ✅ Good: Structured metadata
logger.info('Order placed', {
orderId: '123',
userId: '456',
total: 99.99,
items: 3,
});
// ❌ Bad: Unstructured string
logger.info(`Order 123 placed by user 456 for $99.99`);3. Always Use Correlation Context
// ✅ Good: Context-wrapped handler
app.post('/orders', (req, res) => {
CorrelationContextManager.run(
{
correlationId: req.headers['x-correlation-id'] || generateId(),
userId: req.user.id,
},
async () => {
await createOrder(req.body);
}
);
});
// ❌ Bad: No correlation tracking
app.post('/orders', async (req, res) => {
await createOrder(req.body);
});4. Create Service-Specific Loggers
// ✅ Good: Service-specific logger
const paymentLogger = new Logger({ service: 'payment-service' });
// ❌ Bad: Generic logger everywhere
const logger = new Logger({ service: 'app' });5. Log Errors with Context
// ✅ Good: Error with context
try {
await operation();
} catch (error: unknown) {
const err = ErrorFactory.fromUnknown(error);
logger.error('Operation failed', err, { operationId, userId });
}
// ❌ Bad: Generic error log
try {
await operation();
} catch (error) {
logger.error('Error occurred', error);
}Log Output Examples
JSON Output (Default)
{
"timestamp": "2024-01-01T12:00:00.000Z",
"level": "info",
"message": "Payment processed",
"service": "payment-service",
"correlationId": "cor-1704110400000-a1b2c3d4",
"userId": "user-123",
"tenantId": "tenant-456",
"metadata": {
"paymentId": "pay-789",
"amount": 99.99,
"currency": "USD"
}
}Formatted Output
2024-01-01T12:00:00.000Z [INFO] payment-service (cor-1704110400000-a1b2c3d4) Payment processed
Metadata: {
"paymentId": "pay-789",
"amount": 99.99,
"currency": "USD"
}Error Output
{
"timestamp": "2024-01-01T12:00:00.000Z",
"level": "error",
"message": "Payment processing failed",
"service": "payment-service",
"correlationId": "cor-1704110400000-a1b2c3d4",
"metadata": {
"paymentId": "pay-789"
},
"error": {
"name": "DatabaseError",
"code": "DATABASE_ERROR",
"message": "Connection timeout",
"stack": "DatabaseError: Connection timeout\n at ...",
"metadata": {
"operation": "insert",
"table": "payments"
}
}
}Testing
import { describe, it, expect, vi } from 'vitest';
import { Logger, LogLevel } from '@beethovn/logging';
describe('MyService', () => {
it('should log operations', () => {
const logger = new Logger({ json: true });
const logSpy = vi.spyOn(console, 'log');
logger.info('Operation completed', { id: '123' });
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('"message":"Operation completed"')
);
});
});Migration Guide
From console.log
// Before
console.log('User logged in:', userId);
// After
logger.info('User logged in', { userId });From winston/bunyan
// Before (winston)
const logger = winston.createLogger({
transports: [new winston.transports.Console()],
});
// After
const logger = new Logger({
service: 'my-service',
});
logger.addTransport(new ConsoleTransport());Performance Considerations
- Log level filtering happens before serialization (zero overhead for filtered logs)
- JSON serialization only occurs for logs that will be output
- Correlation context uses
AsyncLocalStorage(minimal overhead) - Child loggers share transports (no duplication)
TypeScript Support
Full TypeScript support with strict types:
import type { LogEntry, LogContext, LoggerConfig } from '@beethovn/logging';
const config: LoggerConfig = {
level: LogLevel.INFO,
service: 'my-service',
json: true,
timestamps: true,
colorize: false,
};
const context: LogContext = {
correlationId: 'cor-123',
userId: 'user-456',
};License
MIT
Package Version: 1.0.0
Dependencies: @beethovn/errors
Node Version: >= 18.0.0
