支持集成第三方日志库 (#190)

* 更新 ILogger 签名

改为纯可变参数兼容主流日志库

* 拆分日志类型与实现

* 新增 setLoggerFactory 方法

* tweak

* getLoggerName 返回类名,默认情况下子类无需重写

* 更新日志说明文档

* 增加测试

* 使用 getSystemInstanceTypeName,避免压缩导致获取类名不一致
This commit is contained in:
LINGYE
2025-10-26 11:53:46 +08:00
committed by GitHub
parent 3ea55303dc
commit 0cd99209c4
11 changed files with 449 additions and 247 deletions

View File

@@ -238,6 +238,50 @@ class HierarchicalLoggingExample {
}
```
### 集成第三方日志库
通过 `setLoggerFactory` 可以将业务代码中的日志器替换为第三方日志库(如 winston、pino、nestjs Logger 等)。
**说明**: 目前框架内部日志仍使用 ConsoleLogger自定义日志器仅影响业务代码如 EntitySystem
#### 基本用法
```typescript
import { setLoggerFactory } from '@esengine/ecs-framework';
setLoggerFactory((name?: string) => {
// 返回实现 ILogger 接口的日志器实例
return yourLogger;
});
```
#### 使用示例
```typescript
// 集成 Winston
setLoggerFactory((name?: string) => winston.createLogger({ /* ... */ }));
// 集成 Pino
setLoggerFactory((name?: string) => pino({ name }));
// 集成 NestJS Logger
setLoggerFactory((name?: string) => new Logger(name));
```
#### EntitySystem 中的使用
EntitySystem 会自动使用类名创建日志器:
```typescript
class PlayerMovementSystem extends EntitySystem {
// this.logger 自动使用 'PlayerMovementSystem' 作为名称
protected process(entities: readonly Entity[]): void {
this.logger.info(`处理 ${entities.length} 个玩家实体`);
}
}
```
### 自定义输出
```typescript
@@ -547,4 +591,4 @@ class LoggingConfiguration {
LoggingConfiguration.setupLogging();
```
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。

View File

@@ -847,10 +847,10 @@ export abstract class EntitySystem<
/**
* 获取Logger名称
* 子类可以重写此方法来自定义logger名称
* 默认返回类的构造函数名称, 子类可以重写此方法来自定义logger名称
*/
protected getLoggerName(): string {
return 'EntitySystem';
return getSystemInstanceTypeName(this);
}
/**
@@ -1093,4 +1093,4 @@ export abstract class EntitySystem<
}
return true;
}
}
}

View File

@@ -6,6 +6,7 @@ import { createLogger } from '../../Utils/Logger';
import type { IComponent } from '../../Types';
import { PlatformManager } from '../../Platform/PlatformManager';
import type { IPlatformAdapter, PlatformWorker } from '../../Platform/IPlatformAdapter';
import { getSystemInstanceTypeName } from '../Decorators';
/**
* Worker处理函数类型
@@ -853,7 +854,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
}
protected override getLoggerName(): string {
return 'WorkerEntitySystem';
return getSystemInstanceTypeName(this);
}
}
@@ -1007,4 +1008,4 @@ class PlatformWorkerPool {
this.taskQueue.length = 0;
this.busyWorkers.clear();
}
}
}

View File

@@ -1,85 +1,6 @@
/**
*
*/
export enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
Fatal = 4,
None = 5
}
import { Colors, LogLevel } from "./Constants";
import { ILogger, LoggerColorConfig, LoggerConfig } from "./Types";
/**
*
*/
export interface ILogger {
debug(message: string, ...args: unknown[]): void;
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
fatal(message: string, ...args: unknown[]): void;
}
/**
*
*/
export interface LoggerColorConfig {
debug?: string;
info?: string;
warn?: string;
error?: string;
fatal?: string;
reset?: string;
}
/**
*
*/
export const Colors = {
// 基础颜色
BLACK: '\x1b[30m',
RED: '\x1b[31m',
GREEN: '\x1b[32m',
YELLOW: '\x1b[33m',
BLUE: '\x1b[34m',
MAGENTA: '\x1b[35m',
CYAN: '\x1b[36m',
WHITE: '\x1b[37m',
// 亮色版本
BRIGHT_BLACK: '\x1b[90m',
BRIGHT_RED: '\x1b[91m',
BRIGHT_GREEN: '\x1b[92m',
BRIGHT_YELLOW: '\x1b[93m',
BRIGHT_BLUE: '\x1b[94m',
BRIGHT_MAGENTA: '\x1b[95m',
BRIGHT_CYAN: '\x1b[96m',
BRIGHT_WHITE: '\x1b[97m',
// 特殊
RESET: '\x1b[0m',
BOLD: '\x1b[1m',
UNDERLINE: '\x1b[4m'
} as const;
/**
*
*/
export interface LoggerConfig {
/** 日志级别 */
level: LogLevel;
/** 是否启用时间戳 */
enableTimestamp: boolean;
/** 是否启用颜色 */
enableColors: boolean;
/** 日志前缀 */
prefix?: string;
/** 自定义输出函数 */
output?: (level: LogLevel, message: string) => void;
/** 自定义颜色配置 */
colors?: LoggerColorConfig;
}
/**
*
@@ -279,157 +200,3 @@ export class ConsoleLogger implements ILogger {
};
}
}
/**
*
*/
export class LoggerManager {
private static _instance: LoggerManager;
private _loggers = new Map<string, ILogger>();
private _defaultLogger: ILogger;
private _defaultLevel = LogLevel.Info;
private constructor() {
this._defaultLogger = new ConsoleLogger({
level: this._defaultLevel,
});
}
/**
*
* @returns
*/
public static getInstance(): LoggerManager {
if (!LoggerManager._instance) {
LoggerManager._instance = new LoggerManager();
}
return LoggerManager._instance;
}
/**
*
* @param name
* @returns
*/
public getLogger(name?: string): ILogger {
if (!name) {
return this._defaultLogger;
}
if (!this._loggers.has(name)) {
const logger = new ConsoleLogger({
prefix: name,
level: this._defaultLevel,
});
this._loggers.set(name, logger);
}
return this._loggers.get(name)!;
}
/**
*
* @param name
* @param logger
*/
public setLogger(name: string, logger: ILogger): void {
this._loggers.set(name, logger);
}
/**
*
* @param level
*/
public setGlobalLevel(level: LogLevel): void {
this._defaultLevel = level;
if (this._defaultLogger instanceof ConsoleLogger) {
this._defaultLogger.setLevel(level);
}
for (const logger of this._loggers.values()) {
if (logger instanceof ConsoleLogger) {
logger.setLevel(level);
}
}
}
/**
*
* @param parentName
* @param childName
* @returns
*/
public createChildLogger(parentName: string, childName: string): ILogger {
const fullName = `${parentName}.${childName}`;
return this.getLogger(fullName);
}
/**
*
* @param colors
*/
public setGlobalColors(colors: LoggerColorConfig): void {
if (this._defaultLogger instanceof ConsoleLogger) {
this._defaultLogger.setColors(colors);
}
for (const logger of this._loggers.values()) {
if (logger instanceof ConsoleLogger) {
logger.setColors(colors);
}
}
}
/**
*
*/
public resetColors(): void {
if (this._defaultLogger instanceof ConsoleLogger) {
this._defaultLogger.setColors({});
}
for (const logger of this._loggers.values()) {
if (logger instanceof ConsoleLogger) {
logger.setColors({});
}
}
}
}
/**
*
*/
export const Logger = LoggerManager.getInstance().getLogger();
/**
*
* @param name
* @returns
*/
export function createLogger(name: string): ILogger {
return LoggerManager.getInstance().getLogger(name);
}
/**
*
* @param colors
*/
export function setLoggerColors(colors: LoggerColorConfig): void {
LoggerManager.getInstance().setGlobalColors(colors);
}
/**
*
*/
export function resetLoggerColors(): void {
LoggerManager.getInstance().resetColors();
}
/**
*
* @param level
*/
export function setGlobalLogLevel(level: LogLevel): void {
LoggerManager.getInstance().setGlobalLevel(level);
}

View File

@@ -0,0 +1,41 @@
/**
* 日志级别
*/
export enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
Fatal = 4,
None = 5
}
/**
* 预定义的颜色常量
*/
export const Colors = {
// 基础颜色
BLACK: '\x1b[30m',
RED: '\x1b[31m',
GREEN: '\x1b[32m',
YELLOW: '\x1b[33m',
BLUE: '\x1b[34m',
MAGENTA: '\x1b[35m',
CYAN: '\x1b[36m',
WHITE: '\x1b[37m',
// 亮色版本
BRIGHT_BLACK: '\x1b[90m',
BRIGHT_RED: '\x1b[91m',
BRIGHT_GREEN: '\x1b[92m',
BRIGHT_YELLOW: '\x1b[93m',
BRIGHT_BLUE: '\x1b[94m',
BRIGHT_MAGENTA: '\x1b[95m',
BRIGHT_CYAN: '\x1b[96m',
BRIGHT_WHITE: '\x1b[97m',
// 特殊
RESET: '\x1b[0m',
BOLD: '\x1b[1m',
UNDERLINE: '\x1b[4m'
} as const;

View File

@@ -0,0 +1,194 @@
import { ConsoleLogger } from "./ConsoleLogger";
import { LogLevel } from "./Constants";
import { ILogger, LoggerColorConfig } from "./Types";
/**
* 日志管理器
*/
export class LoggerManager {
private static _instance: LoggerManager;
private _loggers = new Map<string, ILogger>();
private _defaultLogger?: ILogger;
private _defaultLevel = LogLevel.Info;
private _loggerFactory?: (name?: string) => ILogger;
private constructor() {}
private get defaultLogger(): ILogger {
if (!this._defaultLogger) {
this._defaultLogger = this.createDefaultLogger();
}
return this._defaultLogger;
}
// 新增: 创建默认 logger 的逻辑
private createDefaultLogger(): ILogger {
if (this._loggerFactory) {
return this._loggerFactory();
}
return new ConsoleLogger({ level: this._defaultLevel });
}
/**
* 获取日志管理器实例
* @returns 日志管理器实例
*/
public static getInstance(): LoggerManager {
if (!LoggerManager._instance) {
LoggerManager._instance = new LoggerManager();
}
return LoggerManager._instance;
}
/**
* 获取或创建日志器
* @param name 日志器名称
* @returns 日志器实例
*/
public getLogger(name?: string): ILogger {
if (!name) {
return this.defaultLogger;
}
if (!this._loggers.has(name)) {
const logger = this._loggerFactory
? this._loggerFactory(name)
: new ConsoleLogger({ prefix: name, level: this._defaultLevel });
this._loggers.set(name, logger);
}
return this._loggers.get(name)!;
}
/**
* 设置日志器
* @param name 日志器名称
* @param logger 日志器实例
*/
public setLogger(name: string, logger: ILogger): void {
this._loggers.set(name, logger);
}
/**
* 设置全局日志级别
* @param level 日志级别
*/
public setGlobalLevel(level: LogLevel): void {
this._defaultLevel = level;
if (this._defaultLogger instanceof ConsoleLogger) {
this._defaultLogger.setLevel(level);
}
for (const logger of this._loggers.values()) {
if (logger instanceof ConsoleLogger) {
logger.setLevel(level);
}
}
}
/**
* 创建子日志器
* @param parentName 父日志器名称
* @param childName 子日志器名称
* @returns 子日志器实例
*/
public createChildLogger(parentName: string, childName: string): ILogger {
const fullName = `${parentName}.${childName}`;
return this.getLogger(fullName);
}
/**
* 设置全局颜色配置
* @param colors 颜色配置
*/
public setGlobalColors(colors: LoggerColorConfig): void {
if (this._defaultLogger instanceof ConsoleLogger) {
this._defaultLogger.setColors(colors);
}
for (const logger of this._loggers.values()) {
if (logger instanceof ConsoleLogger) {
logger.setColors(colors);
}
}
}
/**
* 重置为默认颜色配置
*/
public resetColors(): void {
if (this._defaultLogger instanceof ConsoleLogger) {
this._defaultLogger.setColors({});
}
for (const logger of this._loggers.values()) {
if (logger instanceof ConsoleLogger) {
logger.setColors({});
}
}
}
/**
* 设置日志器工厂方法
* @param factory 日志器工厂方法
*/
public setLoggerFactory(factory: (name?: string) => ILogger): void {
if (this._defaultLogger || this._loggers.size > 0) {
console.warn(
'[LoggerManager] setLoggerFactory 应该在导入 ECS 模块之前调用。' +
'已创建的 logger 引用不会被更新。'
);
}
this._loggerFactory = factory;
// 清空已创建的 logger, 下次获取时使用新工厂方法
this._defaultLogger = undefined;
this._loggers.clear();
}
}
/**
* 默认日志器实例
*/
export const Logger = LoggerManager.getInstance().getLogger();
/**
* 创建命名日志器
* @param name 日志器名称
* @returns 日志器实例
*/
export function createLogger(name: string): ILogger {
return LoggerManager.getInstance().getLogger(name);
}
/**
* 设置全局日志颜色配置
* @param colors 颜色配置
*/
export function setLoggerColors(colors: LoggerColorConfig): void {
LoggerManager.getInstance().setGlobalColors(colors);
}
/**
* 重置日志颜色为默认配置
*/
export function resetLoggerColors(): void {
LoggerManager.getInstance().resetColors();
}
/**
* 设置全局日志级别
* @param level 日志级别
*/
export function setGlobalLogLevel(level: LogLevel): void {
LoggerManager.getInstance().setGlobalLevel(level);
}
/**
* 设置日志器工厂方法
* @param factory 日志器工厂方法
*/
export function setLoggerFactory(factory: (name?: string) => ILogger): void {
LoggerManager.getInstance().setLoggerFactory(factory);
}

View File

@@ -0,0 +1,42 @@
import type { LogLevel } from "./Constants";
/**
* 日志接口
*/
export interface ILogger {
debug(...args: unknown[]): void;
info(...args: unknown[]): void;
warn(...args: unknown[]): void;
error(...args: unknown[]): void;
fatal(...args: unknown[]): void;
}
/**
* 日志颜色配置接口
*/
export interface LoggerColorConfig {
debug?: string;
info?: string;
warn?: string;
error?: string;
fatal?: string;
reset?: string;
}
/**
* 日志配置
*/
export interface LoggerConfig {
/** 日志级别 */
level: LogLevel;
/** 是否启用时间戳 */
enableTimestamp: boolean;
/** 是否启用颜色 */
enableColors: boolean;
/** 日志前缀 */
prefix?: string;
/** 自定义输出函数 */
output?: (level: LogLevel, message: string) => void;
/** 自定义颜色配置 */
colors?: LoggerColorConfig;
}

View File

@@ -0,0 +1,4 @@
export * from './ConsoleLogger';
export * from './Constants';
export * from './LoggerManager';
export * from './Types';

View File

@@ -5,4 +5,4 @@ export * from './GlobalManager';
export * from './PerformanceMonitor';
export { Time } from './Time';
export * from './Debug';
export * from './Logger';
export * from './Logger';

View File

@@ -214,4 +214,11 @@ describe('EntitySystem', () => {
});
});
describe('日志器命名', () => {
it('应该使用类名作为日志器名称', () => {
const loggerName = (system as any).getLoggerName();
expect(loggerName).toBe('ConcreteEntitySystem');
});
});
});

View File

@@ -153,24 +153,125 @@ describe('Logger', () => {
});
});
describe('LoggerManager - 自定义工厂', () => {
let manager: LoggerManager;
beforeEach(() => {
manager = LoggerManager.getInstance();
});
afterEach(() => {
// 重置工厂,恢复默认 ConsoleLogger
(manager as any)._loggerFactory = undefined;
(manager as any)._defaultLogger = undefined;
(manager as any)._loggers.clear();
});
it('应该支持设置自定义日志器工厂', () => {
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
fatal: jest.fn()
};
manager.setLoggerFactory(() => mockLogger);
const logger = manager.getLogger('CustomLogger');
expect(logger).toBe(mockLogger);
});
it('应该将日志器名称传递给工厂方法', () => {
const factorySpy = jest.fn(() => ({
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
fatal: jest.fn()
}));
manager.setLoggerFactory(factorySpy);
manager.getLogger('TestLogger');
expect(factorySpy).toHaveBeenCalledWith('TestLogger');
});
it('应该在设置工厂后清空已创建的日志器', () => {
const logger1 = manager.getLogger('TestLogger');
manager.setLoggerFactory(() => ({
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
fatal: jest.fn()
}));
const logger2 = manager.getLogger('TestLogger');
expect(logger2).not.toBe(logger1);
});
it('应该延迟创建默认日志器直到首次使用', () => {
const factorySpy = jest.fn(() => ({
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
fatal: jest.fn()
}));
manager.setLoggerFactory(factorySpy);
// 此时不应该调用工厂
expect(factorySpy).not.toHaveBeenCalled();
// 获取默认日志器时才调用
manager.getLogger();
expect(factorySpy).toHaveBeenCalled();
});
it('应该在已创建日志器后设置工厂时发出警告', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
// 先创建一个日志器
manager.getLogger('ExistingLogger');
// 再设置工厂
manager.setLoggerFactory(() => ({
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
fatal: jest.fn()
}));
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('setLoggerFactory 应该在导入 ECS 模块之前调用')
);
warnSpy.mockRestore();
});
});
describe('全局颜色配置', () => {
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
consoleSpy = jest.spyOn(console, 'info').mockImplementation();
});
afterEach(() => {
consoleSpy.mockRestore();
resetLoggerColors();
});
it('应该支持全局设置颜色配置', () => {
const logger = createLogger('TestLogger');
setLoggerColors({
info: Colors.MAGENTA
});
const logger = createLogger('TestLogger');
logger.info('测试消息');
const call = consoleSpy.mock.calls[0][0];
@@ -179,17 +280,18 @@ describe('Logger', () => {
});
it('应该支持重置颜色配置为默认值', () => {
const logger = createLogger('TestLogger');
setLoggerColors({
info: Colors.MAGENTA
});
resetLoggerColors();
const logger = createLogger('TestLogger');
logger.info('测试消息');
const call = consoleSpy.mock.calls[0][0];
expect(call).toContain('\x1b[32m');
});
});
});
});