Custom Logging

MSR provides flexible logging through the ILogger interface, allowing you to customize or suppress all output from the migration system.

Table of Contents

  1. Overview
  2. The ILogger Interface
  3. Built-in Logger Implementations
    1. Quick Overview
    2. ConsoleLogger (Default)
    3. SilentLogger
    4. FileLogger
  4. Creating Custom Loggers
    1. File Logger Example
    2. Cloud Logger Example
    3. Combined Logger Example
  5. Use Cases
    1. Development
    2. Testing
    3. Production
    4. CI/CD
  6. Which Services Accept Logger?
  7. Best Practices

Overview

By default, MSR uses ConsoleLogger which outputs all messages to the console using standard console.* methods. However, you can customize this behavior by:

  • Using SilentLogger to suppress all output (useful for testing or library usage)
  • Creating custom logger implementations for files, cloud services, or other destinations

All services that produce output accept an optional ILogger parameter in their constructors.


The ILogger Interface

The ILogger interface defines five logging methods:

interface ILogger {
  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;
}

Built-in Logger Implementations

MSR provides three built-in logger implementations. For detailed documentation on each, see the Logger Documentation.

Quick Overview

  • ConsoleLogger - Default logger that outputs to console. Perfect for development and debugging.
  • SilentLogger - Suppresses all output. Ideal for testing and silent operations.
  • FileLogger - Writes to files with automatic rotation. Best for production environments.

ConsoleLogger (Default)

import { MigrationScriptExecutor, ConsoleLogger } from '@migration-script-runner/core';

const executor = new MigrationScriptExecutor({ handler, 
    logger: new ConsoleLogger()
});

→ Full ConsoleLogger Documentation

SilentLogger

import { MigrationScriptExecutor, SilentLogger } from '@migration-script-runner/core';

const executor = new MigrationScriptExecutor({ handler, 
    logger: new SilentLogger()
});

→ Full SilentLogger Documentation

FileLogger

import { MigrationService, FileLogger } from '@migration-script-runner/core';

const logger = new FileLogger({
    logPath: '/var/log/migrations.log',
    maxFileSize: 10 * 1024 * 1024,  // 10MB
    maxFiles: 10
});

const service = new MigrationService(logger);

→ Full FileLogger Documentation


Creating Custom Loggers

File Logger Example

Log all migration activity to a file:

import { ILogger } from '@migration-script-runner/core';
import fs from 'fs';

class FileLogger implements ILogger {
  constructor(private logPath: string) {}

  private writeToFile(level: string, message: string, ...args: unknown[]): void {
    const timestamp = new Date().toISOString();
    const logMessage = `[${timestamp}] [${level}] ${message} ${args.join(' ')}\n`;
    fs.appendFileSync(this.logPath, logMessage);
  }

  info(message: string, ...args: unknown[]): void {
    this.writeToFile('INFO', message, ...args);
  }

  warn(message: string, ...args: unknown[]): void {
    this.writeToFile('WARN', message, ...args);
  }

  error(message: string, ...args: unknown[]): void {
    this.writeToFile('ERROR', message, ...args);
  }

  debug(message: string, ...args: unknown[]): void {
    this.writeToFile('DEBUG', message, ...args);
  }

  log(message: string, ...args: unknown[]): void {
    this.writeToFile('LOG', message, ...args);
  }
}

// Use the file logger
const executor = new MigrationScriptExecutor({ handler, 
  logger: new FileLogger('/var/log/migrations.log')
});
await executor.migrate();

Output in /var/log/migrations.log:

[2025-01-22T01:30:45.123Z] [INFO] Preparing backup...
[2025-01-22T01:30:45.456Z] [INFO] Backup prepared successfully: /backups/backup.bkp
[2025-01-22T01:30:45.789Z] [INFO] Processing...
[2025-01-22T01:30:46.012Z] [LOG] V202501220100_test.ts: processing...
[2025-01-22T01:30:46.345Z] [INFO] Migration finished successfully!

Cloud Logger Example

Send logs to a cloud service like AWS CloudWatch or Datadog:

import { ILogger } from '@migration-script-runner/core';
import { CloudWatchLogs } from '@aws-sdk/client-cloudwatch-logs';

class CloudWatchLogger implements ILogger {
  private client: CloudWatchLogs;
  private logGroupName: string;
  private logStreamName: string;

  constructor(logGroupName: string, logStreamName: string) {
    this.client = new CloudWatchLogs({ region: 'us-east-1' });
    this.logGroupName = logGroupName;
    this.logStreamName = logStreamName;
  }

  private async sendLog(level: string, message: string, ...args: unknown[]): Promise<void> {
    await this.client.putLogEvents({
      logGroupName: this.logGroupName,
      logStreamName: this.logStreamName,
      logEvents: [{
        timestamp: Date.now(),
        message: `[${level}] ${message} ${args.join(' ')}`
      }]
    });
  }

  info(message: string, ...args: unknown[]): void {
    this.sendLog('INFO', message, ...args).catch(console.error);
  }

  warn(message: string, ...args: unknown[]): void {
    this.sendLog('WARN', message, ...args).catch(console.error);
  }

  error(message: string, ...args: unknown[]): void {
    this.sendLog('ERROR', message, ...args).catch(console.error);
  }

  debug(message: string, ...args: unknown[]): void {
    this.sendLog('DEBUG', message, ...args).catch(console.error);
  }

  log(message: string, ...args: unknown[]): void {
    this.sendLog('LOG', message, ...args).catch(console.error);
  }
}

// Use in production
const executor = new MigrationScriptExecutor({ handler, 
  logger: new CloudWatchLogger('/aws/migrations', 'production')
});

Combined Logger Example

Log to both console and file simultaneously:

class CombinedLogger implements ILogger {
  constructor(
    private loggers: ILogger[]
  ) {}

  info(message: string, ...args: unknown[]): void {
    this.loggers.forEach(logger => logger.info(message, ...args));
  }

  warn(message: string, ...args: unknown[]): void {
    this.loggers.forEach(logger => logger.warn(message, ...args));
  }

  error(message: string, ...args: unknown[]): void {
    this.loggers.forEach(logger => logger.error(message, ...args));
  }

  debug(message: string, ...args: unknown[]): void {
    this.loggers.forEach(logger => logger.debug(message, ...args));
  }

  log(message: string, ...args: unknown[]): void {
    this.loggers.forEach(logger => logger.log(message, ...args));
  }
}

// Log to both console and file
const executor = new MigrationScriptExecutor({ handler, 
  logger: new CombinedLogger([
    new ConsoleLogger(),
    new FileLogger('/var/log/migrations.log')
  ])
});

Use Cases

Development

Use the default ConsoleLogger for immediate visual feedback:

const config = new Config();
const executor = new MigrationScriptExecutor({ handler }, config);
// or explicitly: new MigrationScriptExecutor({ handler,  logger: new ConsoleLogger() });

Testing

Use SilentLogger to keep test output clean:

import { SilentLogger } from '@migration-script-runner/core';

describe('Migration Tests', () => {
  it('should execute migrations successfully', async () => {
    const executor = new MigrationScriptExecutor({ handler, 
      logger: new SilentLogger()
    });
    const result = await executor.migrate();
    expect(result.success).toBe(true);
  });
});

Production

Use custom loggers for structured logging and monitoring:

// Send to cloud service for centralized logging
const executor = new MigrationScriptExecutor({ handler, 
  logger: new CloudWatchLogger('/prod/migrations', process.env.HOSTNAME)
});

// Or use combined logging
const executor = new MigrationScriptExecutor({ handler, 
  logger: new CombinedLogger([
    new FileLogger('/var/log/migrations.log'),
    new CloudWatchLogger('/prod/migrations', process.env.HOSTNAME)
  ])
});

CI/CD

Format logs for your CI system:

class CILogger implements ILogger {
  info(message: string, ...args: unknown[]): void {
    console.log(`::notice::${message}`, ...args);
  }

  warn(message: string, ...args: unknown[]): void {
    console.log(`::warning::${message}`, ...args);
  }

  error(message: string, ...args: unknown[]): void {
    console.log(`::error::${message}`, ...args);
  }

  debug(message: string, ...args: unknown[]): void {
    console.log(`::debug::${message}`, ...args);
  }

  log(message: string, ...args: unknown[]): void {
    console.log(message, ...args);
  }
}

Which Services Accept Logger?

The following services accept an optional ILogger parameter:

  • MigrationScriptExecutor - Main migration executor
  • BackupService - Backup creation and restoration
  • MigrationService - Migration script discovery
  • MigrationRenderer - Migration output rendering (delegates to render strategies)
  • AsciiTableRenderStrategy - ASCII table rendering
  • JsonRenderStrategy - JSON output rendering
  • Utils.parseRunnable() - Migration script parsing

Example:

import {
  MigrationScriptExecutor,
  BackupService,
  MigrationService,
  SilentLogger
} from '@migration-script-runner/core';

const logger = new SilentLogger();

// Pass logger through dependency injection
const executor = new MigrationScriptExecutor({ handler,  logger });

// Other services accept logger directly
const backupService = new BackupService(handler, logger);
const migrationService = new MigrationService(logger);

Best Practices

  1. Consistent Logging - Use the same logger instance across all services for consistent output
  2. Error Handling - Always handle async logging errors (especially for cloud loggers)
  3. Performance - Avoid synchronous file I/O in production; use async logging or queues
  4. Structured Logs - Include timestamps, log levels, and context in your custom loggers
  5. Testing - Always use SilentLogger in test suites to keep output clean
  6. Production - Use cloud-based logging for centralized monitoring and alerting