UNPKG

hook-engine

Version:

Production-grade webhook engine with comprehensive adapter support, security, reliability, structured logging, and CLI tools.

422 lines (421 loc) 14.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.logger = exports.StructuredLogger = void 0; exports.createLogger = createLogger; const events_1 = require("events"); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const fs_1 = require("fs"); const uuid_1 = require("uuid"); class StructuredLogger extends events_1.EventEmitter { constructor(config = {}, context = {}) { super(); this.outputs = new Map(); this.logBuffer = []; this.config = { level: 'info', format: 'json', outputs: [{ type: 'console', config: { colorize: true, timestamp: true, level: 'info' } }], enableColors: true, enableTimestamps: true, enableStackTrace: true, maxFileSize: 10 * 1024 * 1024, // 10MB maxFiles: 5, rotateDaily: false, ...config }; this.context = context; this.initializeOutputs(); this.startFlushTimer(); } async initializeOutputs() { for (const output of this.config.outputs) { try { const transport = await this.createTransport(output); this.outputs.set(output.type, transport); } catch (error) { console.error(`Failed to initialize ${output.type} transport:`, error); } } } async createTransport(output) { switch (output.type) { case 'console': return new ConsoleTransport(output.config); case 'file': return new FileTransport(output.config); case 'http': return new HttpTransport(output.config); case 'database': return new DatabaseTransport(output.config); default: throw new Error(`Unsupported transport type: ${output.type}`); } } startFlushTimer() { this.flushTimer = setInterval(() => { this.flush().catch(console.error); }, 5000); // Flush every 5 seconds } shouldLog(level) { const levels = ['debug', 'info', 'warn', 'error', 'fatal']; const configLevelIndex = levels.indexOf(this.config.level); const messageLevelIndex = levels.indexOf(level); return messageLevelIndex >= configLevelIndex; } generateEventId() { return (0, uuid_1.v4)(); } createBaseEntry(level, message, context, metadata) { return { timestamp: new Date().toISOString(), level, eventId: this.generateEventId(), message, source: context?.source || this.context.source || 'hook-engine', context: { ...this.context, ...context }, metadata: metadata || {}, pid: process.pid, hostname: require('os').hostname() }; } debug(message, context, metadata) { if (!this.shouldLog('debug')) return; const entry = this.createBaseEntry('debug', message, context, metadata); this.writeLog(entry); } info(message, context, metadata) { if (!this.shouldLog('info')) return; const entry = this.createBaseEntry('info', message, context, metadata); this.writeLog(entry); } warn(message, context, metadata) { if (!this.shouldLog('warn')) return; const entry = this.createBaseEntry('warn', message, context, metadata); this.writeLog(entry); } error(message, error, context, metadata) { if (!this.shouldLog('error')) return; const entry = { timestamp: new Date().toISOString(), level: 'error', eventId: this.generateEventId(), error: { name: error?.name || 'Error', message: error?.message || message, stack: this.config.enableStackTrace ? error?.stack : undefined, code: error?.code }, context: { operation: context?.operation || this.context.operation || 'unknown', source: context?.source || this.context.source || 'hook-engine', webhookId: context?.webhookId || this.context.webhookId, adapterId: context?.adapterId || this.context.adapterId, retryAttempt: context?.custom?.retryAttempt }, metadata: { ...metadata, originalMessage: message } }; this.writeLog(entry); } fatal(message, error, context, metadata) { if (!this.shouldLog('fatal')) return; const entry = { timestamp: new Date().toISOString(), level: 'fatal', eventId: this.generateEventId(), error: { name: error?.name || 'FatalError', message: error?.message || message, stack: this.config.enableStackTrace ? error?.stack : undefined, code: error?.code }, context: { operation: context?.operation || this.context.operation || 'unknown', source: context?.source || this.context.source || 'hook-engine', webhookId: context?.webhookId || this.context.webhookId, adapterId: context?.adapterId || this.context.adapterId, retryAttempt: context?.custom?.retryAttempt }, metadata: { ...metadata, originalMessage: message } }; this.writeLog(entry); // Emit fatal event for immediate handling this.emit('fatal', entry); } webhook(entry) { if (!this.shouldLog(entry.level)) return; const webhookEntry = { timestamp: new Date().toISOString(), eventId: this.generateEventId(), ...entry }; this.writeLog(webhookEntry); this.emit('webhook', webhookEntry); } security(entry) { if (!this.shouldLog(entry.level)) return; const securityEntry = { timestamp: new Date().toISOString(), eventId: this.generateEventId(), ...entry }; this.writeLog(securityEntry); this.emit('security', securityEntry); } performance(entry) { if (!this.shouldLog(entry.level)) return; const perfEntry = { timestamp: new Date().toISOString(), eventId: this.generateEventId(), ...entry }; this.writeLog(perfEntry); this.emit('performance', perfEntry); } child(context) { return new StructuredLogger(this.config, { ...this.context, ...context }); } setLevel(level) { this.config.level = level; } async writeLog(entry) { // Add to buffer for batch processing this.logBuffer.push(entry); // Write to all configured outputs const promises = Array.from(this.outputs.values()).map(transport => transport.write(entry).catch(error => { console.error('Transport write error:', error); })); await Promise.allSettled(promises); } async flush() { if (this.logBuffer.length === 0) return; const promises = Array.from(this.outputs.values()).map(transport => transport.flush().catch(error => { console.error('Transport flush error:', error); })); await Promise.allSettled(promises); this.logBuffer = []; } async close() { if (this.flushTimer) { clearInterval(this.flushTimer); } await this.flush(); const promises = Array.from(this.outputs.values()).map(transport => transport.close().catch(error => { console.error('Transport close error:', error); })); await Promise.allSettled(promises); this.outputs.clear(); } } exports.StructuredLogger = StructuredLogger; class ConsoleTransport { constructor(config) { this.config = config; } async write(entry) { const formatted = this.formatEntry(entry); console.log(formatted); } formatEntry(entry) { if (this.config.colorize) { return this.colorizeEntry(entry); } return JSON.stringify(entry); } colorizeEntry(entry) { const colors = { debug: '\x1b[36m', // Cyan info: '\x1b[32m', // Green warn: '\x1b[33m', // Yellow error: '\x1b[31m', // Red fatal: '\x1b[35m', // Magenta reset: '\x1b[0m' }; const color = colors[entry.level] || colors.reset; const timestamp = entry.timestamp ? `[${entry.timestamp}] ` : ''; const level = `${color}${entry.level.toUpperCase()}${colors.reset}`; const message = entry.message || JSON.stringify(entry); return `${timestamp}${level}: ${message}`; } async flush() { // Console doesn't need flushing } async close() { // Console doesn't need closing } } class FileTransport { constructor(config) { this.writeBuffer = []; this.config = config; this.ensureLogDirectory(); } async ensureLogDirectory() { const dir = path_1.default.dirname(this.config.filename); if (!(0, fs_1.existsSync)(dir)) { await promises_1.default.mkdir(dir, { recursive: true }); } } async write(entry) { const formatted = JSON.stringify(entry) + '\n'; this.writeBuffer.push(formatted); // Write immediately for error and fatal levels if (entry.level === 'error' || entry.level === 'fatal') { await this.flush(); } } async flush() { if (this.writeBuffer.length === 0) return; const content = this.writeBuffer.join(''); await promises_1.default.appendFile(this.config.filename, content, 'utf8'); this.writeBuffer = []; // Check file size and rotate if needed await this.rotateIfNeeded(); } async rotateIfNeeded() { try { const stats = await promises_1.default.stat(this.config.filename); if (stats.size > this.config.maxSize) { await this.rotateFile(); } } catch (error) { // File doesn't exist yet, no rotation needed } } async rotateFile() { const ext = path_1.default.extname(this.config.filename); const base = this.config.filename.slice(0, -ext.length); // Rotate existing files for (let i = this.config.maxFiles - 1; i > 0; i--) { const oldFile = `${base}.${i}${ext}`; const newFile = `${base}.${i + 1}${ext}`; if ((0, fs_1.existsSync)(oldFile)) { await promises_1.default.rename(oldFile, newFile); } } // Move current file to .1 await promises_1.default.rename(this.config.filename, `${base}.1${ext}`); } async close() { await this.flush(); } } class HttpTransport { constructor(config) { this.buffer = []; this.config = config; } async write(entry) { this.buffer.push(entry); // Send immediately for critical entries if (entry.level === 'fatal' || entry.severity === 'critical') { await this.flush(); } } async flush() { if (this.buffer.length === 0) return; const payload = { logs: this.buffer, timestamp: new Date().toISOString(), source: 'hook-engine' }; try { // In a real implementation, use fetch or axios console.log(`Would send ${this.buffer.length} logs to ${this.config.url}`); this.buffer = []; } catch (error) { console.error('Failed to send logs via HTTP:', error); } } async close() { await this.flush(); } } class DatabaseTransport { constructor(config) { this.buffer = []; this.config = config; } async write(entry) { this.buffer.push(entry); if (this.buffer.length >= this.config.batchSize) { await this.flush(); } } async flush() { if (this.buffer.length === 0) return; try { // In a real implementation, insert into database console.log(`Would insert ${this.buffer.length} logs into database`); this.buffer = []; } catch (error) { console.error('Failed to insert logs into database:', error); } } async close() { await this.flush(); } } // Factory function for creating logger instances function createLogger(config, context) { return new StructuredLogger(config, context); } // Default logger instance exports.logger = createLogger({ level: process.env.LOG_LEVEL || 'info', format: 'json', outputs: [ { type: 'console', config: { colorize: process.env.NODE_ENV !== 'production', timestamp: true, level: 'info' } }, { type: 'file', config: { filename: './logs/hook-engine.log', maxSize: 10 * 1024 * 1024, maxFiles: 5, level: 'info' } } ] }); // Export types for external use __exportStar(require("../types/logging"), exports);