dnsweeper
Version:
Advanced CLI tool for DNS record risk analysis and cleanup. Features CSV import for Cloudflare/Route53, automated risk assessment, and parallel DNS validation.
588 lines (470 loc) • 19.4 kB
text/typescript
/**
* structured-logger.ts のユニットテスト
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { unlink, writeFile, mkdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import {
StructuredLogger,
LogLevel,
LOG_LEVEL_NAMES,
ConsoleTransport,
FileTransport,
createLogger,
getLogger,
setGlobalLogger,
logMethod,
type LogEntry,
type LogFormat
} from '../../src/lib/structured-logger.js';
describe('StructuredLogger', () => {
let logger: StructuredLogger;
let tempFiles: string[] = [];
beforeEach(() => {
logger = new StructuredLogger();
tempFiles = [];
});
afterEach(async () => {
// テスト用一時ファイルをクリーンアップ
for (const file of tempFiles) {
try {
if (existsSync(file)) {
await unlink(file);
}
} catch {
// ファイルが存在しない場合は無視
}
}
tempFiles = [];
vi.clearAllMocks();
});
const createTempFile = (): string => {
const filePath = join(tmpdir(), `test-log-${Date.now()}-${Math.random().toString(36).substring(2)}.log`);
tempFiles.push(filePath);
return filePath;
};
describe('LogLevel', () => {
it('ログレベルが正しく定義されている', () => {
expect(LogLevel.ERROR).toBe(0);
expect(LogLevel.WARN).toBe(1);
expect(LogLevel.INFO).toBe(2);
expect(LogLevel.DEBUG).toBe(5);
});
it('ログレベル名が正しく定義されている', () => {
expect(LOG_LEVEL_NAMES[LogLevel.ERROR]).toBe('error');
expect(LOG_LEVEL_NAMES[LogLevel.WARN]).toBe('warn');
expect(LOG_LEVEL_NAMES[LogLevel.INFO]).toBe('info');
expect(LOG_LEVEL_NAMES[LogLevel.DEBUG]).toBe('debug');
});
});
describe('ConsoleTransport', () => {
let consoleLogSpy: any;
let consoleErrorSpy: any;
let consoleWarnSpy: any;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
consoleWarnSpy.mockRestore();
});
it('コンソールに正常に出力', () => {
const transport = new ConsoleTransport(LogLevel.INFO);
const entry: LogEntry = {
timestamp: '2023-01-01T00:00:00.000Z',
level: LogLevel.INFO,
levelName: 'info',
message: 'Test message'
};
transport.write(entry);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('INFO')
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Test message')
);
});
it('エラーレベルはconsole.errorを使用', () => {
const transport = new ConsoleTransport(LogLevel.ERROR);
const entry: LogEntry = {
timestamp: '2023-01-01T00:00:00.000Z',
level: LogLevel.ERROR,
levelName: 'error',
message: 'Error message'
};
transport.write(entry);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('ERROR')
);
});
it('警告レベルはconsole.warnを使用', () => {
const transport = new ConsoleTransport(LogLevel.WARN);
const entry: LogEntry = {
timestamp: '2023-01-01T00:00:00.000Z',
level: LogLevel.WARN,
levelName: 'warn',
message: 'Warning message'
};
transport.write(entry);
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('WARN')
);
});
it('ログレベルフィルタリング', () => {
const transport = new ConsoleTransport(LogLevel.WARN);
expect(transport.shouldLog(LogLevel.ERROR)).toBe(true);
expect(transport.shouldLog(LogLevel.WARN)).toBe(true);
expect(transport.shouldLog(LogLevel.INFO)).toBe(false);
expect(transport.shouldLog(LogLevel.DEBUG)).toBe(false);
});
it('JSONフォーマット出力', () => {
const transport = new ConsoleTransport(LogLevel.INFO, { json: true });
const entry: LogEntry = {
timestamp: '2023-01-01T00:00:00.000Z',
level: LogLevel.INFO,
levelName: 'info',
message: 'Test message',
meta: { key: 'value' }
};
transport.write(entry);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('"message":"Test message"')
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('"key":"value"')
);
});
it('メタデータを含む出力', () => {
const transport = new ConsoleTransport(LogLevel.INFO);
const entry: LogEntry = {
timestamp: '2023-01-01T00:00:00.000Z',
level: LogLevel.INFO,
levelName: 'info',
message: 'Test message',
meta: { userId: 123, action: 'login' }
};
transport.write(entry);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('{"userId":123,"action":"login"}')
);
});
});
describe('FileTransport', () => {
it('ファイルに正常に出力', async () => {
const logFile = createTempFile();
const transport = new FileTransport(logFile, LogLevel.INFO);
const entry: LogEntry = {
timestamp: '2023-01-01T00:00:00.000Z',
level: LogLevel.INFO,
levelName: 'info',
message: 'Test message'
};
await transport.write(entry);
expect(existsSync(logFile)).toBe(true);
const content = await import('node:fs/promises').then(fs => fs.readFile(logFile, 'utf-8'));
expect(content).toContain('"message":"Test message"');
});
it('ディレクトリが存在しない場合は作成', async () => {
const logDir = join(tmpdir(), `test-log-dir-${Date.now()}`);
const logFile = join(logDir, 'test.log');
tempFiles.push(logFile);
const transport = new FileTransport(logFile, LogLevel.INFO);
const entry: LogEntry = {
timestamp: '2023-01-01T00:00:00.000Z',
level: LogLevel.INFO,
levelName: 'info',
message: 'Test message'
};
await transport.write(entry);
expect(existsSync(logFile)).toBe(true);
});
it('ファイルローテーション', async () => {
const logFile = createTempFile();
const transport = new FileTransport(logFile, LogLevel.INFO, { json: true }, {
maxSize: 100, // 100バイトで小さく設定
maxFiles: 3
});
// 複数のログエントリを書き込んでローテーションを発生させる
for (let i = 0; i < 10; i++) {
const entry: LogEntry = {
timestamp: '2023-01-01T00:00:00.000Z',
level: LogLevel.INFO,
levelName: 'info',
message: `Test message ${i} with some additional content to exceed the size limit`
};
await transport.write(entry);
}
// ローテーションファイルが作成されることを確認
expect(existsSync(logFile)).toBe(true);
expect(existsSync(`${logFile}.1`)).toBe(true);
});
});
describe('StructuredLogger', () => {
it('トランスポートを追加', () => {
const transport = new ConsoleTransport(LogLevel.INFO);
logger.addTransport(transport);
// プライベートプロパティなので、間接的にテスト
expect(() => logger.info('test')).not.toThrow();
});
it('デフォルトメタデータを設定', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.INFO, { json: true });
logger.addTransport(transport);
logger.setDefaultMeta({ service: 'test', version: '1.0.0' });
logger.info('Test message');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('"service":"test"')
);
consoleLogSpy.mockRestore();
});
it('コンテキストスタック管理', () => {
logger.pushContext('module1');
logger.pushContext('function1');
expect(logger.getCurrentContext()).toBe('module1:function1');
expect(logger.popContext()).toBe('function1');
expect(logger.getCurrentContext()).toBe('module1');
expect(logger.popContext()).toBe('module1');
expect(logger.getCurrentContext()).toBeUndefined();
});
it('各レベルのログメソッド', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.SILLY);
logger.addTransport(transport);
logger.error('Error message');
logger.warn('Warning message');
logger.info('Info message');
logger.debug('Debug message');
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('ERROR'));
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('WARN'));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('INFO'));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('DEBUG'));
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
consoleWarnSpy.mockRestore();
});
it('エラーオブジェクトを含むログ', () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.ERROR, { json: true });
logger.addTransport(transport);
const error = new Error('Test error');
error.stack = 'Error: Test error\n at test';
logger.error('Error occurred', { context: 'test' }, error);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('"name":"Error"')
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('"message":"Test error"')
);
consoleErrorSpy.mockRestore();
});
it('タイマー機能', async () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.INFO, { json: true });
logger.addTransport(transport);
const timer = logger.startTimer('test-operation');
// 少し待機
await new Promise(resolve => setTimeout(resolve, 10));
timer();
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('"duration"')
);
consoleLogSpy.mockRestore();
});
it('プロファイル機能(成功)', async () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.INFO);
logger.addTransport(transport);
const result = await logger.profile('test-function', async () => {
await new Promise(resolve => setTimeout(resolve, 10));
return 'success';
});
expect(result).toBe('success');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Starting: test-function')
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Completed: test-function')
);
consoleLogSpy.mockRestore();
});
it('プロファイル機能(エラー)', async () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.INFO);
logger.addTransport(transport);
await expect(logger.profile('test-function', async () => {
throw new Error('Test error');
})).rejects.toThrow('Test error');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Starting: test-function')
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed: test-function')
);
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
it('子ロガーの作成', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.INFO, { json: true });
logger.addTransport(transport);
logger.setDefaultMeta({ service: 'parent' });
const childLogger = logger.child({ module: 'child' }, 'child-context');
childLogger.info('Child message');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('"service":"parent"')
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('"module":"child"')
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('child-context')
);
consoleLogSpy.mockRestore();
});
});
describe('ログレベル解析', () => {
it('文字列からログレベルを正しく解析', () => {
expect(StructuredLogger.parseLogLevel('error')).toBe(LogLevel.ERROR);
expect(StructuredLogger.parseLogLevel('WARN')).toBe(LogLevel.WARN);
expect(StructuredLogger.parseLogLevel('info')).toBe(LogLevel.INFO);
expect(StructuredLogger.parseLogLevel('debug')).toBe(LogLevel.DEBUG);
expect(StructuredLogger.parseLogLevel('unknown')).toBe(LogLevel.INFO);
});
it('warning の別名をサポート', () => {
expect(StructuredLogger.parseLogLevel('warning')).toBe(LogLevel.WARN);
});
});
describe('createLogger', () => {
it('デフォルト設定でロガーを作成', () => {
const logger = createLogger();
expect(logger).toBeInstanceOf(StructuredLogger);
});
it('文字列レベルでロガーを作成', () => {
const logger = createLogger({ level: 'debug' });
expect(logger).toBeInstanceOf(StructuredLogger);
});
it('ファイル出力を含むロガーを作成', () => {
const logFile = createTempFile();
const logger = createLogger({
level: LogLevel.INFO,
file: logFile,
meta: { service: 'test' }
});
expect(logger).toBeInstanceOf(StructuredLogger);
});
it('コンソール出力を無効化', () => {
const logger = createLogger({ console: false });
expect(logger).toBeInstanceOf(StructuredLogger);
});
});
describe('グローバルロガー', () => {
afterEach(() => {
// グローバルロガーをリセット
setGlobalLogger(createLogger());
});
it('グローバルロガーを設定・取得', () => {
const customLogger = createLogger({ level: 'debug' });
setGlobalLogger(customLogger);
const retrieved = getLogger();
expect(retrieved).toBe(customLogger);
});
it('グローバルロガーがない場合はデフォルトを作成', () => {
// グローバルロガーをクリア(privateなので間接的にテスト)
const logger = getLogger();
expect(logger).toBeInstanceOf(StructuredLogger);
});
});
describe('logMethodデコレーター', () => {
class TestClass {
(LogLevel.DEBUG, true, true)
async testMethod(param1: string, param2: number): Promise<string> {
return `${param1}-${param2}`;
}
(LogLevel.DEBUG)
async errorMethod(): Promise<void> {
throw new Error('Test error');
}
}
it('メソッド呼び出しをログに記録', async () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.DEBUG);
const logger = createLogger();
logger.addTransport(transport);
setGlobalLogger(logger);
const instance = new TestClass();
const result = await instance.testMethod('test', 123);
expect(result).toBe('test-123');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Method called: TestClass.testMethod')
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Method completed: TestClass.testMethod')
);
consoleLogSpy.mockRestore();
});
it('メソッドエラーをログに記録', async () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.DEBUG);
const logger = createLogger();
logger.addTransport(transport);
setGlobalLogger(logger);
const instance = new TestClass();
await expect(instance.errorMethod()).rejects.toThrow('Test error');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Method failed: TestClass.errorMethod')
);
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
});
describe('エラーハンドリング', () => {
it('無効なトランスポートでもエラーにならない', () => {
expect(() => {
logger.addTransport(null as any);
}).not.toThrow();
});
it('空のメッセージでもログを記録', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.INFO);
logger.addTransport(transport);
logger.info('');
expect(consoleLogSpy).toHaveBeenCalled();
consoleLogSpy.mockRestore();
});
it('大きなメタデータオブジェクトを処理', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.INFO, { json: true });
logger.addTransport(transport);
const largeMeta = {
data: Array.from({ length: 1000 }, (_, i) => ({ id: i, value: `value-${i}` }))
};
logger.info('Large meta test', largeMeta);
expect(consoleLogSpy).toHaveBeenCalled();
consoleLogSpy.mockRestore();
});
});
describe('パフォーマンス', () => {
it('大量のログを高速で処理', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const transport = new ConsoleTransport(LogLevel.INFO);
logger.addTransport(transport);
const start = Date.now();
for (let i = 0; i < 1000; i++) {
logger.info(`Message ${i}`, { index: i });
}
const duration = Date.now() - start;
expect(duration).toBeLessThan(1000); // 1秒以内
expect(consoleLogSpy).toHaveBeenCalledTimes(1000);
consoleLogSpy.mockRestore();
});
});
});