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
JavaScript
"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);