UNPKG

@diagramers/api

Version:

Diagramers API - A comprehensive Node.js API template with TypeScript, Firebase Functions, and Socket.io

396 lines (341 loc) 10.3 kB
export interface LoggerConfig { level: string; format: 'json' | 'simple' | 'colored'; transports: string[]; file?: { enabled: boolean; path: string; maxSize: string; maxFiles: number; }; console?: { enabled: boolean; colors: boolean; }; database?: { enabled: boolean; collection: string; connection?: any; }; external?: { enabled: boolean; type: 'serilog' | 'sentry' | 'loggly' | 'papertrail'; url?: string; apiKey?: string; appName?: string; }; } export interface LogEntry { timestamp: string; level: string; message: string; meta?: any; trace?: string; userId?: string; requestId?: string; sessionId?: string; } export interface LogTransport { name: string; log(entry: LogEntry): Promise<void>; isEnabled(): boolean; } class ConsoleTransport implements LogTransport { name = 'console'; private colors: boolean; private format: string; constructor(config: LoggerConfig) { this.colors = config.console?.colors ?? true; this.format = config.format; } isEnabled(): boolean { return true; // Console is always enabled } async log(entry: LogEntry): Promise<void> { const formattedMessage = this.formatMessage(entry); switch (entry.level) { case 'error': console.error(formattedMessage); break; case 'warn': console.warn(formattedMessage); break; case 'info': console.log(formattedMessage); break; case 'debug': console.debug(formattedMessage); break; default: console.log(formattedMessage); } } private formatMessage(entry: LogEntry): string { if (this.format === 'json') { return JSON.stringify(entry); } if (this.format === 'colored' && this.colors) { const colors = { error: '\x1b[31m', // Red warn: '\x1b[33m', // Yellow info: '\x1b[36m', // Cyan debug: '\x1b[35m', // Magenta verbose: '\x1b[90m' // Gray }; const reset = '\x1b[0m'; return `${colors[entry.level as keyof typeof colors]}[${entry.level.toUpperCase()}]${reset} ${entry.timestamp} - ${entry.message}`; } return `[${entry.level.toUpperCase()}] ${entry.timestamp} - ${entry.message}`; } } class FileTransport implements LogTransport { name = 'file'; private enabled: boolean; private path: string; private fs: any; constructor(config: LoggerConfig) { this.enabled = config.file?.enabled ?? false; this.path = config.file?.path ?? './logs/app.log'; if (this.enabled) { // Dynamically import fs-extra to avoid issues this.fs = require('fs-extra'); this.ensureLogDirectory(); } } isEnabled(): boolean { return this.enabled; } async log(entry: LogEntry): Promise<void> { if (!this.enabled || !this.fs) return; try { const logLine = JSON.stringify(entry) + '\n'; await this.fs.appendFile(this.path, logLine); } catch (error) { console.error('Failed to write to log file:', error); } } private async ensureLogDirectory(): Promise<void> { if (!this.fs) return; try { const lastSlashIndex = this.path.lastIndexOf('/'); if (lastSlashIndex > 0) { const dir = this.path.substring(0, lastSlashIndex); await this.fs.ensureDir(dir); } } catch (error) { console.error('Failed to create log directory:', error); } } } class DatabaseTransport implements LogTransport { name = 'database'; private enabled: boolean; private collection: string; private connection: any; constructor(config: LoggerConfig) { this.enabled = config.database?.enabled ?? false; this.collection = config.database?.collection ?? 'logs'; this.connection = config.database?.connection; } isEnabled(): boolean { return this.enabled && this.connection; } async log(entry: LogEntry): Promise<void> { if (!this.enabled || !this.connection) return; try { const logDocument = { ...entry, createdAt: new Date(entry.timestamp) }; await this.connection.collection(this.collection).insertOne(logDocument); } catch (error) { console.error('Failed to write to database:', error); } } } class ExternalTransport implements LogTransport { name = 'external'; private enabled: boolean; private type: string; private url?: string; private apiKey?: string; private appName?: string; constructor(config: LoggerConfig) { this.enabled = config.external?.enabled ?? false; this.type = config.external?.type ?? 'serilog'; this.url = config.external?.url; this.apiKey = config.external?.apiKey; this.appName = config.external?.appName; } isEnabled(): boolean { return this.enabled === true && this.url !== undefined; } async log(entry: LogEntry): Promise<void> { if (!this.enabled || !this.url) return; try { const payload = this.formatPayload(entry); await fetch(this.url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(this.apiKey && { 'Authorization': `Bearer ${this.apiKey}` }) }, body: JSON.stringify(payload) }); } catch (error) { console.error('Failed to send to external logging service:', error); } } private formatPayload(entry: LogEntry): any { switch (this.type) { case 'serilog': return { timestamp: entry.timestamp, level: entry.level, message: entry.message, properties: entry.meta, ...(this.appName && { application: this.appName }) }; case 'sentry': return { level: entry.level, message: entry.message, timestamp: entry.timestamp, extra: entry.meta, tags: { ...(this.appName && { application: this.appName }) } }; case 'loggly': return { level: entry.level, message: entry.message, timestamp: entry.timestamp, meta: entry.meta, ...(this.appName && { application: this.appName }) }; default: return entry; } } } class Logger { private level: string; private transports: LogTransport[] = []; private requestId?: string; private userId?: string; private sessionId?: string; constructor(config: LoggerConfig) { this.level = config.level || 'info'; // Initialize transports this.transports.push(new ConsoleTransport(config)); if (config.file?.enabled) { this.transports.push(new FileTransport(config)); } if (config.database?.enabled) { this.transports.push(new DatabaseTransport(config)); } if (config.external?.enabled) { this.transports.push(new ExternalTransport(config)); } } private shouldLog(level: string): boolean { const levels = ['error', 'warn', 'info', 'debug', 'verbose']; return levels.indexOf(level) <= levels.indexOf(this.level); } private createLogEntry(level: string, message: string, meta?: any): LogEntry { return { timestamp: new Date().toISOString(), level: level.toUpperCase(), message, meta, ...(this.requestId && { requestId: this.requestId }), ...(this.userId && { userId: this.userId }), ...(this.sessionId && { sessionId: this.sessionId }) }; } private async writeToTransports(entry: LogEntry): Promise<void> { const enabledTransports = this.transports.filter(t => t.isEnabled()); await Promise.allSettled( enabledTransports.map(transport => transport.log(entry)) ); } info(message: string, meta?: any): void { if (this.shouldLog('info')) { const entry = this.createLogEntry('info', message, meta); this.writeToTransports(entry); } } warn(message: string, meta?: any): void { if (this.shouldLog('warn')) { const entry = this.createLogEntry('warn', message, meta); this.writeToTransports(entry); } } error(message: string, meta?: any): void { if (this.shouldLog('error')) { const entry = this.createLogEntry('error', message, meta); this.writeToTransports(entry); } } debug(message: string, meta?: any): void { if (this.shouldLog('debug')) { const entry = this.createLogEntry('debug', message, meta); this.writeToTransports(entry); } } verbose(message: string, meta?: any): void { if (this.shouldLog('verbose')) { const entry = this.createLogEntry('verbose', message, meta); this.writeToTransports(entry); } } // Method to change log level at runtime setLevel(level: string): void { this.level = level; } // Method to get current log level getLevel(): string { return this.level; } // Method to set context for request tracking setContext(requestId?: string, userId?: string, sessionId?: string): void { this.requestId = requestId; this.userId = userId; this.sessionId = sessionId; } // Method to add custom transport addTransport(transport: LogTransport): void { this.transports.push(transport); } // Method to get all transports getTransports(): LogTransport[] { return this.transports; } } export const logger = new Logger({ level: process.env.LOG_LEVEL || 'info', format: (process.env.LOG_FORMAT as 'json' | 'simple' | 'colored') || 'colored', transports: ['console'], console: { enabled: true, colors: process.env.LOG_COLORS !== 'false' }, file: { enabled: process.env.LOG_FILE_ENABLED === 'true', path: process.env.LOG_FILE_PATH || './logs/app.log', maxSize: process.env.LOG_FILE_MAX_SIZE || '10m', maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES || '5') }, database: { enabled: process.env.LOG_DATABASE_ENABLED === 'true', collection: process.env.LOG_DATABASE_COLLECTION || 'logs' }, external: { enabled: process.env.LOG_EXTERNAL_ENABLED === 'true', type: (process.env.LOG_EXTERNAL_TYPE as 'serilog' | 'sentry' | 'loggly' | 'papertrail') || 'serilog', url: process.env.LOG_EXTERNAL_URL, apiKey: process.env.LOG_EXTERNAL_API_KEY, appName: process.env.LOG_EXTERNAL_APP_NAME } });