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
JavaScript
/**
* 構造化ログシステム(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