From ffeac3ee7f144adee874f93285d943aa8ebcf061 Mon Sep 17 00:00:00 2001 From: kaolayu Date: Thu, 18 Dec 2025 15:44:55 +0800 Subject: [PATCH] feat(logging): add structured logger and fx integration --- src/core/system/System.ts | 78 +++++++- src/index.ts | 15 ++ src/utils/Logger.ts | 350 ++++++++++++++++++++++++++++++++++++ src/utils/MathUtils.ts | 18 +- tests/core/System.test.ts | 362 +++++++++++++++++++++++++++++++++++++- 5 files changed, 811 insertions(+), 12 deletions(-) create mode 100644 src/utils/Logger.ts diff --git a/src/core/system/System.ts b/src/core/system/System.ts index c502a21..0260338 100644 --- a/src/core/system/System.ts +++ b/src/core/system/System.ts @@ -73,6 +73,18 @@ import { OperationLayerData } from "../../data/layers/OperationLayerData"; import { Message } from "../../communication/messaging/Message"; import { NodeType } from "../../core/types/NodeType"; import { MathUtils } from "../../utils/MathUtils"; +import { + Logger, + LogLevel, + configureLogger, + setLogLevel, + setModuleLogLevel, + getLogBuffer, + createLogger, + LogEntry, + LoggerConfig, + LogTransport, +} from "../../utils/Logger"; // ==================== 类型定义和接口 ==================== @@ -171,6 +183,8 @@ function isDataItem(item: unknown): item is DataItem { * 用于系统测试和验证 */ class A { + protected logger = createLogger("test/A"); + /** * 测试方法 */ @@ -178,7 +192,7 @@ class A { try { console.log("console A"); } catch (error) { - console.error("Error in class A test:", error); + this.logger.error("Error in class A test", error as Error); } } } @@ -188,6 +202,8 @@ class A { * 继承自类A,用于测试继承功能 */ class B extends A { + protected logger = createLogger("test/B"); + constructor() { super(); } @@ -199,7 +215,7 @@ class B extends A { try { console.log("console B"); } catch (error) { - console.error("Error in class B test:", error); + this.logger.error("Error in class B test", error as Error); } } } @@ -300,6 +316,58 @@ export class fx { * 可直接访问完整的 MathUtils 模块 */ static MathUtils = MathUtils; + + // ==================== 日志系统 ==================== + + /** + * Logger 模块引用 + */ + static Logger = Logger; + static LogLevel = LogLevel; + + /** + * 创建模块日志记录器 + * @param module 模块名称 + * @returns Logger 实例 + */ + static createLogger(module: string): Logger { + return createLogger(module); + } + + /** + * 配置全局日志系统 + * @param config 日志配置 + */ + static configureLogger(config: Partial): void { + configureLogger(config); + } + + /** + * 设置全局日志级别 + * @param level 日志级别 + */ + static setLogLevel(level: LogLevel): void { + setLogLevel(level); + } + + /** + * 设置特定模块的日志级别 + * @param module 模块名称 + * @param level 日志级别 + */ + static setModuleLogLevel(module: string, level: LogLevel): void { + setModuleLogLevel(module, level); + } + + /** + * 获取日志缓冲区中的最近日志 + * @param count 要获取的日志数量(可选) + * @returns 日志条目数组 + */ + static getLogBuffer(count?: number): LogEntry[] { + return getLogBuffer(count); + } + // ==================== 系统状态属性 ==================== /** 看板列表 */ @@ -3735,6 +3803,7 @@ export class fx { * 启动 FX 引擎核心功能 */ static init = function (): void { + const logger = createLogger("system"); try { if (fx.isStart === false) { fx.isStart = true; @@ -3748,7 +3817,7 @@ export class fx { // fx.parseLibraryBody(fx_json_data.library.operationArray, true); } } catch (error) { - console.error("Error initializing FX system:", error); + logger.error("Error initializing FX system", error as Error); fx.isStart = false; fx.code = false; } @@ -3768,6 +3837,7 @@ export class fx { lflist: any[], brlist: any[] ): void { + const logger = createLogger("system/data"); try { if (!obj || !obj.tree || !Array.isArray(obj.tree)) { throw new Error("Invalid object structure for recursive reading"); @@ -3788,7 +3858,7 @@ export class fx { } } } catch (error) { - console.error("Error in recursive data reading:", error); + logger.error("Error in recursive data reading", error as Error, { obj: obj?.name || "unknown" }); } } } diff --git a/src/index.ts b/src/index.ts index f71c5db..32671c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,21 @@ export { OperationLayerData } from "./data/layers/OperationLayerData"; export { Message } from "./communication/messaging/Message"; export { NodeType } from "./core/types/NodeType"; export { MathUtils } from "./utils/MathUtils"; +export { + Logger, + LogLevel, + LogEntry, + LoggerConfig, + LogTransport, + ConsoleTransport, + MemoryTransport, + CallbackTransport, + createLogger, + configureLogger, + setLogLevel, + setModuleLogLevel, + getLogBuffer, +} from "./utils/Logger"; /** * 环境兼容性处理:支持多种 JavaScript 运行环境 diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts new file mode 100644 index 0000000..576497a --- /dev/null +++ b/src/utils/Logger.ts @@ -0,0 +1,350 @@ +/** + * Logger - Structured logging system for SoonFX + * Provides log levels, transports, and module-scoped logging + */ + +/** + * Log severity levels + */ +export enum LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + NONE = 4, +} + +/** + * Log entry structure + */ +export interface LogEntry { + timestamp: Date; + level: LogLevel; + module: string; + message: string; + data?: Record; + error?: Error; +} + +/** + * Logger configuration + */ +export interface LoggerConfig { + level: LogLevel; + formatter: (entry: LogEntry) => string; + transports: LogTransport[]; +} + +/** + * Log transport interface + * Transports handle writing log entries to different destinations + */ +export interface LogTransport { + write(entry: LogEntry): void; +} + +/** + * Console transport - outputs logs to browser/Node console + */ +export class ConsoleTransport implements LogTransport { + write(entry: LogEntry): void { + const formatted = this.formatEntry(entry); + const levelName = LogLevel[entry.level]; + + switch (entry.level) { + case LogLevel.DEBUG: + console.debug(formatted); + break; + case LogLevel.INFO: + console.info(formatted); + break; + case LogLevel.WARN: + console.warn(formatted); + break; + case LogLevel.ERROR: + console.error(formatted); + if (entry.error) { + console.error(entry.error); + } + break; + default: + console.log(formatted); + } + } + + private formatEntry(entry: LogEntry): string { + const timestamp = entry.timestamp.toISOString(); + const levelName = LogLevel[entry.level].padEnd(5); + const module = entry.module.padEnd(20); + let message = `[${timestamp}] ${levelName} [${module}] ${entry.message}`; + + if (entry.data && Object.keys(entry.data).length > 0) { + message += ` ${JSON.stringify(entry.data)}`; + } + + return message; + } +} + +/** + * Memory transport - stores logs in an in-memory ring buffer + */ +export class MemoryTransport implements LogTransport { + private buffer: LogEntry[] = []; + private maxSize: number; + + constructor(maxSize: number = 1000) { + this.maxSize = maxSize; + } + + write(entry: LogEntry): void { + this.buffer.push(entry); + if (this.buffer.length > this.maxSize) { + this.buffer.shift(); + } + } + + /** + * Get recent log entries + */ + getLogs(count?: number): LogEntry[] { + if (count === undefined) { + return [...this.buffer]; + } + return this.buffer.slice(-count); + } + + /** + * Clear the log buffer + */ + clear(): void { + this.buffer = []; + } + + /** + * Get the current buffer size + */ + size(): number { + return this.buffer.length; + } +} + +/** + * Callback transport - triggers user-defined callback for each log + */ +export class CallbackTransport implements LogTransport { + private callback: (entry: LogEntry) => void; + + constructor(callback: (entry: LogEntry) => void) { + this.callback = callback; + } + + write(entry: LogEntry): void { + try { + this.callback(entry); + } catch (error) { + // Avoid infinite loops - use console directly if callback fails + console.error("Error in log callback:", error); + } + } +} + +/** + * Default formatter for log entries + */ +function defaultFormatter(entry: LogEntry): string { + const timestamp = entry.timestamp.toISOString(); + const levelName = LogLevel[entry.level].padEnd(5); + const module = entry.module.padEnd(20); + let message = `[${timestamp}] ${levelName} [${module}] ${entry.message}`; + + if (entry.data && Object.keys(entry.data).length > 0) { + message += ` ${JSON.stringify(entry.data)}`; + } + + if (entry.error) { + message += `\nError: ${entry.error.message}`; + if (entry.error.stack) { + message += `\nStack: ${entry.error.stack}`; + } + } + + return message; +} + +/** + * Global logger configuration + */ +let globalConfig: LoggerConfig = { + level: LogLevel.INFO, + formatter: defaultFormatter, + transports: [new ConsoleTransport()], +}; + +/** + * Module-specific log levels + */ +const moduleLogLevels: Map = new Map(); + +/** + * Global memory transport instance for log buffer access + */ +let globalMemoryTransport: MemoryTransport | null = null; + +/** + * Logger class - provides structured logging with module scoping + */ +export class Logger { + private module: string; + private config: LoggerConfig; + + constructor(module: string, config?: Partial) { + this.module = module; + this.config = { + level: config?.level ?? globalConfig.level, + formatter: config?.formatter ?? globalConfig.formatter, + transports: config?.transports ?? globalConfig.transports, + }; + } + + /** + * Check if a log level should be written + */ + private shouldLog(level: LogLevel): boolean { + // Check module-specific level first + const moduleLevel = moduleLogLevels.get(this.module); + const effectiveLevel = moduleLevel ?? this.config.level; + return level >= effectiveLevel; + } + + /** + * Write a log entry + */ + private write(level: LogLevel, message: string, error?: Error, data?: Record): void { + if (!this.shouldLog(level)) { + return; + } + + const entry: LogEntry = { + timestamp: new Date(), + level, + module: this.module, + message, + data, + error, + }; + + for (const transport of this.config.transports) { + try { + transport.write(entry); + } catch (err) { + // Avoid infinite loops - use console directly if transport fails + console.error("Error in log transport:", err); + } + } + } + + /** + * Log a debug message + */ + debug(message: string, data?: Record): void { + this.write(LogLevel.DEBUG, message, undefined, data); + } + + /** + * Log an info message + */ + info(message: string, data?: Record): void { + this.write(LogLevel.INFO, message, undefined, data); + } + + /** + * Log a warning message + */ + warn(message: string, data?: Record): void { + this.write(LogLevel.WARN, message, undefined, data); + } + + /** + * Log an error message + */ + error(message: string, error?: Error, data?: Record): void { + this.write(LogLevel.ERROR, message, error, data); + } + + /** + * Create a child logger with a submodule name + */ + child(submodule: string): Logger { + const childModule = `${this.module}/${submodule}`; + return new Logger(childModule, this.config); + } +} + +/** + * Configure the global logger + */ +export function configureLogger(config: Partial): void { + if (config.level !== undefined) { + globalConfig.level = config.level; + } + if (config.formatter !== undefined) { + globalConfig.formatter = config.formatter; + } + if (config.transports !== undefined) { + globalConfig.transports = config.transports; + // Check if MemoryTransport is in the list + globalMemoryTransport = config.transports.find( + (t) => t instanceof MemoryTransport + ) as MemoryTransport | null; + } +} + +/** + * Set the global log level + */ +export function setLogLevel(level: LogLevel): void { + globalConfig.level = level; +} + +/** + * Set log level for a specific module + */ +export function setModuleLogLevel(module: string, level: LogLevel): void { + moduleLogLevels.set(module, level); +} + +/** + * Get recent log entries from memory buffer + */ +export function getLogBuffer(count?: number): LogEntry[] { + if (globalMemoryTransport) { + return globalMemoryTransport.getLogs(count); + } + return []; +} + +/** + * Create a new logger instance for a module + */ +export function createLogger(module: string): Logger { + return new Logger(module); +} + +/** + * Initialize default logger configuration with memory transport + */ +export function initializeLogger(): void { + // Add memory transport if not already present + const hasMemoryTransport = globalConfig.transports.some( + (t) => t instanceof MemoryTransport + ); + if (!hasMemoryTransport) { + globalMemoryTransport = new MemoryTransport(1000); + globalConfig.transports.push(globalMemoryTransport); + } +} + +// Initialize logger on module load +initializeLogger(); + diff --git a/src/utils/MathUtils.ts b/src/utils/MathUtils.ts index fc8a8d4..ee8bb2a 100644 --- a/src/utils/MathUtils.ts +++ b/src/utils/MathUtils.ts @@ -5,6 +5,10 @@ * 重构日期: 2025年12月 */ +import { createLogger } from "./Logger"; + +const logger = createLogger("MathUtils"); + /** * 判断是否为有效数字 * @param value 待检查的值 @@ -34,7 +38,7 @@ export function mix(v1: number, v2: number, t: number): number { } return v1 * (1 - t) + v2 * t; } catch (error) { - console.error("Error in mix function:", error); + logger.error("Error in mix function", error as Error, { v1, v2, t }); return 0; } } @@ -70,7 +74,7 @@ export function cross( } return p1x * p2y - p1y * p2x; } catch (error) { - console.error("Error in cross function:", error); + logger.error("Error in cross function", error as Error, { p1x, p1y, p2x, p2y }); return 0; } } @@ -98,7 +102,7 @@ export function dot( } return p1x * p2x + p1y * p2y; } catch (error) { - console.error("Error in dot function:", error); + logger.error("Error in dot function", error as Error, { p1x, p1y, p2x, p2y }); return 0; } } @@ -118,7 +122,7 @@ export function length(a: number, b: number): number { } return Math.sqrt(a * a + b * b); } catch (error) { - console.error("Error in length function:", error); + logger.error("Error in length function", error as Error, { a, b }); return 0; } } @@ -148,7 +152,7 @@ export function distance( const b = Math.abs(p1y - p2y); return length(a, b); } catch (error) { - console.error("Error in distance function:", error); + logger.error("Error in distance function", error as Error, { p1x, p1y, p2x, p2y }); return 0; } } @@ -171,7 +175,7 @@ export function clamp(value: number, min: number, max: number): number { } return Math.max(min, Math.min(max, value)); } catch (error) { - console.error("Error in clamp function:", error); + logger.error("Error in clamp function", error as Error, { value, min, max }); return value; } } @@ -195,7 +199,7 @@ export function normalize(x: number, y: number): [number, number] { } return [x / len, y / len]; } catch (error) { - console.error("Error in normalize function:", error); + logger.error("Error in normalize function", error as Error, { x, y }); return [0, 0]; } } diff --git a/tests/core/System.test.ts b/tests/core/System.test.ts index e4ff503..2861e67 100644 --- a/tests/core/System.test.ts +++ b/tests/core/System.test.ts @@ -1,5 +1,6 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { fx } from '../../src/core/system/System'; +import { Logger, LogLevel, MemoryTransport, ConsoleTransport, CallbackTransport } from '../../src/utils/Logger'; describe('fx (System)', () => { describe('Math Utilities', () => { @@ -47,5 +48,364 @@ describe('fx (System)', () => { expect(fx.evaluateExpression('2 + 3 * 4')).toBe(14); }); }); + + describe('Logger System', () => { + beforeEach(() => { + // Reset logger configuration before each test + fx.setLogLevel(LogLevel.INFO); + fx.configureLogger({ + transports: [new ConsoleTransport(), new MemoryTransport(100)] + }); + }); + + describe('Logger Creation', () => { + it('createLogger should create a logger instance', () => { + const logger = fx.createLogger('test-module'); + expect(logger).toBeInstanceOf(Logger); + }); + + it('Logger should have all log methods', () => { + const logger = fx.createLogger('test'); + expect(typeof logger.debug).toBe('function'); + expect(typeof logger.info).toBe('function'); + expect(typeof logger.warn).toBe('function'); + expect(typeof logger.error).toBe('function'); + expect(typeof logger.child).toBe('function'); + }); + }); + + describe('Log Levels', () => { + it('setLogLevel should change global log level', () => { + fx.setLogLevel(LogLevel.WARN); + expect(fx.getLogBuffer().length).toBe(0); + }); + + it('setModuleLogLevel should set module-specific log level', () => { + fx.setModuleLogLevel('test-module', LogLevel.DEBUG); + const logger = fx.createLogger('test-module'); + + // Should be able to log at DEBUG level for this module + logger.debug('Debug message'); + const logs = fx.getLogBuffer(); + expect(logs.length).toBeGreaterThan(0); + expect(logs[logs.length - 1].level).toBe(LogLevel.DEBUG); + }); + + it('should respect global log level filtering', () => { + fx.setLogLevel(LogLevel.ERROR); + const logger = fx.createLogger('test'); + + logger.debug('Debug message'); + logger.info('Info message'); + logger.warn('Warn message'); + + const logs = fx.getLogBuffer(); + const recentLogs = logs.filter(log => + log.message.includes('Debug') || + log.message.includes('Info') || + log.message.includes('Warn') + ); + expect(recentLogs.length).toBe(0); + }); + + it('should log at or above the set log level', () => { + fx.setLogLevel(LogLevel.WARN); + const logger = fx.createLogger('test'); + + logger.warn('Warn message'); + logger.error('Error message'); + + const logs = fx.getLogBuffer(); + const recentLogs = logs.filter(log => + log.message.includes('Warn') || log.message.includes('Error') + ); + expect(recentLogs.length).toBeGreaterThanOrEqual(2); + }); + }); + + describe('Log Methods', () => { + it('debug should create DEBUG level log entry', () => { + fx.setLogLevel(LogLevel.DEBUG); + const logger = fx.createLogger('test'); + logger.debug('Debug message', { key: 'value' }); + + const logs = fx.getLogBuffer(); + const lastLog = logs[logs.length - 1]; + expect(lastLog.level).toBe(LogLevel.DEBUG); + expect(lastLog.message).toBe('Debug message'); + expect(lastLog.module).toBe('test'); + expect(lastLog.data).toEqual({ key: 'value' }); + }); + + it('info should create INFO level log entry', () => { + const logger = fx.createLogger('test'); + logger.info('Info message', { data: 123 }); + + const logs = fx.getLogBuffer(); + const lastLog = logs[logs.length - 1]; + expect(lastLog.level).toBe(LogLevel.INFO); + expect(lastLog.message).toBe('Info message'); + expect(lastLog.data).toEqual({ data: 123 }); + }); + + it('warn should create WARN level log entry', () => { + const logger = fx.createLogger('test'); + logger.warn('Warning message'); + + const logs = fx.getLogBuffer(); + const lastLog = logs[logs.length - 1]; + expect(lastLog.level).toBe(LogLevel.WARN); + expect(lastLog.message).toBe('Warning message'); + }); + + it('error should create ERROR level log entry with error object', () => { + const logger = fx.createLogger('test'); + const testError = new Error('Test error'); + logger.error('Error message', testError, { context: 'test' }); + + const logs = fx.getLogBuffer(); + const lastLog = logs[logs.length - 1]; + expect(lastLog.level).toBe(LogLevel.ERROR); + expect(lastLog.message).toBe('Error message'); + expect(lastLog.error).toBe(testError); + expect(lastLog.data).toEqual({ context: 'test' }); + }); + + it('error should work without error object', () => { + const logger = fx.createLogger('test'); + logger.error('Error message without error object'); + + const logs = fx.getLogBuffer(); + const lastLog = logs[logs.length - 1]; + expect(lastLog.level).toBe(LogLevel.ERROR); + expect(lastLog.message).toBe('Error message without error object'); + expect(lastLog.error).toBeUndefined(); + }); + }); + + describe('Child Loggers', () => { + it('child should create logger with submodule name', () => { + const parentLogger = fx.createLogger('parent'); + const childLogger = parentLogger.child('child'); + + childLogger.info('Child log message'); + + const logs = fx.getLogBuffer(); + const lastLog = logs[logs.length - 1]; + expect(lastLog.module).toBe('parent/child'); + }); + + it('child logger should inherit parent configuration', () => { + fx.setLogLevel(LogLevel.DEBUG); + const parentLogger = fx.createLogger('parent'); + const childLogger = parentLogger.child('child'); + + childLogger.debug('Debug from child'); + + const logs = fx.getLogBuffer(); + const lastLog = logs[logs.length - 1]; + expect(lastLog.level).toBe(LogLevel.DEBUG); + expect(lastLog.module).toBe('parent/child'); + }); + }); + + describe('Log Buffer', () => { + it('getLogBuffer should return recent log entries', () => { + const logger = fx.createLogger('test'); + logger.info('Message 1'); + logger.info('Message 2'); + logger.info('Message 3'); + + const logs = fx.getLogBuffer(); + expect(logs.length).toBeGreaterThanOrEqual(3); + }); + + it('getLogBuffer with count should limit returned entries', () => { + const logger = fx.createLogger('test'); + logger.info('Message 1'); + logger.info('Message 2'); + logger.info('Message 3'); + logger.info('Message 4'); + logger.info('Message 5'); + + const logs = fx.getLogBuffer(2); + expect(logs.length).toBe(2); + }); + + it('log entries should have timestamp', () => { + const logger = fx.createLogger('test'); + logger.info('Timestamp test'); + + const logs = fx.getLogBuffer(); + const lastLog = logs[logs.length - 1]; + expect(lastLog.timestamp).toBeInstanceOf(Date); + expect(lastLog.timestamp.getTime()).toBeLessThanOrEqual(Date.now()); + }); + }); + + describe('Logger Configuration', () => { + it('configureLogger should update transports', () => { + const memoryTransport = new MemoryTransport(50); + fx.configureLogger({ + transports: [memoryTransport] + }); + + const logger = fx.createLogger('test'); + logger.info('Test message'); + + const logs = memoryTransport.getLogs(); + expect(logs.length).toBeGreaterThan(0); + expect(logs[logs.length - 1].message).toBe('Test message'); + }); + + it('configureLogger should update log level', () => { + fx.configureLogger({ + level: LogLevel.WARN + }); + + const logger = fx.createLogger('test'); + logger.info('Should not log'); + logger.warn('Should log'); + + const logs = fx.getLogBuffer(); + const infoLogs = logs.filter(log => log.message === 'Should not log'); + const warnLogs = logs.filter(log => log.message === 'Should log'); + + expect(infoLogs.length).toBe(0); + expect(warnLogs.length).toBeGreaterThan(0); + }); + }); + + describe('Transports', () => { + it('MemoryTransport should store logs', () => { + const transport = new MemoryTransport(10); + const logger = new Logger('test', { + transports: [transport], + level: LogLevel.DEBUG + }); + + logger.info('Test 1'); + logger.info('Test 2'); + + const logs = transport.getLogs(); + expect(logs.length).toBe(2); + expect(logs[0].message).toBe('Test 1'); + expect(logs[1].message).toBe('Test 2'); + }); + + it('MemoryTransport should respect max size', () => { + const transport = new MemoryTransport(3); + const logger = new Logger('test', { + transports: [transport], + level: LogLevel.DEBUG + }); + + logger.info('Test 1'); + logger.info('Test 2'); + logger.info('Test 3'); + logger.info('Test 4'); + logger.info('Test 5'); + + const logs = transport.getLogs(); + expect(logs.length).toBe(3); + expect(logs[0].message).toBe('Test 3'); // Oldest should be dropped + expect(logs[logs.length - 1].message).toBe('Test 5'); + }); + + it('MemoryTransport clear should empty buffer', () => { + const transport = new MemoryTransport(10); + const logger = new Logger('test', { + transports: [transport], + level: LogLevel.DEBUG + }); + + logger.info('Test message'); + expect(transport.size()).toBe(1); + + transport.clear(); + expect(transport.size()).toBe(0); + expect(transport.getLogs().length).toBe(0); + }); + + it('CallbackTransport should call callback', () => { + const callback = vi.fn(); + const transport = new CallbackTransport(callback); + const logger = new Logger('test', { + transports: [transport], + level: LogLevel.DEBUG + }); + + logger.info('Test message', { key: 'value' }); + + expect(callback).toHaveBeenCalledTimes(1); + const entry = callback.mock.calls[0][0]; + expect(entry.message).toBe('Test message'); + expect(entry.data).toEqual({ key: 'value' }); + expect(entry.module).toBe('test'); + }); + + it('ConsoleTransport should write to console', () => { + const consoleSpy = { + debug: vi.spyOn(console, 'debug').mockImplementation(() => {}), + info: vi.spyOn(console, 'info').mockImplementation(() => {}), + warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), + error: vi.spyOn(console, 'error').mockImplementation(() => {}) + }; + + const transport = new ConsoleTransport(); + const logger = new Logger('test', { + transports: [transport], + level: LogLevel.DEBUG + }); + + logger.debug('Debug message'); + logger.info('Info message'); + logger.warn('Warn message'); + logger.error('Error message', new Error('Test error')); + + expect(consoleSpy.debug).toHaveBeenCalled(); + expect(consoleSpy.info).toHaveBeenCalled(); + expect(consoleSpy.warn).toHaveBeenCalled(); + expect(consoleSpy.error).toHaveBeenCalledTimes(2); // Message + error object + + // Cleanup + consoleSpy.debug.mockRestore(); + consoleSpy.info.mockRestore(); + consoleSpy.warn.mockRestore(); + consoleSpy.error.mockRestore(); + }); + }); + + describe('LogLevel Enum', () => { + it('LogLevel should have correct values', () => { + expect(LogLevel.DEBUG).toBe(0); + expect(LogLevel.INFO).toBe(1); + expect(LogLevel.WARN).toBe(2); + expect(LogLevel.ERROR).toBe(3); + expect(LogLevel.NONE).toBe(4); + }); + + it('fx.LogLevel should be accessible', () => { + expect(fx.LogLevel).toBe(LogLevel); + expect(fx.LogLevel.DEBUG).toBe(0); + }); + }); + + describe('Integration with fx singleton', () => { + it('fx.Logger should be accessible', () => { + expect(fx.Logger).toBe(Logger); + }); + + it('should work with fx.createLogger', () => { + const logger = fx.createLogger('integration-test'); + logger.info('Integration test message'); + + const logs = fx.getLogBuffer(); + const lastLog = logs[logs.length - 1]; + expect(lastLog.module).toBe('integration-test'); + expect(lastLog.message).toBe('Integration test message'); + }); + }); + }); });