CompositeLogger
Forward log messages to multiple destinations simultaneously
Table of contents
- Overview
- Basic Usage
- Dynamic Logger Management
- Advanced Patterns
- API Reference
- Use Cases
- Best Practices
- Performance Considerations
- Example: Complete Setup
Overview
CompositeLogger implements the Composite Pattern to forward log messages to multiple logger implementations simultaneously. This allows you to log to several destinations (console + file, file + cloud service, etc.) without managing each logger separately.
Key Features:
- Forward to multiple loggers simultaneously
- Dynamic logger management (add/remove at runtime)
- Support for nested composites
- Zero-configuration default behavior
- Type-safe logger interface
Basic Usage
Log to Console and File
import {
CompositeLogger,
ConsoleLogger,
FileLogger,
MigrationScriptExecutor
} from '@migration-script-runner/core';
// Create composite with multiple loggers
const logger = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: '/var/log/migrations.log' })
]);
const executor = new MigrationScriptExecutor({ handler, logger });
await executor.migrate();
Every log message will be written to both the console and the file.
Log to Multiple Files
const logger = new CompositeLogger([
new FileLogger({ logPath: '/var/log/migrations.log' }),
new FileLogger({ logPath: '/var/log/audit.log' }),
new FileLogger({ logPath: '/tmp/debug.log' })
]);
Dynamic Logger Management
Adding Loggers at Runtime
const logger = new CompositeLogger([
new ConsoleLogger()
]);
// Later, enable file logging
logger.addLogger(new FileLogger({ logPath: '/var/log/app.log' }));
// Now logs to both console and file
logger.info('This goes to console AND file');
Removing Loggers at Runtime
const fileLogger = new FileLogger({ logPath: '/tmp/temp.log' });
const logger = new CompositeLogger([
new ConsoleLogger(),
fileLogger
]);
// Later, disable file logging
const removed = logger.removeLogger(fileLogger);
console.log(`Removed: ${removed}`); // true
// Now only logs to console
logger.info('This only goes to console');
Querying Registered Loggers
const logger = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: '/var/log/app.log' })
]);
const loggers = logger.getLoggers();
console.log(`Logging to ${loggers.length} destinations`);
Advanced Patterns
Nested Composite Loggers
CompositeLogger can contain other CompositeLoggers:
// Create specialized composites
const localLoggers = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: '/var/log/local.log' })
]);
const cloudLoggers = new CompositeLogger([
new CloudWatchLogger(),
new DatadogLogger()
]);
// Combine them in a top-level composite
const logger = new CompositeLogger([
localLoggers,
cloudLoggers
]);
// Logs to ALL four destinations
logger.info('Migration started');
Conditional Logging
Enable/disable specific loggers based on environment:
const logger = new CompositeLogger([
new ConsoleLogger()
]);
// Enable file logging in production
if (process.env.NODE_ENV === 'production') {
logger.addLogger(new FileLogger({
logPath: '/var/log/production.log',
maxFileSize: 100 * 1024 * 1024, // 100MB
maxFiles: 30
}));
}
// Enable debug logging in development
if (process.env.NODE_ENV === 'development') {
logger.addLogger(new FileLogger({
logPath: '/tmp/debug.log',
includeTimestamp: true
}));
}
Silent Mode for Testing
Disable all output during tests:
import { CompositeLogger, SilentLogger } from '@migration-script-runner/core';
describe('Migration Tests', () => {
it('should migrate successfully', async () => {
// Use empty composite (no loggers)
const logger = new CompositeLogger();
// Or use SilentLogger explicitly
const silentLogger = new CompositeLogger([new SilentLogger()]);
const executor = new MigrationScriptExecutor({ handler, logger });
const result = await executor.migrate();
expect(result.success).to.be.true;
});
});
API Reference
Constructor
constructor(loggers?: ILogger[])
Creates a new CompositeLogger instance.
Parameters:
loggers(optional) - Array of logger instances to forward messages to
Example:
// Empty composite
const logger1 = new CompositeLogger();
// With initial loggers
const logger2 = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: '/var/log/app.log' })
]);
addLogger()
addLogger(logger: ILogger): void
Add a logger to the composite. The logger will start receiving all subsequent log messages.
Parameters:
logger- Logger instance to add
Example:
const composite = new CompositeLogger();
composite.addLogger(new ConsoleLogger());
composite.addLogger(new FileLogger({ logPath: '/var/log/app.log' }));
removeLogger()
removeLogger(logger: ILogger): boolean
Remove a logger from the composite. The logger will stop receiving log messages.
Parameters:
logger- Logger instance to remove
Returns:
trueif logger was found and removedfalseif logger was not found
Example:
const fileLogger = new FileLogger({ logPath: '/tmp/temp.log' });
const composite = new CompositeLogger([fileLogger]);
if (composite.removeLogger(fileLogger)) {
console.log('File logging disabled');
}
getLoggers()
getLoggers(): ILogger[]
Get all registered loggers. Returns a copy of the loggers array to prevent external modification.
Returns:
- Array of registered logger instances
Example:
const composite = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: '/var/log/app.log' })
]);
const count = composite.getLoggers().length;
console.log(`Logging to ${count} destinations`);
Log Methods
All standard ILogger methods forward to all registered loggers:
info(message: string, ...args: unknown[]): void
warn(message: string, ...args: unknown[]): void
error(message: string, ...args: unknown[]): void
debug(message: string, ...args: unknown[]): void
log(message: string, ...args: unknown[]): void
Example:
const logger = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: '/var/log/app.log' })
]);
// Goes to both console and file
logger.info('Migration started');
logger.warn('Schema mismatch detected');
logger.error('Migration failed', new Error('Connection lost'));
Use Cases
Development Environment
// Console + debug file
const logger = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({
logPath: '/tmp/debug.log',
includeTimestamp: true
})
]);
Production Environment
// File + cloud service
const logger = new CompositeLogger([
new FileLogger({
logPath: '/var/log/migrations.log',
maxFileSize: 100 * 1024 * 1024,
maxFiles: 30
}),
new CloudWatchLogger({
logGroupName: '/app/migrations',
logStreamName: process.env.INSTANCE_ID
})
]);
CI/CD Pipeline
// Console + file for artifact collection
const logger = new CompositeLogger([
new ConsoleLogger(), // For real-time viewing
new FileLogger({
logPath: '/ci/artifacts/migration.log'
}) // For build artifacts
]);
Multi-Tenant Application
// Separate log per tenant
const logger = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: `/var/log/tenant-${tenantId}.log` }),
new CloudWatchLogger({ logStreamName: `tenant-${tenantId}` })
]);
Best Practices
Keep References to Removable Loggers
// ✅ Good - can remove later
const fileLogger = new FileLogger({ logPath: '/tmp/temp.log' });
const logger = new CompositeLogger([
new ConsoleLogger(),
fileLogger
]);
// Later...
logger.removeLogger(fileLogger);
// ❌ Bad - can't remove without reference
const logger = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: '/tmp/temp.log' })
]);
// No way to remove the FileLogger!
Use Meaningful Logger Groups
// Group related loggers
const persistentLoggers = new CompositeLogger([
new FileLogger({ logPath: '/var/log/app.log' }),
new CloudWatchLogger()
]);
const ephemeralLoggers = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: '/tmp/debug.log' })
]);
// Combine or use separately based on needs
const logger = process.env.DEBUG
? new CompositeLogger([persistentLoggers, ephemeralLoggers])
: persistentLoggers;
Handle Logger Failures Gracefully
CompositeLogger doesn’t catch exceptions from individual loggers. If one logger throws, the rest won’t receive the message. Wrap loggers that might fail:
class SafeCloudLogger implements ILogger {
constructor(private cloudLogger: CloudWatchLogger) {}
info(message: string, ...args: unknown[]): void {
try {
this.cloudLogger.info(message, ...args);
} catch (error) {
console.error('Cloud logging failed:', error);
}
}
// ... implement other methods similarly
}
const logger = new CompositeLogger([
new ConsoleLogger(),
new SafeCloudLogger(new CloudWatchLogger())
]);
Performance Considerations
Logger Count Impact
Each logger adds processing overhead. For high-throughput applications:
// ✅ Good - reasonable number of loggers
const logger = new CompositeLogger([
new ConsoleLogger(),
new FileLogger({ logPath: '/var/log/app.log' })
]);
// ⚠️ Consider - many loggers may impact performance
const logger = new CompositeLogger([
logger1, logger2, logger3, logger4,
logger5, logger6, logger7, logger8
]);
Empty Composite
An empty CompositeLogger is extremely efficient (no-op):
// Negligible performance impact
const logger = new CompositeLogger();
logger.info('message'); // Does nothing
Example: Complete Setup
import {
CompositeLogger,
ConsoleLogger,
FileLogger,
MigrationScriptExecutor
} from '@migration-script-runner/core';
// Create loggers based on environment
const createLogger = (): CompositeLogger => {
const loggers = [];
// Always log to console in development
if (process.env.NODE_ENV !== 'production') {
loggers.push(new ConsoleLogger());
}
// Always log to file
loggers.push(new FileLogger({
logPath: process.env.LOG_PATH || '/var/log/migrations.log',
maxFileSize: 50 * 1024 * 1024, // 50MB
maxFiles: 10,
includeTimestamp: true
}));
// Add cloud logging in production
if (process.env.NODE_ENV === 'production') {
loggers.push(new CloudWatchLogger({
logGroupName: '/app/migrations',
logStreamName: process.env.INSTANCE_ID || 'default'
}));
}
return new CompositeLogger(loggers);
};
// Use the logger
const logger = createLogger();
const executor = new MigrationScriptExecutor({ handler, logger });
const result = await executor.migrate();
if (result.success) {
logger.info(`✅ Migrated ${result.executed.length} scripts`);
} else {
logger.error('❌ Migration failed', result.errors);
}