UNPKG

dash-core

Version:

A foundational toolkit of types, collections, services, and architectural patterns designed to accelerate application development.

255 lines (207 loc) 8.49 kB
import path from 'path'; import { createLogger, format, Logger, transports } from 'winston'; const RESET = '\x1b[0m'; // Reset to default color const RED = '\x1b[31m'; const YELLOW = '\x1b[33m'; const CYAN = "\x1b[36m"; const GREEN = "\x1b[32m"; export enum LogLevel { DEBUG = 'debug', INFO = 'info', WARNING = 'warn', ERROR = 'error' } const LogLevelMap: Record<LogLevel, number> = { [LogLevel.ERROR]: 1, [LogLevel.WARNING]: 2, [LogLevel.INFO]: 3, [LogLevel.DEBUG]: 4 }; const defaultFormat = format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), format.errors({ stack: true }), format.splat()); const fileFormat = format.combine( defaultFormat, format.printf((info) => { const { timestamp, level, message, service } = info; const splat = (info[Symbol.for('splat')] || []) as unknown[]; let baseLog = `${timestamp}\t${level}\t${service}\t${message}`; if (splat && splat.length > 0) baseLog += '\n' + deserializeSplat(splat); return baseLog; })); const consoleFormat = format.combine( defaultFormat, format.printf((info) => { const { timestamp, level, message, service } = info; const splat = (info[Symbol.for('splat')] || []) as unknown[]; let baseLog = level === LogLevel.INFO || level === LogLevel.DEBUG ? `${CYAN}${timestamp}${RESET}\t${level}\t${GREEN}${service}${RESET}\t${message}` : `${timestamp}\t${level}\t${service}\t${message}`; if (splat && splat.length > 0) baseLog += '\n' + deserializeSplat(splat); if (level === LogLevel.ERROR) return `${RED}${baseLog}${RESET}`; if (level === LogLevel.WARNING) return `${YELLOW}${baseLog}${RESET}`; return baseLog; })); function deserializeSplat(splat: unknown[]): string { return splat .map((data) => { if (data instanceof Error) { return data.stack; } if (typeof data === 'object') { try { return JSON.stringify(data, null, 2); } catch { return String(data); } } return String(data); }) .join(' '); } function parseFilter(input?: string): Map<string, boolean> { const map = new Map<string, boolean>(); if (!input) { return map; } input.split(",").forEach((raw) => { const item = raw.trim(); if (item === "") return; const isIncluded = !item.startsWith("!"); const name = item.replace(/^!/, "").trim(); map.set(name, isIncluded); }); return map; } function createWinstonLogger(options: ServiceInfo): Logger { const globalLogLevel = options.globalLogLevel; const targetLogLevel = options.logLevel; const logLevel = LogLevelMap[globalLogLevel] < LogLevelMap[targetLogLevel] ? globalLogLevel : targetLogLevel const projectDirectory = options.logPath || process.cwd(); const logDirectory = path.join(projectDirectory, 'logs'); const result = createLogger({ level: logLevel, format: fileFormat, defaultMeta: { service: 'user-service' }, transports: [ new transports.File({ filename: path.join(logDirectory, 'error.log'), level: LogLevel.ERROR }), new transports.File({ filename: path.join(logDirectory, 'combined.log') }), ], }); if (options.verbose) { result.add( new transports.Console({ format: consoleFormat }) ); } return result; } /** Encapsulates service logging with configurable options. */ export class ServiceLogger { private readonly logger: Logger; private readonly options: ServiceInfo; private get isObservable(): boolean { const item = this.options.globalFilter.get(this.options.serviceName); if (item === undefined) return true; return item; } /** * Creates a new instance of the ServiceLogger class with specified options. * If no options are provided, defaults are used. * @param {ServiceOptions} [options={}] - Configuration options for the logger. * @param {string} options.serviceName - The name of the service (default is "nameless-service"). * @param {LogLevel} options.logLevel - The default log level (default is LogLevel.DEBUG). * @param {boolean} options.verbose - Indicates whether verbose logging is enabled (default is true). * @param {string} options.logPath - The directory where logs will be stored (default is the current working directory). * @param {LogLevel} options.globalLogLevel - The global log level for the service (default is LogLevel.DEBUG). * @param {string} options.globalFilter - A filter string for global logging configuration. */ constructor(options: ServiceOptions = {}) { this.options = { serviceName: options.serviceName || "nameless-service", logLevel: options.logLevel || LogLevel.DEBUG, verbose: options.verbose || true, logPath: options.logPath || process.cwd(), globalLogLevel: typeof options.globalLogLevel === 'string' ? options.globalLogLevel as LogLevel : options.globalLogLevel || LogLevel.DEBUG, globalFilter: parseFilter(options.globalFilter) } this.logger = createWinstonLogger(this.options) this.logger.defaultMeta = { service: this.options.serviceName }; } /** * Logs an informational message if the service is observable. * @param {string} message - The message to log. * @param {...unknown[]} meta - Additional metadata to log alongside the message. */ public info(message: string, ...meta: unknown[]): void { if (!this.isObservable) return; this.logger.info(message, ...meta); }; /** * Logs a warning message. * @param {string} message - The message to log. * @param {...unknown[]} meta - Additional metadata to log alongside the message. */ public warning(message: string, ...meta: unknown[]): void { this.logger.warn(message, ...meta); } /** * Logs a debug message if the service is observable. * @param {string} message - The message to log. * @param {...unknown[]} meta - Additional metadata to log alongside the message. */ public debug(message: string, ...meta: unknown[]): void { if (!this.isObservable) return; this.logger.debug(message, ...meta); } /** * Logs an error message and returns an Error object. * @param {string} message - The error message. * @param {...unknown[]} meta - Additional metadata to log alongside the message. * @returns {Error} The created error. */ public error(message: string, ...meta: unknown[]): Error { this.logger.error(message, ...meta); return new Error(message); } /** * Adds transport to the logger to write logs to a specified file. * @param {string} relativePath - The relative path where the log file will be stored. * @param {LogLevel} [logLevel] - The log level for this transport. If not provided, all logs will be written. */ public addTransport(relativePath: string, logLevel?: LogLevel): void { if (!this.options.verbose) return; let transport; const fullLogPath = path.join(this.options.logPath, relativePath); if (!logLevel) { transport = new transports.File({ filename: path.join(fullLogPath) }); } else { transport = new transports.File({ filename: path.join(fullLogPath), level: logLevel }); } this.logger.add(transport); } } type ServiceInfo = { serviceName: string; logLevel: LogLevel; verbose: boolean; logPath: string; globalLogLevel: LogLevel; globalFilter: Map<string, boolean>; } export type ServiceOptions = { serviceName?: string; logLevel?: LogLevel; verbose?: boolean; logPath?: string; globalLogLevel?: LogLevel | string; globalFilter?: string }