UNPKG

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.

461 lines 14.2 kB
/** * 構造化ログシステム(winston相当機能) */ import { existsSync } from 'node:fs'; import { writeFile, mkdir } from 'node:fs/promises'; import { dirname } from 'node:path'; /** * ログレベル */ export var LogLevel; (function (LogLevel) { LogLevel[LogLevel["ERROR"] = 0] = "ERROR"; LogLevel[LogLevel["WARN"] = 1] = "WARN"; LogLevel[LogLevel["INFO"] = 2] = "INFO"; LogLevel[LogLevel["HTTP"] = 3] = "HTTP"; LogLevel[LogLevel["VERBOSE"] = 4] = "VERBOSE"; LogLevel[LogLevel["DEBUG"] = 5] = "DEBUG"; LogLevel[LogLevel["SILLY"] = 6] = "SILLY"; })(LogLevel || (LogLevel = {})); /** * ログレベル名 */ export const LOG_LEVEL_NAMES = { [LogLevel.ERROR]: 'error', [LogLevel.WARN]: 'warn', [LogLevel.INFO]: 'info', [LogLevel.HTTP]: 'http', [LogLevel.VERBOSE]: 'verbose', [LogLevel.DEBUG]: 'debug', [LogLevel.SILLY]: 'silly', }; /** * ログトランスポート(出力先) */ export class LogTransport { level; format; constructor(level = LogLevel.INFO, format = {}) { this.level = level; this.format = format; } shouldLog(level) { return level <= this.level; } formatEntry(entry) { if (this.format.json) { return JSON.stringify(entry); } const timestamp = this.format.timestamp ? typeof this.format.timestamp === 'string' ? new Date().toISOString() : entry.timestamp : ''; const level = this.format.colorize ? this.colorizeLevel(entry.levelName) : entry.levelName.toUpperCase(); const message = entry.message; const meta = entry.meta && Object.keys(entry.meta).length > 0 ? this.format.prettyPrint ? `\n${JSON.stringify(entry.meta, null, 2)}` : ` ${JSON.stringify(entry.meta)}` : ''; const context = entry.context ? ` [${entry.context}]` : ''; const correlationId = entry.correlationId ? ` (${entry.correlationId})` : ''; const duration = entry.duration !== undefined ? ` +${entry.duration}ms` : ''; const parts = [ timestamp && `[${timestamp}]`, level, context, correlationId, message, duration, meta, ].filter(Boolean); return this.format.align ? parts.join(' ').replace(/\s+/g, ' ') : parts.join(' '); } colorizeLevel(levelName) { const colors = { error: '\x1b[31m', // Red warn: '\x1b[33m', // Yellow info: '\x1b[36m', // Cyan http: '\x1b[35m', // Magenta verbose: '\x1b[34m', // Blue debug: '\x1b[32m', // Green silly: '\x1b[37m', // White }; const reset = '\x1b[0m'; const color = colors[levelName.toLowerCase()] || ''; return `${color}${levelName.toUpperCase()}${reset}`; } } /** * コンソール出力トランスポート */ export class ConsoleTransport extends LogTransport { constructor(level = LogLevel.INFO, format = { colorize: true, timestamp: true }) { super(level, format); } write(entry) { const formatted = this.formatEntry(entry); if (entry.level === LogLevel.ERROR) { console.error(formatted); } else if (entry.level === LogLevel.WARN) { console.warn(formatted); } else { console.log(formatted); } } } /** * ファイル出力トランスポート */ export class FileTransport extends LogTransport { filename; maxSize; maxFiles; currentSize = 0; constructor(filename, level = LogLevel.INFO, format = { json: true, timestamp: true }, options = {}) { super(level, format); this.filename = filename; this.maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB this.maxFiles = options.maxFiles || 5; } async write(entry) { try { const formatted = this.formatEntry(entry) + '\n'; // ディレクトリが存在しない場合は作成 const dir = dirname(this.filename); if (!existsSync(dir)) { await mkdir(dir, { recursive: true }); } // ファイルサイズローテーション if (this.currentSize + formatted.length > this.maxSize) { await this.rotateFile(); this.currentSize = 0; } await writeFile(this.filename, formatted, { flag: 'a' }); this.currentSize += formatted.length; } catch (error) { console.error('Failed to write log file:', error); } } async rotateFile() { try { // ファイルを番号付きでローテーション for (let i = this.maxFiles - 1; i > 0; i--) { const oldFile = `${this.filename}.${i}`; const newFile = `${this.filename}.${i + 1}`; if (existsSync(oldFile)) { if (i === this.maxFiles - 1) { // 最古のファイルを削除 const { unlink } = await import('node:fs/promises'); await unlink(oldFile); } else { const { rename } = await import('node:fs/promises'); await rename(oldFile, newFile); } } } // 現在のファイルを .1 にリネーム if (existsSync(this.filename)) { const { rename } = await import('node:fs/promises'); await rename(this.filename, `${this.filename}.1`); } } catch (error) { console.error('Failed to rotate log file:', error); } } } /** * 構造化ロガー */ export class StructuredLogger { transports = []; defaultMeta = {}; contextStack = []; constructor(transports = []) { this.transports = transports; } /** * トランスポートを追加 */ addTransport(transport) { this.transports.push(transport); } /** * デフォルトメタデータを設定 */ setDefaultMeta(meta) { this.defaultMeta = { ...this.defaultMeta, ...meta }; } /** * コンテキストをプッシュ */ pushContext(context) { this.contextStack.push(context); } /** * コンテキストをポップ */ popContext() { return this.contextStack.pop(); } /** * 現在のコンテキストを取得 */ getCurrentContext() { return this.contextStack.length > 0 ? this.contextStack.join(':') : undefined; } /** * ログを出力 */ log(level, message, meta, options) { const entry = { timestamp: new Date().toISOString(), level, levelName: LOG_LEVEL_NAMES[level], message, meta: { ...this.defaultMeta, ...meta }, context: options?.context || this.getCurrentContext(), correlationId: options?.correlationId, duration: options?.duration, }; // エラー情報の追加 if (options?.error) { entry.error = { name: options.error.name, message: options.error.message, stack: options.error.stack, code: options.error.code, }; } // 各トランスポートにログを送信 for (const transport of this.transports) { if (transport.shouldLog(level)) { const result = transport.write(entry); if (result instanceof Promise) { result.catch((error) => { console.error('Transport write error:', error); }); } } } } /** * エラーログ */ error(message, meta, error) { this.log(LogLevel.ERROR, message, meta, { error }); } /** * 警告ログ */ warn(message, meta) { this.log(LogLevel.WARN, message, meta); } /** * 情報ログ */ info(message, meta) { this.log(LogLevel.INFO, message, meta); } /** * HTTPログ */ http(message, meta) { this.log(LogLevel.HTTP, message, meta); } /** * 詳細ログ */ verbose(message, meta) { this.log(LogLevel.VERBOSE, message, meta); } /** * デバッグログ */ debug(message, meta) { this.log(LogLevel.DEBUG, message, meta); } /** * 詳細デバッグログ */ silly(message, meta) { this.log(LogLevel.SILLY, message, meta); } /** * タイマー開始 */ startTimer(label) { const start = Date.now(); return () => { const duration = Date.now() - start; this.info(`Timer: ${label}`, { duration }); }; } /** * パフォーマンス測定 */ async profile(label, fn, level = LogLevel.INFO) { const start = Date.now(); this.log(level, `Starting: ${label}`); try { const result = await fn(); const duration = Date.now() - start; this.log(level, `Completed: ${label}`, { duration }); return result; } catch (error) { const duration = Date.now() - start; this.error(`Failed: ${label}`, { duration }, error instanceof Error ? error : new Error(String(error))); throw error; } } /** * 子ロガーを作成 */ child(meta, context) { const childLogger = new StructuredLogger(this.transports); childLogger.setDefaultMeta({ ...this.defaultMeta, ...meta }); if (context) { childLogger.contextStack = [...this.contextStack, context]; } else { childLogger.contextStack = [...this.contextStack]; } return childLogger; } /** * ログレベルを文字列から解析 */ static parseLogLevel(level) { const normalizedLevel = level.toLowerCase(); switch (normalizedLevel) { case 'error': return LogLevel.ERROR; case 'warn': case 'warning': return LogLevel.WARN; case 'info': return LogLevel.INFO; case 'http': return LogLevel.HTTP; case 'verbose': return LogLevel.VERBOSE; case 'debug': return LogLevel.DEBUG; case 'silly': return LogLevel.SILLY; default: return LogLevel.INFO; } } } /** * デフォルトロガーを作成 */ export function createLogger(options = {}) { const level = typeof options.level === 'string' ? StructuredLogger.parseLogLevel(options.level) : options.level || LogLevel.INFO; const transports = []; // コンソール出力 if (options.console !== false) { const consoleFormat = { colorize: process.stdout.isTTY, timestamp: true, align: true, }; transports.push(new ConsoleTransport(level, consoleFormat)); } // ファイル出力 if (options.file) { const fileFormat = { json: true, timestamp: true, }; transports.push(new FileTransport(options.file, level, fileFormat)); } const logger = new StructuredLogger(transports); if (options.meta) { logger.setDefaultMeta(options.meta); } return logger; } /** * グローバルロガー */ let globalLogger = null; /** * グローバルロガーを設定 */ export function setGlobalLogger(logger) { globalLogger = logger; } /** * グローバルロガーを取得 */ export function getLogger() { if (!globalLogger) { globalLogger = createLogger({ level: process.env.LOG_LEVEL || 'info', console: true, file: process.env.LOG_FILE, meta: { service: 'dnsweeper', version: process.env.npm_package_version || '1.0.0', }, }); } return globalLogger; } /** * ログミドルウェア(関数デコレーター) */ export function logMethod(level = LogLevel.DEBUG, includeArgs = false, includeResult = false) { return function (target, propertyName, descriptor) { if (!descriptor || typeof descriptor.value !== 'function') { return descriptor; } const method = descriptor.value; descriptor.value = async function (...args) { const logger = getLogger(); const className = target.constructor.name; const methodName = `${className}.${propertyName}`; const meta = {}; if (includeArgs) { meta.args = args; } logger.pushContext(methodName); const timer = logger.startTimer(methodName); try { logger.log(level, `Method called: ${methodName}`, meta); const result = await method.apply(this, args); if (includeResult) { logger.log(level, `Method completed: ${methodName}`, { result }); } else { logger.log(level, `Method completed: ${methodName}`); } timer(); return result; } catch (error) { logger.error(`Method failed: ${methodName}`, meta, error instanceof Error ? error : new Error(String(error))); timer(); throw error; } finally { logger.popContext(); } }; return descriptor; }; } //# sourceMappingURL=structured-logger.js.map