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.

511 lines (449 loc) 12.6 kB
/** * DNSweeper 監視・メトリクス収集システム * * アプリケーションのパフォーマンス監視とメトリクス収集機能 */ import { EventEmitter } from 'events'; import os from 'os'; import { performance } from 'perf_hooks'; export interface Metric { name: string; value: number; timestamp: Date; tags?: Record<string, string>; unit?: string; } export interface Counter { name: string; value: number; tags?: Record<string, string>; } export interface Timer { name: string; startTime: number; tags?: Record<string, string>; } export interface HealthCheckResult { status: 'healthy' | 'degraded' | 'unhealthy'; checks: { name: string; status: 'pass' | 'warn' | 'fail'; message?: string; duration?: number; }[]; timestamp: Date; } export interface SystemMetrics { cpu: { usage: number; loadAverage: number[]; }; memory: { total: number; used: number; free: number; percentage: number; }; process: { pid: number; uptime: number; memoryUsage: NodeJS.MemoryUsage; }; timestamp: Date; } export interface MetricsOptions { flushInterval?: number; // ミリ秒 maxMetrics?: number; enableSystemMetrics?: boolean; systemMetricsInterval?: number; // ミリ秒 } export class MetricsCollector extends EventEmitter { private metrics: Metric[] = []; private counters: Map<string, Counter> = new Map(); private timers: Map<string, Timer> = new Map(); private gauges: Map<string, Metric> = new Map(); private options: Required<MetricsOptions>; private flushTimer?: NodeJS.Timeout; private systemMetricsTimer?: NodeJS.Timeout; private startTime: number = Date.now(); constructor(options: MetricsOptions = {}) { super(); this.options = { flushInterval: options.flushInterval || 60000, // 1分 maxMetrics: options.maxMetrics || 10000, enableSystemMetrics: options.enableSystemMetrics ?? true, systemMetricsInterval: options.systemMetricsInterval || 30000, // 30秒 }; this.startFlushTimer(); if (this.options.enableSystemMetrics) { this.startSystemMetricsCollection(); } } /** * カウンターを増加 */ increment(name: string, value: number = 1, tags?: Record<string, string>): void { const key = this.getCounterKey(name, tags); const counter = this.counters.get(key); if (counter) { counter.value += value; } else { this.counters.set(key, { name, value, tags }); } // メトリクスイベントを発行 this.emit('metric', { type: 'counter', name, value, tags, timestamp: new Date(), }); } /** * カウンターを減少 */ decrement(name: string, value: number = 1, tags?: Record<string, string>): void { this.increment(name, -value, tags); } /** * ゲージ値を設定 */ gauge(name: string, value: number, tags?: Record<string, string>, unit?: string): void { const key = this.getGaugeKey(name, tags); const metric: Metric = { name, value, timestamp: new Date(), tags, unit, }; this.gauges.set(key, metric); this.addMetric(metric); // メトリクスイベントを発行 this.emit('metric', { type: 'gauge', ...metric, }); } /** * タイマーを開始 */ startTimer(name: string, tags?: Record<string, string>): string { const key = this.getTimerKey(name, tags); const timer: Timer = { name, startTime: performance.now(), tags, }; this.timers.set(key, timer); return key; } /** * タイマーを停止して計測時間を記録 */ stopTimer(key: string): number | null { const timer = this.timers.get(key); if (!timer) { return null; } const duration = performance.now() - timer.startTime; this.timers.delete(key); // ヒストグラムメトリクスとして記録 const metric: Metric = { name: `${timer.name}.duration`, value: duration, timestamp: new Date(), tags: timer.tags, unit: 'ms', }; this.addMetric(metric); // メトリクスイベントを発行 this.emit('metric', { type: 'histogram', ...metric, }); return duration; } /** * 時間計測デコレーター用のユーティリティ */ async measureAsync<T>( name: string, fn: () => Promise<T>, tags?: Record<string, string>, ): Promise<T> { const timerKey = this.startTimer(name, tags); try { const result = await fn(); this.stopTimer(timerKey); this.increment(`${name}.success`, 1, tags); return result; } catch (error) { this.stopTimer(timerKey); this.increment(`${name}.error`, 1, tags); throw error; } } /** * 同期関数の時間計測 */ measure<T>(name: string, fn: () => T, tags?: Record<string, string>): T { const timerKey = this.startTimer(name, tags); try { const result = fn(); this.stopTimer(timerKey); this.increment(`${name}.success`, 1, tags); return result; } catch (error) { this.stopTimer(timerKey); this.increment(`${name}.error`, 1, tags); throw error; } } /** * ヘルスチェック実行 */ async healthCheck( checks: Array<{ name: string; check: () => Promise<boolean | { pass: boolean; message?: string }>; }>, ): Promise<HealthCheckResult> { const results = await Promise.all( checks.map(async ({ name, check }) => { const startTime = performance.now(); try { const result = await check(); const duration = performance.now() - startTime; if (typeof result === 'boolean') { return { name, status: result ? ('pass' as const) : ('fail' as const), duration, }; } else { return { name, status: result.pass ? ('pass' as const) : ('fail' as const), message: result.message, duration, }; } } catch (error) { const duration = performance.now() - startTime; return { name, status: 'fail' as const, message: error instanceof Error ? error.message : 'Unknown error', duration, }; } }), ); const failedChecks = results.filter((r) => r.status === 'fail').length; const warnChecks = results.filter((r) => 'status' in r && r.status === ('warn' as any)).length; let status: HealthCheckResult['status']; if (failedChecks > 0) { status = 'unhealthy'; } else if (warnChecks > 0) { status = 'degraded'; } else { status = 'healthy'; } const result: HealthCheckResult = { status, checks: results, timestamp: new Date(), }; this.emit('healthcheck', result); return result; } /** * システムメトリクスを収集 */ private collectSystemMetrics(): SystemMetrics { const cpus = os.cpus(); const totalMemory = os.totalmem(); const freeMemory = os.freemem(); const usedMemory = totalMemory - freeMemory; // CPU使用率計算 let totalIdle = 0; let totalTick = 0; cpus.forEach((cpu) => { for (const type in cpu.times) { totalTick += cpu.times[type as keyof typeof cpu.times]; } totalIdle += cpu.times.idle; }); const cpuUsage = 100 - (100 * totalIdle) / totalTick; const metrics: SystemMetrics = { cpu: { usage: cpuUsage, loadAverage: os.loadavg(), }, memory: { total: totalMemory, used: usedMemory, free: freeMemory, percentage: (usedMemory / totalMemory) * 100, }, process: { pid: process.pid, uptime: (Date.now() - this.startTime) / 1000, // 秒 memoryUsage: process.memoryUsage(), }, timestamp: new Date(), }; // ゲージとして記録 this.gauge('system.cpu.usage', metrics.cpu.usage, { type: 'percentage' }, '%'); this.gauge('system.memory.used', metrics.memory.used, { type: 'bytes' }, 'bytes'); this.gauge('system.memory.percentage', metrics.memory.percentage, { type: 'percentage' }, '%'); this.gauge( 'process.memory.heapUsed', metrics.process.memoryUsage.heapUsed, { type: 'bytes' }, 'bytes', ); this.gauge('process.uptime', metrics.process.uptime, { type: 'seconds' }, 's'); return metrics; } /** * メトリクスを取得 */ getMetrics(): Metric[] { return [...this.metrics]; } /** * カウンターを取得 */ getCounters(): Counter[] { return Array.from(this.counters.values()); } /** * ゲージを取得 */ getGauges(): Metric[] { return Array.from(this.gauges.values()); } /** * 統計サマリーを取得 */ getSummary(): { metrics: number; counters: number; gauges: number; activeTimers: number; uptime: number; } { return { metrics: this.metrics.length, counters: this.counters.size, gauges: this.gauges.size, activeTimers: this.timers.size, uptime: (Date.now() - this.startTime) / 1000, }; } /** * メトリクスをリセット */ reset(): void { this.metrics = []; this.counters.clear(); this.gauges.clear(); // タイマーは保持(実行中の可能性があるため) } /** * すべてをクリア */ clear(): void { this.reset(); this.timers.clear(); } /** * リソースのクリーンアップ */ destroy(): void { if (this.flushTimer) { clearInterval(this.flushTimer); } if (this.systemMetricsTimer) { clearInterval(this.systemMetricsTimer); } this.clear(); this.removeAllListeners(); } private addMetric(metric: Metric): void { this.metrics.push(metric); // 最大数を超えたら古いメトリクスを削除 if (this.metrics.length > this.options.maxMetrics) { this.metrics = this.metrics.slice(-this.options.maxMetrics); } } private getCounterKey(name: string, tags?: Record<string, string>): string { return tags ? `${name}:${JSON.stringify(tags)}` : name; } private getGaugeKey(name: string, tags?: Record<string, string>): string { return this.getCounterKey(name, tags); } private getTimerKey(name: string, tags?: Record<string, string>): string { return `${this.getCounterKey(name, tags)}:${Date.now()}:${Math.random()}`; } private startFlushTimer(): void { this.flushTimer = setInterval(() => { this.flush(); }, this.options.flushInterval); } private startSystemMetricsCollection(): void { // 初回収集 this.collectSystemMetrics(); this.systemMetricsTimer = setInterval(() => { this.collectSystemMetrics(); }, this.options.systemMetricsInterval); } private flush(): void { const metrics = this.getMetrics(); const counters = this.getCounters(); const gauges = this.getGauges(); if (metrics.length > 0 || counters.length > 0 || gauges.length > 0) { this.emit('flush', { metrics, counters, gauges, timestamp: new Date(), }); } // メトリクスをクリア(カウンターとゲージは保持) this.metrics = []; } } // シングルトンインスタンス let defaultCollector: MetricsCollector | null = null; /** * デフォルトのメトリクスコレクターを取得 */ export function getMetricsCollector(options?: MetricsOptions): MetricsCollector { if (!defaultCollector) { defaultCollector = new MetricsCollector(options); } return defaultCollector; } /** * メトリクスデコレーター */ export function metric(name?: string) { return function (target: any, propertyName: string, descriptor: PropertyDescriptor) { const metricName = name || `${target.constructor.name}.${propertyName}`; const originalMethod = descriptor.value; const collector = getMetricsCollector(); if (originalMethod.constructor.name === 'AsyncFunction') { descriptor.value = async function (...args: any[]) { return collector.measureAsync(metricName, () => originalMethod.apply(this, args)); }; } else { descriptor.value = function (...args: any[]) { return collector.measure(metricName, () => originalMethod.apply(this, args)); }; } return descriptor; }; }