import { generateUuidV4FromUrl } from "#Source/identifier/index.ts" import { ConsoleDebugLogEmitter, ConsoleErrorLogEmitter, ConsoleInfoLogEmitter, ConsoleLogLogEmitter, ConsoleWarnLogEmitter, } from "./log-emitter.ts" import type { LogEmitter, LogEmitterOptions } from "./log-emitter.ts" import type { LogRecord } from "./log-record.ts" import { getGlobalLogScheduler } from "./log-scheduler.ts" import type { LogTask } from "./log-scheduler.ts" import type { LogType } from "./log-type.ts" const globalLogScheduler = getGlobalLogScheduler() /** * @description Describe an object that exposes a `Logger` instance. */ export interface LoggerFriendly { logger: Logger } /** * @description Describe options that may provide an existing `Logger`. */ export interface LoggerFriendlyOptions { logger?: Logger | undefined } /** * @description Describe an emitter registration for a specific log type. */ export interface LogEmitterItem { logType: LogType // oxlint-disable-next-line no-explicit-any LogEmitter: new (...args: any[]) => LogEmitter injectOptions?: ((options: LogEmitterOptions) => LogEmitterOptions) | undefined } /** * @description Define the default console-based emitters. */ const DEFAULT_CONSOLE_LOG_EMITTER_ITEMS: LogEmitterItem[] = [ { logType: "log", LogEmitter: ConsoleLogLogEmitter, }, { logType: "info", LogEmitter: ConsoleInfoLogEmitter, }, { logType: "warn", LogEmitter: ConsoleWarnLogEmitter, }, { logType: "error", LogEmitter: ConsoleErrorLogEmitter, }, { logType: "debug", LogEmitter: ConsoleDebugLogEmitter, }, ] /** * @description Describe runtime logger configuration values. */ export interface LoggerConfigs { enabled: boolean autoSend: boolean filter: (logRecord: LogRecord) => boolean emitters: LogEmitterItem[] } /** * @description Describe options used to construct a `Logger`. */ export interface LoggerOptions { name?: string | undefined parent?: Logger | undefined configs?: Partial | undefined } /** * @description 表示支持层级继承、标签管理与延迟发送的日志记录器。 * * 规范: * - 初始配置的优先级为 parentConfigs -> configs -> 默认值。 * - 当 parent 为 undefined 时,parentConfigs 也应该是 undefined。 * - 更新 parent 时,parentConfigs 也应该同步更新。 * - 作为 parent 修改自己的 configs 时,不会影响 child 的 configs。 * - 作为 child 可以独立修改自己的 configs,也可以将 parentConfigs 应用到自己的 configs。 */ export class Logger implements Disposable { /** * @description 从选项中解析 logger;如果未提供,则基于全局 logger 派生一个新 logger。 * * @example * ``` * // Expect: example1 === customLogger * const customLogger = new Logger({ name: "Custom" }) * const example1 = Logger.fromOptions({ logger: customLogger }) * * // Expect: example2 !== globalLogger * // Expect: example2.hasParent() === true * const globalLogger = getGlobalLogger() * const example2 = Logger.fromOptions({}) * * class CustomClass implements LoggerFriendly { * logger: Logger * * constructor(options: LoggerFriendlyOptions) { * this.logger = Logger.fromOptions(options) * } * } * * // Expect: example3.logger instanceof Logger * const example3 = new CustomClass({ logger: customLogger }) * ``` */ static fromOptions(options: LoggerFriendlyOptions): Logger { if (options.logger !== undefined) { return options.logger } const parent = getGlobalLogger() const logger = Logger.derive(parent) return logger } /** * @description Create a child logger from a parent logger. */ static derive(parentLogger: Logger): Logger { const logger = new Logger({ parent: parentLogger }) return logger } protected defaultName: string protected name?: string | undefined protected parent?: Logger | undefined protected childs: Set protected parentConfigs?: LoggerConfigs | undefined protected configs: LoggerConfigs protected instanceTags: string[] protected sessionTags: string[] protected onceTags: string[] private logTasks: LogTask[] private cachedLogMethodMap: Map this> constructor(options: LoggerOptions) { this.defaultName = "Unnamed" this.name = options.name this.parent = options.parent this.childs = new Set() this.parent?.addChild(this) const parentConfigs = options.parent?.getConfigs() const configs = options.configs this.parentConfigs = parentConfigs this.configs = { enabled: parentConfigs?.enabled ?? configs?.enabled ?? true, autoSend: parentConfigs?.autoSend ?? configs?.autoSend ?? true, filter: parentConfigs?.filter ?? configs?.filter ?? ((): boolean => true), emitters: parentConfigs?.emitters ?? configs?.emitters ?? DEFAULT_CONSOLE_LOG_EMITTER_ITEMS, } this.instanceTags = [] this.sessionTags = [] this.onceTags = [] this.logTasks = [] this.cachedLogMethodMap = new Map() } derive(): Logger { const logger = Logger.derive(this) return logger } /** * @description Set the default name if the current name is empty, * and return the logger for chaining. */ setDefaultName(name: string): this { this.defaultName = name return this } /** * @description Get the logger display name. */ getName(): string { return this.name ?? this.defaultName } /** * @description Set the logger display name. */ setName(name: string | undefined): this { this.name = name return this } /** * @description Check whether the logger currently has a parent. */ hasParent(): boolean { return this.parent !== undefined } /** * @description Set or clear the parent logger. */ setParent(parentLogger: Logger): this { if (this.parent === parentLogger) { return this } this.parent?.delChild(this) this.parent = parentLogger this.parent?.addChild(this) this.updateParentConfigs() return this } unsetParent(): this { if (this.parent === undefined) { return this } const parent = this.parent this.parent = undefined parent.delChild(this) this.updateParentConfigs() return this } addChild(childLogger: Logger): this { if (this.childs.has(childLogger) === true) { return this } this.childs.add(childLogger) childLogger.setParent(this) return this } delChild(childLogger: Logger): this { if (this.childs.has(childLogger) === false) { return this } this.childs.delete(childLogger) childLogger.unsetParent() return this } /** * @description Get merged effective configs from parent and local overrides. */ getConfigs(): LoggerConfigs { return { ...this.parentConfigs, ...this.configs } } /** * @description Update local configs and refresh inherited configs on child loggers. */ setConfigs(configs: Partial): this { this.configs = { ...this.getConfigs(), ...configs } for (const child of this.childs) { child.updateParentConfigs() } return this } /** * @description Refresh inherited configs from parent logger; should be called when * parent logger or its configs are updated. */ updateParentConfigs(): this { this.parentConfigs = this.parent?.getConfigs() return this } /** * @description Apply current parent configs onto local configs. */ useParentConfigs(): this { if (this.parentConfigs !== undefined) { this.setConfigs(this.parentConfigs) } return this } /** * @description Get all configured emitter registrations. */ getAllLogEmitters(): LogEmitterItem[] { const emitters = this.getConfigs().emitters return emitters } /** * @description Get emitter registrations for a specific log type. */ getLogEmitters(logType: LogType): LogEmitterItem[] { const emitters = this.getConfigs().emitters const filteredEmitters = emitters.filter((item) => item.logType === logType) return filteredEmitters } /** * @description 支持为同一类型的 Log 添加多个 LogEmitter。 */ addLogEmitters(logEmitters: LogEmitterItem[]): this { const newEmitters = [...this.getConfigs().emitters] for (const item of logEmitters) { if ( !newEmitters.some((e) => e.logType === item.logType && e.LogEmitter === item.LogEmitter) ) { newEmitters.push(item) } } this.setConfigs({ emitters: newEmitters, }) return this } /** * @description Remove matched emitter registrations. */ removeLogEmitters(logEmitters: LogEmitterItem[]): this { const emitters = this.getConfigs().emitters const remainingEmitters = emitters.filter((item) => { return !logEmitters.some( (e) => e.logType === item.logType && e.LogEmitter === item.LogEmitter, ) }) this.setConfigs({ emitters: remainingEmitters, }) return this } /** * @description Invoke all emitters that match the record log type. */ invokeLogEmitter(record: LogRecord): void { const { type } = record if (type === undefined) { return } const logEmitterList = this.getLogEmitters(type) if (logEmitterList.length === 0) { return } logEmitterList.forEach((item) => { const injectOptions = item.injectOptions ?? ((v: T): T => v) const LogEmitter = item.LogEmitter const logEmitter = new LogEmitter(injectOptions(record)) logEmitter.emit() }) } /** * @description Get full hierarchical name tags from root to current logger. */ getNameTags(): string[] { const names = [...(this.parent?.getNameTags() ?? []), this.getName()] return names } /** * @description Add persistent instance tags. */ addInstanceTags(tags: string[]): this { this.instanceTags.push(...tags) return this } /** * @description Remove matched instance tags. */ removeInstanceTags(tags: string[]): this { this.instanceTags = this.instanceTags.filter((tag) => !tags.includes(tag)) return this } /** * @description Get current instance tags. */ getInstanceTags(): string[] { return this.instanceTags } /** * @description Clear all instance tags. */ clearInstanceTags(): this { this.instanceTags = [] return this } /** * @description Add session tags used until `tagEnd` is called. */ addSessionTags(tags: string[]): this { this.sessionTags.push(...tags) return this } /** * @description Remove matched session tags. */ removeSessionTags(tags: string[]): this { this.sessionTags = this.sessionTags.filter((tag) => !tags.includes(tag)) return this } /** * @description Get current session tags. */ getSessionTags(): string[] { return this.sessionTags } /** * @description Clear all session tags. */ clearSessionTags(): this { this.sessionTags = [] return this } /** * @description Start and automatically end a session tag scope on dispose. */ autoTag(tags: string[]): Disposable { this.tagStart(tags) return { [Symbol.dispose]: (): void => { this.tagEnd() }, } } /** * @description Start a session tag scope. */ tagStart(tags: string[]): this { this.addSessionTags(tags) return this } /** * @description End the current session tag scope. */ tagEnd(): this { this.clearSessionTags() return this } /** * @description Add one-shot tags consumed by the next log call. */ addOnceTags(tags: string[]): this { this.onceTags.push(...tags) return this } /** * @description Get current one-shot tags. */ getOnceTags(): string[] { return this.onceTags } /** * @description Clear all one-shot tags. */ clearOnceTags(): this { this.onceTags = [] return this } /** * @description Build and cache a log method bound to type and prefix. */ private makeLogMethod(logType: LogType, prefix: string): (...args: unknown[]) => this { const cachedLogMethodKey = `${logType}-${prefix}` const cachedLogMethod = this.cachedLogMethodMap.get(cachedLogMethodKey) if (cachedLogMethod !== undefined) { return cachedLogMethod } const newMethod = (...messages: unknown[]): this => { const configs = this.getConfigs() if (configs.enabled === false) { return this } const tags = [ prefix, ...this.getNameTags(), ...this.getInstanceTags(), ...this.getSessionTags(), ...this.getOnceTags(), ] const logRecord: LogRecord = { type: logType, tags, messages, } // oxlint-disable-next-line no-array-callback-reference if (configs.filter(logRecord) === false) { return this } this.logTasks.push({ record: logRecord, emit: () => { this.invokeLogEmitter(logRecord) }, }) if (configs.autoSend === true) { this.send() } this.clearOnceTags() return this } this.cachedLogMethodMap.set(cachedLogMethodKey, newMethod) return newMethod } /** * @description Queue a `log` level record. */ log(...messages: unknown[]): this { return this.makeLogMethod("log", "🟢")(...messages) } /** * @description Queue an `info` level record. */ info(...messages: unknown[]): this { return this.makeLogMethod("info", "🔵")(...messages) } /** * @description Queue a `warn` level record. */ warn(...messages: unknown[]): this { return this.makeLogMethod("warn", "🟡")(...messages) } /** * @description Queue an `error` level record. */ error(...messages: unknown[]): this { return this.makeLogMethod("error", "🔴")(...messages) } /** * @description Queue a `debug` level record. */ debug(...messages: unknown[]): this { return this.makeLogMethod("debug", "🟤")(...messages) } /** * @description Create a child logger configured for manual batched sending. */ batch(id?: string | undefined): Logger { const logger = Logger.derive(this) logger.setConfigs({ autoSend: false }) logger.addInstanceTags([id ?? generateUuidV4FromUrl()]) return logger } /** * @description Flush queued records to the global scheduler. */ send(): void { globalLogScheduler.enqueue(this.logTasks) this.logTasks = [] } /** * @description Flush queued records when used with `using` disposal. */ [Symbol.dispose](): void { this.send() } } /** * @description Get the singleton global `Logger` instance. */ export const getGlobalLogger: () => Logger = (() => { let instance: Logger | undefined = undefined return (): Logger => { instance = instance ?? new Logger({ name: "GlobalLogger" }) return instance } })()