import { isReactNative } from "./utils/is-react-native.ts"; export enum LogLevelEnum { trace, debug, info, warn, error, } export type LogLevel = keyof typeof LogLevelEnum; export type Sink = (logLevel: LogLevel, message: string, ...data: any[]) => void; const logToConsole: Sink = (logLevel, message, ...rest) => { let logMethod; switch (logLevel) { case "error": if (isReactNative()) { message = `ERROR: ${message}`; logMethod = console.info; break; } logMethod = console.error; break; case "warn": if (isReactNative()) { message = `WARN: ${message}`; logMethod = console.info; break; } logMethod = console.warn; break; case "info": logMethod = console.info; break; case "trace": logMethod = console.trace; break; default: logMethod = console.log; break; } logMethod(message, ...rest); }; const DEFAULT_LOG_LEVEL: LogLevel = "info"; const DEFAULT_SINK: Sink = logToConsole; type DefaultStringType = string & {}; export type Logger = { withExtraTags: (...extraTags: string[]) => Logger; getLogLevel: () => LogLevel; } & Record void>; export type ConfigureLoggersOptions = T extends "default" ? never : Partial<{ [K in T | "default"]: Partial<{ sink: K extends "default" ? Sink : Sink | null; level: K extends "default" ? LogLevel : LogLevel | null; }>; }>; export type LoggerSystem = { getLogger: (scope: T, options?: { tags?: string[] }) => Logger; configureLoggers: (optionsByScope?: ConfigureLoggersOptions) => void; restoreDefaults: () => void; }; export const createLoggerSystem = (): LoggerSystem => { const sinkByScope = new Map([["default", DEFAULT_SINK]]); const logLevelByScope = new Map([["default", DEFAULT_LOG_LEVEL]]); const getLogger = (scope: T, options: { tags?: string[] } = {}): Logger => { const tagString = (options.tags ?? []).filter(Boolean).join(","); const constructLogFunction = (logLevel: LogLevel) => (message: string, ...data: any[]) => { const scopedLogLevel = logLevelByScope.get(scope) ?? logLevelByScope.get("default")!; if (LogLevelEnum[logLevel] >= LogLevelEnum[scopedLogLevel]) { const scopedSink = sinkByScope.get(scope) ?? sinkByScope.get("default")!; scopedSink( logLevel, `[${scope}]${tagString.length ? `(${tagString})` : ""}: ${message}`, ...data, ); } }; return { withExtraTags: (...extraTags: string[]) => { return getLogger(scope, { ...options, tags: options.tags ? options.tags.concat(extraTags) : extraTags, }); }, getLogLevel: () => { return logLevelByScope.get(scope) ?? logLevelByScope.get("default")!; }, ...({ error: constructLogFunction("error"), trace: constructLogFunction("trace"), debug: constructLogFunction("debug"), info: constructLogFunction("info"), warn: constructLogFunction("warn"), } satisfies Record>), }; }; const configureLoggers = (optionsByScope?: ConfigureLoggersOptions) => { for (const scope in optionsByScope) { const options = optionsByScope[scope]!; if (options.sink) { sinkByScope.set(scope, options.sink); } else if (options.sink === null && scope !== "default") { sinkByScope.delete(scope); } if (options.level) { logLevelByScope.set(scope, options.level); } else if (options.level === null && scope !== "default") { logLevelByScope.delete(scope); } } }; const restoreDefaults = () => { sinkByScope.clear(); logLevelByScope.clear(); sinkByScope.set("default", DEFAULT_SINK); logLevelByScope.set("default", DEFAULT_LOG_LEVEL); }; return { getLogger, configureLoggers, restoreDefaults, }; };