UNPKG

loglayer

Version:

A modern logging library with a fluent API for specifying log messages, metadata and errors

1,556 lines (1,555 loc) 57.5 kB
import { PluginCallbackType, PluginCallbackType as PluginCallbackType$1 } from "@loglayer/plugin"; import { LAZY_EVAL_ERROR, LAZY_SYMBOL, LogLevel, LogLevel as LogLevel$1, LogLevelPriority, countLazyValues, hasPromiseValues, isLazy, lazy, replacePromiseValues, resolveLazyValues, resolveMessages, resolvePromiseValues } from "@loglayer/shared"; import { DefaultContextManager, MockContextManager } from "@loglayer/context-manager"; import { DefaultLogLevelManager, MockLogLevelManager } from "@loglayer/log-level-manager"; import { BaseTransport, LogLevelPriority as LogLevelPriority$1, LoggerlessTransport } from "@loglayer/transport"; //#region src/PluginManager.ts const CALLBACK_LIST = [ PluginCallbackType$1.onBeforeDataOut, PluginCallbackType$1.onMetadataCalled, PluginCallbackType$1.onBeforeMessageOut, PluginCallbackType$1.transformLogLevel, PluginCallbackType$1.shouldSendToLogger, PluginCallbackType$1.onContextCalled ]; /** * A class that manages plugins and runs their callbacks. * Used by LogLayer to run plugins at various stages of the logging process. */ var PluginManager = class { idToPlugin; transformLogLevel = []; onBeforeDataOut = []; shouldSendToLogger = []; onMetadataCalled = []; onBeforeMessageOut = []; onContextCalled = []; constructor(plugins) { this.idToPlugin = {}; this.mapPlugins(plugins); this.indexPlugins(); } mapPlugins(plugins) { for (const plugin of plugins) { if (!plugin.id) plugin.id = Date.now().toString() + Math.random().toString(); if (this.idToPlugin[plugin.id]) throw new Error(`[LogLayer] Plugin with id ${plugin.id} already exists.`); plugin["registeredAt"] = Date.now(); this.idToPlugin[plugin.id] = plugin; } } indexPlugins() { this.transformLogLevel = []; this.onBeforeDataOut = []; this.shouldSendToLogger = []; this.onMetadataCalled = []; this.onBeforeMessageOut = []; this.onContextCalled = []; const pluginList = Object.values(this.idToPlugin).sort((a, b) => a.registeredAt - b.registeredAt); for (const plugin of pluginList) { if (plugin.disabled) return; for (const callback of CALLBACK_LIST) if (plugin[callback] && plugin.id) this[callback].push(plugin.id); } } hasPlugins(callbackType) { return this[callbackType].length > 0; } countPlugins(callbackType) { if (callbackType) return this[callbackType].length; return Object.keys(this.idToPlugin).length; } addPlugins(plugins) { this.mapPlugins(plugins); this.indexPlugins(); } enablePlugin(id) { const plugin = this.idToPlugin[id]; if (plugin) plugin.disabled = false; this.indexPlugins(); } disablePlugin(id) { const plugin = this.idToPlugin[id]; if (plugin) plugin.disabled = true; this.indexPlugins(); } removePlugin(id) { delete this.idToPlugin[id]; this.indexPlugins(); } /** * Runs plugins that define transformLogLevel. Returns the transformed log level or the original if no transformation is applied. * If multiple plugins transform the log level, the last one wins. */ runTransformLogLevel(params, loglayer) { let transformedLevel = null; for (const pluginId of this.transformLogLevel) { const plugin = this.idToPlugin[pluginId]; if (plugin.transformLogLevel) { const result = plugin.transformLogLevel({ data: params.data, logLevel: params.logLevel, messages: params.messages, error: params.error, metadata: params.metadata, context: params.context, groups: params.groups, schema: params.schema, prefix: params.prefix }, loglayer); if (result !== null && result !== void 0 && result !== false) transformedLevel = result; } } return transformedLevel !== null && transformedLevel !== void 0 && transformedLevel !== false ? transformedLevel : params.logLevel; } /** * Runs plugins that defines onBeforeDataOut. */ runOnBeforeDataOut(params, loglayer) { const initialData = { ...params }; const originalData = initialData.data; for (const pluginId of this.onBeforeDataOut) { const plugin = this.idToPlugin[pluginId]; if (plugin.onBeforeDataOut) { const result = plugin.onBeforeDataOut({ data: initialData.data, logLevel: initialData.logLevel, error: initialData.error, metadata: initialData.metadata, context: initialData.context, groups: initialData.groups, schema: initialData.schema, prefix: initialData.prefix }, loglayer); if (result) { if (!initialData.data) initialData.data = {}; if (result !== originalData) Object.assign(initialData.data, result); } } } return initialData.data; } /** * Runs plugins that define shouldSendToLogger. Any plugin that returns false will prevent the message from being sent to the transport. */ runShouldSendToLogger(params, loglayer) { return !this.shouldSendToLogger.some((pluginId) => { return !this.idToPlugin[pluginId].shouldSendToLogger?.(params, loglayer); }); } /** * Runs plugins that define onMetadataCalled. */ runOnMetadataCalled(metadata, loglayer) { let data = { ...metadata }; for (const pluginId of this.onMetadataCalled) { const result = this.idToPlugin[pluginId].onMetadataCalled?.(data, loglayer); if (result) data = result; else return null; } return data; } runOnBeforeMessageOut(params, loglayer) { let messages = [...params.messages]; for (const pluginId of this.onBeforeMessageOut) { const result = this.idToPlugin[pluginId].onBeforeMessageOut?.({ messages, logLevel: params.logLevel, groups: params.groups, schema: params.schema, prefix: params.prefix }, loglayer); if (result) messages = result; } return messages; } /** * Runs plugins that define onContextCalled. */ runOnContextCalled(context, loglayer) { let data = { ...context }; for (const pluginId of this.onContextCalled) { const result = this.idToPlugin[pluginId].onContextCalled?.(data, loglayer); if (result) data = result; else return null; } return data; } }; //#endregion //#region src/LogLayer.ts /** * Wraps around a logging framework to provide convenience methods that allow * developers to programmatically specify their errors and metadata along with * a message in a consistent fashion. */ var LogLayer = class LogLayer { pluginManager; idToTransport; hasMultipleTransports; singleTransport; contextManager; logLevelManager; _isLoggingLazyError = false; _lazyContextCount = 0; _assignedGroups = null; _groupsConfig = null; _activeGroups = null; _ungroupedBehavior = "all"; /** * The configuration object used to initialize the logger. * This is for internal use only and should not be modified directly. */ _config; constructor(config) { this._config = { ...config, enabled: config.enabled ?? true }; this.contextManager = new DefaultContextManager(); this.logLevelManager = new DefaultLogLevelManager(); if (!this._config.enabled) this.disableLogging(); const plugins = [...config.plugins || [], ...mixinRegistry.pluginsToInit]; this.pluginManager = new PluginManager(plugins); if (!this._config.errorFieldName) this._config.errorFieldName = "err"; if (!this._config.copyMsgOnOnlyError) this._config.copyMsgOnOnlyError = false; this._initializeTransports(this._config.transport); this._groupsConfig = config.groups ? { ...config.groups } : null; this._ungroupedBehavior = config.ungroupedBehavior ?? "all"; this._activeGroups = config.activeGroups ? new Set(config.activeGroups) : null; this._parseEnvGroups(); if (mixinRegistry.logLayerHandlers.length > 0) mixinRegistry.logLayerHandlers.forEach((mixin) => { if (mixin.onConstruct) mixin.onConstruct(this, config); }); } /** * Sets the context manager to use for managing context data. */ withContextManager(contextManager) { if (this.contextManager && typeof this.contextManager[Symbol.dispose] === "function") this.contextManager[Symbol.dispose](); this.contextManager = contextManager; this._lazyContextCount = contextManager.hasContextData() ? countLazyValues(contextManager.getContext()) : 0; return this; } /** * Returns the context manager instance being used. */ getContextManager() { return this.contextManager; } /** * Sets the log level manager to use for managing log levels. */ withLogLevelManager(logLevelManager) { if (this.logLevelManager && typeof this.logLevelManager[Symbol.dispose] === "function") this.logLevelManager[Symbol.dispose](); this.logLevelManager = logLevelManager; return this; } /** * Returns the log level manager instance being used. */ getLogLevelManager() { return this.logLevelManager; } /** * Returns the configuration object used to initialize the logger. */ getConfig() { return this._config; } _initializeTransports(transports) { if (this.idToTransport) { for (const id in this.idToTransport) if (this.idToTransport[id] && typeof this.idToTransport[id][Symbol.dispose] === "function") this.idToTransport[id][Symbol.dispose](); } this.hasMultipleTransports = Array.isArray(transports) && transports.length > 1; this.singleTransport = this.hasMultipleTransports ? null : Array.isArray(transports) ? transports[0] : transports; if (Array.isArray(transports)) this.idToTransport = transports.reduce((acc, transport) => { acc[transport.id] = transport; return acc; }, {}); else this.idToTransport = { [transports.id]: transports }; } /** * Calls child() and sets the prefix to be included with every log message. * * @see {@link https://loglayer.dev/logging-api/basic-logging.html#message-prefixing | Message Prefixing Docs} */ withPrefix(prefix) { const logger = this.child(); logger._config.prefix = prefix; return logger; } /** * Creates a child logger with the specified group(s) persistently assigned. * All logs from the child will be tagged with these groups. * * @see {@link https://loglayer.dev/logging-api/groups.html | Groups Docs} */ withGroup(group) { const logger = this.child(); const newGroups = Array.isArray(group) ? group : [group]; if (logger._assignedGroups) { const combined = new Set([...logger._assignedGroups, ...newGroups]); logger._assignedGroups = Array.from(combined); } else logger._assignedGroups = [...newGroups]; return logger; } /** * Adds a new group definition at runtime. * * @see {@link https://loglayer.dev/logging-api/groups.html | Groups Docs} */ addGroup(name, config) { if (!this._groupsConfig) this._groupsConfig = {}; this._groupsConfig[name] = { ...config }; return this; } /** * Removes a group definition at runtime. * * @see {@link https://loglayer.dev/logging-api/groups.html | Groups Docs} */ removeGroup(name) { if (this._groupsConfig) { delete this._groupsConfig[name]; if (Object.keys(this._groupsConfig).length === 0) this._groupsConfig = null; } return this; } /** * Enables a group by name (sets enabled: true). * * @see {@link https://loglayer.dev/logging-api/groups.html | Groups Docs} */ enableGroup(name) { if (this._groupsConfig?.[name]) this._groupsConfig[name] = { ...this._groupsConfig[name], enabled: true }; return this; } /** * Disables a group by name (sets enabled: false). * * @see {@link https://loglayer.dev/logging-api/groups.html | Groups Docs} */ disableGroup(name) { if (this._groupsConfig?.[name]) this._groupsConfig[name] = { ...this._groupsConfig[name], enabled: false }; return this; } /** * Sets the minimum log level for a group at runtime. * * @see {@link https://loglayer.dev/logging-api/groups.html | Groups Docs} */ setGroupLevel(name, level) { if (this._groupsConfig?.[name]) this._groupsConfig[name] = { ...this._groupsConfig[name], level }; return this; } /** * Sets which groups are active. Only active groups will route logs. * Pass null to clear the filter (all groups active). * * @see {@link https://loglayer.dev/logging-api/groups.html | Groups Docs} */ setActiveGroups(groups) { this._activeGroups = groups ? new Set(groups) : null; return this; } /** * Returns a snapshot of all group configurations. * * @see {@link https://loglayer.dev/logging-api/groups.html | Groups Docs} */ getGroups() { return this._groupsConfig ? { ...this._groupsConfig } : {}; } /** * Appends context data which will be included with * every log entry. * * Passing in an empty value / object will *not* clear the context. * * To clear the context, use {@link https://loglayer.dev/logging-api/context.html#clearing-context | clearContext()}. * * @see {@link https://loglayer.dev/logging-api/context.html | Context Docs} */ withContext(context) { let updatedContext = context; if (!context) { if (this._config.consoleDebug) console.debug("[LogLayer] withContext was called with no context; dropping."); return this; } if (this.pluginManager.hasPlugins(PluginCallbackType$1.onContextCalled)) { updatedContext = this.pluginManager.runOnContextCalled(context, this); if (!updatedContext) { if (this._config.consoleDebug) console.debug("[LogLayer] Context was dropped due to plugin returning falsy value."); return this; } } const currentContext = this.contextManager.getContext(); for (const key of Object.keys(updatedContext)) { const wasLazy = key in currentContext && isLazy(currentContext[key]); const nowLazy = isLazy(updatedContext[key]); if (!wasLazy && nowLazy) this._lazyContextCount++; else if (wasLazy && !nowLazy) this._lazyContextCount--; } this.contextManager.appendContext(updatedContext); return this; } /** * Clears the context data. If keys are provided, only those keys will be removed. * If no keys are provided, all context data will be cleared. */ clearContext(keys) { if (keys !== void 0 && this._lazyContextCount > 0) { const context = this.contextManager.getContext(); const keysToRemove = Array.isArray(keys) ? keys : [keys]; for (const key of keysToRemove) if (key in context && isLazy(context[key])) this._lazyContextCount--; } else if (keys === void 0) this._lazyContextCount = 0; this.contextManager.clearContext(keys); return this; } getContext(options) { const context = this.contextManager.getContext(); if (options?.raw || this._lazyContextCount === 0) return context; const { resolved, errors } = resolveLazyValues(context); if (errors) this._logLazyEvalErrors(errors, "context"); const { resolved: finalResolved, asyncKeys } = replacePromiseValues(resolved); if (asyncKeys) this._logAsyncLazyContextErrors(asyncKeys); return finalResolved; } /** * Add additional plugins. * * @see {@link https://loglayer.dev/plugins/ | Plugins Docs} */ addPlugins(plugins) { this.pluginManager.addPlugins(plugins); } /** * Enables a plugin by id. * * @see {@link https://loglayer.dev/plugins/ | Plugins Docs} */ enablePlugin(id) { this.pluginManager.enablePlugin(id); } /** * Disables a plugin by id. * * @see {@link https://loglayer.dev/plugins/ | Plugins Docs} */ disablePlugin(id) { this.pluginManager.disablePlugin(id); } /** * Removes a plugin by id. * * @see {@link https://loglayer.dev/plugins/ | Plugins Docs} */ removePlugin(id) { this.pluginManager.removePlugin(id); } /** * Specifies metadata to include with the log message * * @see {@link https://loglayer.dev/logging-api/metadata.html | Metadata Docs} */ withMetadata(metadata) { return new LogBuilder(this).withMetadata(metadata); } /** * Specifies an Error to include with the log message * * @see {@link https://loglayer.dev/logging-api/error-handling.html | Error Handling Docs} */ withError(error) { return new LogBuilder(this).withError(error); } /** * Creates a new instance of LogLayer but with the initialization * configuration and context copied over. * * @see {@link https://loglayer.dev/logging-api/child-loggers.html | Child Logging Docs} */ child() { const childLogger = new LogLayer({ ...this._config, transport: Array.isArray(this._config.transport) ? [...this._config.transport] : this._config.transport }).withPluginManager(this.pluginManager).withContextManager(this.contextManager.clone()).withLogLevelManager(this.logLevelManager.clone()); this.contextManager.onChildLoggerCreated({ parentContextManager: this.contextManager, childContextManager: childLogger.contextManager, parentLogger: this, childLogger }); this.logLevelManager.onChildLoggerCreated({ parentLogLevelManager: this.logLevelManager, childLogLevelManager: childLogger.logLevelManager, parentLogger: this, childLogger }); childLogger._lazyContextCount = childLogger.contextManager.hasContextData() ? countLazyValues(childLogger.contextManager.getContext()) : 0; childLogger._groupsConfig = this._groupsConfig; childLogger._activeGroups = this._activeGroups; childLogger._ungroupedBehavior = this._ungroupedBehavior; childLogger._assignedGroups = this._assignedGroups ? [...this._assignedGroups] : null; return childLogger; } /** * Replaces all existing transports with new ones. * * Transport changes only affect the current logger instance. Child loggers * created before the change will retain their original transports, and * parent loggers are not affected when a child modifies its transports. * * @see {@link https://loglayer.dev/logging-api/transport-management.html | Transport Management Docs} */ withFreshTransports(transports) { this._config.transport = transports; this._initializeTransports(transports); return this; } /** * Adds one or more transports to the existing transports. * If a transport with the same ID already exists, it will be replaced. * * Transport changes only affect the current logger instance. Child loggers * created before the change will retain their original transports, and * parent loggers are not affected when a child modifies its transports. * * @see {@link https://loglayer.dev/logging-api/transport-management.html | Transport Management Docs} */ addTransport(transports) { const newTransports = Array.isArray(transports) ? transports : [transports]; const existingTransports = Array.isArray(this._config.transport) ? this._config.transport : [this._config.transport]; const newTransportIds = new Set(newTransports.map((t) => t.id)); for (const transport of newTransports) { const existingTransport = this.idToTransport[transport.id]; if (existingTransport && typeof existingTransport[Symbol.dispose] === "function") existingTransport[Symbol.dispose](); } const allTransports = [...existingTransports.filter((t) => !newTransportIds.has(t.id)), ...newTransports]; this._config.transport = allTransports; for (const transport of newTransports) this.idToTransport[transport.id] = transport; this.hasMultipleTransports = allTransports.length > 1; this.singleTransport = this.hasMultipleTransports ? null : allTransports[0]; return this; } /** * Removes a transport by its ID. * * Transport changes only affect the current logger instance. Child loggers * created before the change will retain their original transports, and * parent loggers are not affected when a child modifies its transports. * * @returns true if the transport was found and removed, false otherwise. * * @see {@link https://loglayer.dev/logging-api/transport-management.html | Transport Management Docs} */ removeTransport(id) { const transport = this.idToTransport[id]; if (!transport) return false; if (typeof transport[Symbol.dispose] === "function") transport[Symbol.dispose](); delete this.idToTransport[id]; const remainingTransports = (Array.isArray(this._config.transport) ? this._config.transport : [this._config.transport]).filter((t) => t.id !== id); this._config.transport = remainingTransports.length === 1 ? remainingTransports[0] : remainingTransports; this.hasMultipleTransports = remainingTransports.length > 1; this.singleTransport = this.hasMultipleTransports ? null : remainingTransports[0] || null; return true; } /** * Replaces all existing plugins with new ones. * * When used with child loggers, it only affects the current logger instance * and does not modify the parent's plugins. * * @see {@link https://loglayer.dev/plugins/ | Plugins Docs} */ withFreshPlugins(plugins) { this._config.plugins = plugins; this.pluginManager = new PluginManager(plugins); return this; } withPluginManager(pluginManager) { this.pluginManager = pluginManager; return this; } /** * Logs only the error object without a log message * * @see {@link https://loglayer.dev/logging-api/error-handling.html | Error Handling Docs} */ errorOnly(error, opts) { const logLevel = opts?.logLevel || LogLevel$1.error; if (!this.isLevelEnabled(logLevel)) return; const { copyMsgOnOnlyError } = this._config; const formatLogConf = { logLevel, err: error }; if ((copyMsgOnOnlyError && opts?.copyMsg !== false || opts?.copyMsg === true) && error?.message) formatLogConf.params = [error.message]; this._formatLog(formatLogConf); } /** * Logs only metadata without a log message * * @see {@link https://loglayer.dev/logging-api/metadata.html | Metadata Docs} */ metadataOnly(metadata, logLevel = LogLevel$1.info) { if (!this.isLevelEnabled(logLevel)) return void 0; const { muteMetadata, consoleDebug } = this._config; if (muteMetadata) return; if (!metadata) { if (consoleDebug) console.debug("[LogLayer] metadataOnly was called with no metadata; dropping."); return; } let data = metadata; if (this.pluginManager.hasPlugins(PluginCallbackType$1.onMetadataCalled)) { data = this.pluginManager.runOnMetadataCalled(metadata, this); if (!data) { if (consoleDebug) console.debug("[LogLayer] Metadata was dropped due to plugin returning falsy value."); return; } } const config = { logLevel, metadata: data }; return this._formatLog(config); } /** * Sends a log message to the logging library under an info log level. * * Supports tagged template syntax: * ```typescript * log.info`User ${userId} logged in`; * ``` * * @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs} */ info(...args) { if (!this.isLevelEnabled(LogLevel$1.info)) return; const messages = resolveMessages(args); this._formatMessage(messages); this._formatLog({ logLevel: LogLevel$1.info, params: messages }); } /** * Sends a log message to the logging library under the warn log level * * Supports tagged template syntax: * ```typescript * log.warn`Request ${requestId} timed out`; * ``` * * @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs} */ warn(...args) { if (!this.isLevelEnabled(LogLevel$1.warn)) return; const messages = resolveMessages(args); this._formatMessage(messages); this._formatLog({ logLevel: LogLevel$1.warn, params: messages }); } /** * Sends a log message to the logging library under the error log level * * Supports tagged template syntax: * ```typescript * log.error`Failed to process ${taskId}`; * ``` * * @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs} */ error(...args) { if (!this.isLevelEnabled(LogLevel$1.error)) return; const messages = resolveMessages(args); this._formatMessage(messages); this._formatLog({ logLevel: LogLevel$1.error, params: messages }); } /** * Sends a log message to the logging library under the debug log level * * Supports tagged template syntax: * ```typescript * log.debug`Cache hit for ${cacheKey}`; * ``` * * @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs} */ debug(...args) { if (!this.isLevelEnabled(LogLevel$1.debug)) return; const messages = resolveMessages(args); this._formatMessage(messages); this._formatLog({ logLevel: LogLevel$1.debug, params: messages }); } /** * Sends a log message to the logging library under the trace log level * * Supports tagged template syntax: * ```typescript * log.trace`Entering ${functionName}`; * ``` * * @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs} */ trace(...args) { if (!this.isLevelEnabled(LogLevel$1.trace)) return; const messages = resolveMessages(args); this._formatMessage(messages); this._formatLog({ logLevel: LogLevel$1.trace, params: messages }); } /** * Sends a log message to the logging library under the fatal log level * * Supports tagged template syntax: * ```typescript * log.fatal`System crash: ${reason}`; * ``` * * @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs} */ fatal(...args) { if (!this.isLevelEnabled(LogLevel$1.fatal)) return; const messages = resolveMessages(args); this._formatMessage(messages); this._formatLog({ logLevel: LogLevel$1.fatal, params: messages }); } /** * Logs a raw log entry with complete control over all log parameters. * * This method allows you to bypass the normal LogLayer API and directly specify * all aspects of a log entry including log level, messages, metadata, and error. * It's useful for scenarios where you need to log structured data that doesn't * fit the standard LogLayer patterns, or when integrating with external logging * systems that provide pre-formatted log entries. * * The raw entry will still go through all LogLayer processing. * * @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs} */ raw(logEntry) { if (!this.isLevelEnabled(logEntry.logLevel)) return void 0; const formatLogConf = { logLevel: logEntry.logLevel, params: logEntry.messages, metadata: logEntry.metadata, rootData: logEntry.rootData, err: logEntry.error, context: logEntry.context }; this._formatMessage(logEntry.messages); return this._formatLog(formatLogConf); } /** * All logging inputs are dropped and stops sending logs to the logging library. * * @see {@link https://loglayer.dev/logging-api/basic-logging.html#enabling-disabling-logging | Enabling/Disabling Logging Docs} */ disableLogging() { this.logLevelManager.disableLogging(); return this; } /** * Enable sending logs to the logging library. * * @see {@link https://loglayer.dev/logging-api/basic-logging.html#enabling-disabling-logging | Enabling/Disabling Logging Docs} */ enableLogging() { this.logLevelManager.enableLogging(); return this; } /** * Disables inclusion of context data in the print * * @see {@link https://loglayer.dev/logging-api/context.html#managing-context | Managing Context Docs} */ muteContext() { this._config.muteContext = true; return this; } /** * Enables inclusion of context data in the print * * @see {@link https://loglayer.dev/logging-api/context.html#managing-context | Managing Context Docs} */ unMuteContext() { this._config.muteContext = false; return this; } /** * Disables inclusion of metadata in the print * * @see {@link https://loglayer.dev/logging-api/metadata.html#controlling-metadata-output | Controlling Metadata Output Docs} */ muteMetadata() { this._config.muteMetadata = true; return this; } /** * Enables inclusion of metadata in the print * * @see {@link https://loglayer.dev/logging-api/metadata.html#controlling-metadata-output | Controlling Metadata Output Docs} */ unMuteMetadata() { this._config.muteMetadata = false; return this; } /** * Enables a specific log level * * @see {@link https://loglayer.dev/logging-api/basic-logging.html#enabling-disabling-logging | Enabling/Disabling Logging Docs} */ enableIndividualLevel(logLevel) { this.logLevelManager.enableIndividualLevel(logLevel); return this; } /** * Disables a specific log level * * @see {@link https://loglayer.dev/logging-api/basic-logging.html#enabling-disabling-logging | Enabling/Disabling Logging Docs} */ disableIndividualLevel(logLevel) { this.logLevelManager.disableIndividualLevel(logLevel); return this; } /** * Sets the minimum log level to be used by the logger. Only messages with * this level or higher severity will be logged. * * For example, if you setLevel(LogLevel.warn), this will: * Enable: * - warn * - error * - fatal * Disable: * - info * - debug * - trace * * @see {@link https://loglayer.dev/logging-api/basic-logging.html#enabling-disabling-logging | Enabling/Disabling Logging Docs} */ setLevel(logLevel) { this.logLevelManager.setLevel(logLevel); return this; } /** * Checks if a specific log level is enabled * * @see {@link https://loglayer.dev/logging-api/basic-logging.html#checking-if-a-log-level-is-enabled | Checking if a Log Level is Enabled Docs} */ isLevelEnabled(logLevel) { return this.logLevelManager.isLevelEnabled(logLevel); } formatContext(context) { const { contextFieldName, muteContext } = this._config; if (context && Object.keys(context).length > 0 && !muteContext) { if (contextFieldName) return { [contextFieldName]: { ...context } }; return { ...context }; } return {}; } formatMetadata(data = null) { const { metadataFieldName, muteMetadata } = this._config; if (data && !muteMetadata) { if (metadataFieldName) return { [metadataFieldName]: { ...data } }; return { ...data }; } return {}; } /** * Returns a logger instance for a specific transport * * @see {@link https://loglayer.dev/logging-api/transport-management.html | Transport Management Docs} */ getLoggerInstance(id) { const transport = this.idToTransport[id]; if (!transport) return; return transport.getLoggerInstance(); } /** * Parses the LOGLAYER_GROUPS environment variable and overrides * _activeGroups and group levels accordingly. * Format: "name,name" or "name:level,name:level" */ _parseEnvGroups() { const envValue = typeof process !== "undefined" ? process.env?.LOGLAYER_GROUPS : void 0; if (!envValue) return; const entries = envValue.split(",").map((s) => s.trim()).filter(Boolean); const activeNames = []; for (const entry of entries) { const colonIdx = entry.indexOf(":"); if (colonIdx > 0) { const name = entry.slice(0, colonIdx); const level = entry.slice(colonIdx + 1); activeNames.push(name); if (this._groupsConfig?.[name]) this._groupsConfig[name] = { ...this._groupsConfig[name], level }; } else activeNames.push(entry); } this._activeGroups = new Set(activeNames); } /** * Merges per-log groups (from LogBuilder) with logger-level assigned groups. */ _mergeGroups(perLogGroups) { if (this._assignedGroups && perLogGroups) { const combined = new Set([...this._assignedGroups, ...perLogGroups]); return Array.from(combined); } return perLogGroups || this._assignedGroups; } /** * Applies ungrouped routing rules for a transport. */ _applyUngroupedRules(transportId) { if (this._ungroupedBehavior === "all") return true; if (this._ungroupedBehavior === "none") return false; return this._ungroupedBehavior.includes(transportId); } /** * Determines whether a transport should receive a log entry based on group routing rules. */ _shouldTransportReceiveLog(transportId, logLevel, groups) { if (!this._groupsConfig) return true; const effectiveGroups = this._mergeGroups(groups); if (!effectiveGroups || effectiveGroups.length === 0) return this._applyUngroupedRules(transportId); let hasAnyDefinedGroup = false; for (const groupName of effectiveGroups) { const groupConfig = this._groupsConfig[groupName]; if (!groupConfig) continue; hasAnyDefinedGroup = true; if (groupConfig.enabled === false) continue; if (this._activeGroups && !this._activeGroups.has(groupName)) continue; if (groupConfig.level) { const groupLevelPriority = LogLevelPriority[groupConfig.level]; if (LogLevelPriority[logLevel] < groupLevelPriority) continue; } if (groupConfig.transports.includes(transportId)) return true; } if (!hasAnyDefinedGroup) return this._applyUngroupedRules(transportId); return false; } _formatMessage(messages = []) { const { prefix } = this._config; if (prefix && typeof messages[0] === "string") messages[0] = `${prefix} ${messages[0]}`; } _formatLog({ logLevel, params = [], metadata = null, rootData = null, err, context = null, groups = null }) { const rawContext = context !== null ? context : this.contextManager.getContext(); let finalContextData; if (this._lazyContextCount > 0 || context !== null) { const contextResult = resolveLazyValues(rawContext); if (contextResult.errors) this._logLazyEvalErrors(contextResult.errors, "context"); const { resolved, asyncKeys } = replacePromiseValues(contextResult.resolved); if (asyncKeys) this._logAsyncLazyContextErrors(asyncKeys); finalContextData = resolved; } else finalContextData = rawContext; let metadataErrors = null; if (metadata) { const metadataResult = resolveLazyValues(metadata); metadata = metadataResult.resolved; metadataErrors = metadataResult.errors; } if (metadataErrors) this._logLazyEvalErrors(metadataErrors, "metadata"); if (metadata ? hasPromiseValues(metadata) : false) return this._resolveAsyncAndProcess(logLevel, params, finalContextData, metadata, rootData, err, context, groups); this._processLog(logLevel, params, finalContextData, metadata, rootData, err, context, groups); } /** * Resolves any Promise values in metadata (from async lazy callbacks) * and then processes the log entry. Context is already fully resolved. */ async _resolveAsyncAndProcess(logLevel, params, contextData, metadata, rootData, err, context, groups) { let resolvedMetadata = null; if (metadata) { const metadataResult = await resolvePromiseValues(metadata); resolvedMetadata = metadataResult.resolved; if (metadataResult.errors) this._logLazyEvalErrors(metadataResult.errors, "metadata"); } this._processLog(logLevel, params, contextData, resolvedMetadata, rootData, err, context, groups); } /** * Logs error entries for lazy evaluation failures. * Calls _processLog directly to bypass lazy evaluation and prevent recursion. */ _logLazyEvalErrors(failures, source) { if (this._isLoggingLazyError) { for (const f of failures) console.error(`[LogLayer] Lazy evaluation error in ${source} key "${f.key}":`, f.error); return; } this._isLoggingLazyError = true; try { for (const failure of failures) { const errorMessage = failure.error instanceof Error ? failure.error.message : String(failure.error); this._processLog(LogLevel$1.error, [`[LogLayer] Lazy evaluation failed for ${source} key "${failure.key}": ${errorMessage}`], {}, null, null, failure.error instanceof Error ? failure.error : void 0, {}); } } finally { this._isLoggingLazyError = false; } } /** * Logs error entries for async lazy values found in context. * Async lazy values are only supported in metadata, not context. */ _logAsyncLazyContextErrors(keys) { if (this._isLoggingLazyError) { for (const key of keys) console.error(`[LogLayer] Async lazy values are not supported in context (key "${key}"). Use async lazy only in metadata.`); return; } this._isLoggingLazyError = true; try { for (const key of keys) this._processLog(LogLevel$1.error, [`[LogLayer] Async lazy values are not supported in context (key "${key}"). Use async lazy only in metadata.`], {}, null, null, void 0, {}); } finally { this._isLoggingLazyError = false; } } /** * Processes a log entry after lazy values have been fully resolved. * Handles data assembly, plugins, and transport dispatch. */ _processLog(logLevel, params, contextData, metadata, rootData, err, context, groups = null) { const { errorSerializer, errorFieldInMetadata, muteContext, contextFieldName, metadataFieldName, errorFieldName, prefix } = this._config; const schema = { contextFieldName: contextFieldName ?? void 0, metadataFieldName: metadataFieldName ?? void 0, errorFieldName: errorFieldName ?? "err" }; const effectiveGroups = this._mergeGroups(groups) ?? void 0; let hasObjData = !!metadata || (muteContext ? false : context !== null ? Object.keys(context).length > 0 : this.contextManager.hasContextData()); let d = {}; if (hasObjData) if (contextFieldName && contextFieldName === metadataFieldName) { const formattedContextData = this.formatContext(contextData)[contextFieldName]; const updatedMetadata = this.formatMetadata(metadata)[metadataFieldName]; d = { [contextFieldName]: { ...formattedContextData, ...updatedMetadata } }; } else d = { ...this.formatContext(contextData), ...this.formatMetadata(metadata) }; if (err) { const serializedError = errorSerializer ? errorSerializer(err) : err; if (errorFieldInMetadata && metadata && metadataFieldName) if (d?.[metadataFieldName]) d[metadataFieldName][errorFieldName] = serializedError; else d = { ...d, [metadataFieldName]: { [errorFieldName]: serializedError } }; else if (errorFieldInMetadata && !metadata && metadataFieldName) d = { ...d, [metadataFieldName]: { [errorFieldName]: serializedError } }; else d = { ...d, [errorFieldName]: serializedError }; hasObjData = true; } if (rootData && Object.keys(rootData).length > 0) { d = { ...d, ...rootData }; hasObjData = true; } if (this.pluginManager.hasPlugins(PluginCallbackType$1.onBeforeDataOut)) { d = this.pluginManager.runOnBeforeDataOut({ data: hasObjData ? d : void 0, logLevel, error: err, metadata, context: contextData, groups: effectiveGroups, schema, prefix }, this); if (d && !hasObjData) hasObjData = true; } if (this.pluginManager.hasPlugins(PluginCallbackType$1.onBeforeMessageOut)) params = this.pluginManager.runOnBeforeMessageOut({ messages: [...params], logLevel, groups: effectiveGroups, schema, prefix }, this); if (this.pluginManager.hasPlugins(PluginCallbackType$1.transformLogLevel)) logLevel = this.pluginManager.runTransformLogLevel({ data: hasObjData ? d : void 0, logLevel, messages: [...params], error: err, metadata, context: contextData, groups: effectiveGroups, schema, prefix }, this); const sharedParams = { data: hasObjData ? d : void 0, error: err, metadata, context: contextData, groups: effectiveGroups, schema, prefix }; if (this.hasMultipleTransports) { const transportPromises = this._config.transport.filter((transport) => { if (!transport.enabled) return false; if (!this._shouldTransportReceiveLog(transport.id, logLevel, effectiveGroups)) return false; return true; }).map(async (transport) => { const currentLogLevel = logLevel; if (this.pluginManager.hasPlugins(PluginCallbackType$1.shouldSendToLogger)) { if (!this.pluginManager.runShouldSendToLogger({ messages: [...params], logLevel: currentLogLevel, transportId: transport.id, ...sharedParams }, this)) return; } return transport._sendToLogger({ logLevel: currentLogLevel, messages: [...params], hasData: hasObjData, ...sharedParams }); }); Promise.all(transportPromises).catch((err) => { if (this._config.consoleDebug) console.error("[LogLayer] Error executing transports:", err); }); } else { if (!this.singleTransport?.enabled) return; if (!this._shouldTransportReceiveLog(this.singleTransport.id, logLevel, effectiveGroups)) return; if (this.pluginManager.hasPlugins(PluginCallbackType$1.shouldSendToLogger)) { if (!this.pluginManager.runShouldSendToLogger({ messages: [...params], logLevel, transportId: this.singleTransport.id, ...sharedParams }, this)) return; } this.singleTransport._sendToLogger({ logLevel, messages: [...params], hasData: hasObjData, ...sharedParams }); } } }; //#endregion //#region src/MockLogBuilder.ts /** * A mock implementation of the ILogBuilder interface that does nothing. * Useful for writing unit tests. */ var MockLogBuilder = class { debug(..._args) {} error(..._args) {} info(..._args) {} trace(..._args) {} warn(..._args) {} fatal(..._args) {} enableLogging() { return this; } disableLogging() { return this; } withMetadata(_metadata) { return this; } withError(_error) { return this; } withGroup(_group) { return this; } }; //#endregion //#region src/MockLogLayer.ts /* istanbul ignore file */ /** * A mock implementation of the ILogLayer interface that does nothing. * Useful for writing unit tests. * MockLogLayer implements both ILogLayer and ILogBuilder for simplicity in testing. */ var MockLogLayer = class { mockLogBuilder = new MockLogBuilder(); mockContextManager = new MockContextManager(); mockLogLevelManager = new MockLogLevelManager(); info(..._args) {} warn(..._args) {} error(..._args) {} debug(..._args) {} trace(..._args) {} fatal(..._args) {} raw(_rawEntry) {} getLoggerInstance(_id) {} errorOnly(_error, _opts) {} metadataOnly(_metadata, _logLevel) {} addPlugins(_plugins) {} removePlugin(_id) {} enablePlugin(_id) {} disablePlugin(_id) {} withPrefix(_prefix) { return this; } withGroup(_group) { return this; } addGroup(_name, _config) { return this; } removeGroup(_name) { return this; } enableGroup(_name) { return this; } disableGroup(_name) { return this; } setGroupLevel(_name, _level) { return this; } setActiveGroups(_groups) { return this; } getGroups() { return {}; } withContext(_context) { return this; } withError(_error) { return this.mockLogBuilder.withError(_error); } withMetadata(_metadata) { return this.mockLogBuilder.withMetadata(_metadata); } getContext(_options) { return {}; } clearContext(_keys) { return this; } enableLogging() { return this; } disableLogging() { return this; } child() { return this; } muteContext() { return this; } unMuteContext() { return this; } muteMetadata() { return this; } unMuteMetadata() { return this; } withFreshTransports(_transports) { return this; } addTransport(_transports) { return this; } removeTransport(_id) { return true; } withFreshPlugins(_plugins) { return this; } withContextManager(_contextManager) { return this; } getContextManager() { return this.mockContextManager; } withLogLevelManager(_logLevelManager) { return this; } getLogLevelManager() { return this.mockLogLevelManager; } getConfig() { return {}; } /** * Sets the mock log builder to use for testing. */ setMockLogBuilder(mockLogBuilder) { this.mockLogBuilder = mockLogBuilder; } enableIndividualLevel(_logLevel) { return this; } disableIndividualLevel(_logLevel) { return this; } setLevel(_logLevel) { return this; } isLevelEnabled(_logLevel) { return true; } /** * Returns the mock log builder used for testing. */ getMockLogBuilder() { return this.mockLogBuilder; } /** * Resets the mock log builder to a new instance of MockLogBuilder. */ resetMockLogBuilder() { this.mockLogBuilder = new MockLogBuilder(); } }; //#endregion //#region src/types/mixin.types.ts /** * The class that the mixin extends */ let LogLayerMixinAugmentType = /* @__PURE__ */ function(LogLayerMixinAugmentType) { /** * Mixin extends the LogBuilder prototype */ LogLayerMixinAugmentType["LogBuilder"] = "LogBuilder"; /** * Mixin extends the LogLayer prototype */ LogLayerMixinAugmentType["LogLayer"] = "LogLayer"; return LogLayerMixinAugmentType; }({}); //#endregion //#region src/mixins.ts const mixinRegistry = { logLayerHandlers: [], pluginsToInit: [], logBuilderHandlers: [] }; /** * Adds one or more mixins to LogLayer. * @param mixin - The mixin(s) to register. Can be a single mixin registration or an array of mixin registrations. */ function useLogLayerMixin(mixin) { const mixins = Array.isArray(mixin) ? mixin : [mixin]; for (const mixinRegistration of mixins) { if (mixinRegistration.pluginsToAdd) for (const plugin of mixinRegistration.pluginsToAdd) mixinRegistry.pluginsToInit.push(plugin); for (const mixinToAdd of mixinRegistration.mixinsToAdd) switch (mixinToAdd.augmentationType) { case "LogLayer": if (mixinToAdd.onConstruct) mixinRegistry.logLayerHandlers.push({ onConstruct: mixinToAdd.onConstruct }); mixinToAdd.augment(LogLayer.prototype); mixinToAdd.augmentMock(MockLogLayer.prototype); break; case "LogBuilder": if (mixinToAdd.onConstruct) mixinRegistry.logBuilderHandlers.push({ onConstruct: mixinToAdd.onConstruct }); mixinToAdd.augment(LogBuilder.prototype); mixinToAdd.augmentMock(MockLogBuilder.prototype); break; } } } //#endregion //#region src/LogBuilder.ts /** * A class that contains methods to specify log metadata and an error and assembles * it to form a data object that can be passed into the transport. */ var LogBuilder = class { err; metadata; structuredLogger; hasMetadata; pluginManager; _groups = null; constructor(structuredLogger) { this.err = null; this.metadata = {}; this.structuredLogger = structuredLogger; this.hasMetadata = false; this.pluginManager = structuredLogger["pluginManager"]; if (mixinRegistry.logBuilderHandlers.length > 0) mixinRegistry.logBuilderHandlers.forEach((mixin) => { if (mixin.onConstruct) mixin.onConstruct(this, structuredLogger); }); } /** * Specifies metadata to include with the log message * * @see {@link https://loglayer.dev/logging-api/metadata.html | Metadata Docs} */ withMetadata(metadata) { const { pluginManager, structuredLogger: { _config: { consoleDebug } } } = this; if (!metadata) { if (consoleDebug) console.debug("[LogLayer] withMetadata was called with no metadata; dropping."); return this; } let data = metadata; if (pluginManager.hasPlugins(PluginCallbackType$1.onMetadataCalled)) { data = pluginManager.runOnMetadataCalled(metadata, this.structuredLogger); if (!data) { if (consoleDebug) console.debug("[LogLayer] Metadata was dropped due to plugin returning falsy value."); return this; } } this.metadata = { ...this.metadata, ...data }; this.hasMetadata = true; return this; } /** * Specifies an Error to include with the log message * * @see {@link https://loglayer.dev/logging-api/error-handling.html | Error Handling Docs} */ withError(error) { this.err = error; return this; } /** * Tags this log entry with one or more groups for routing. * * @see {@link https://loglayer.dev/logging-api/groups.html | Groups Docs} */ withGroup(group) { const newGroups = Array.isArray(group) ? group : [group]; if (this._groups) { const combined = new Set([...this._groups, ...newGroups]); this._groups = Array.from(combined); } else this._groups = [...newGroups]; return this; } /** * Sends a log message to the logging library under an info log level. * * Supports tagged template syntax: * ```typescript * log.withMetadata({ userId }).info`User ${userId} logged in`; * ``` */ info(...args) { if (!this.structuredLogger.isLevelEnabled(LogLevel$1.info)) return; const messages = resolveMessages(args); this.structuredLogger._formatMessage(messages); return this.formatLog(LogLevel$1.info, messages); } /** * Sends a log message to the logging library under the warn log level * * Supports tagged template syntax: * ```typescript * log.withMetadata({ requestId }).warn`Request ${requestId} timed out`; * ``` */ warn(...args) { if (!this.structuredLogger.isLevelEnabled(LogLevel$1.warn)) return; const messages = resolveMessages(args); this.structuredLogger._formatMessage(messages); return this.formatLog(LogLevel$1.warn, messages); } /** * Sends a log message to the logging library under the error log level * * Supports tagged template syntax: * ```typescript * log.withError(err).error`Failed to process ${taskId}`; * ``` */ error(...args) { if (!this.structuredLogger.isLevelEnabled(LogLevel$1.error)) return; const messages = resolveMessages(args); this.structuredLogger._formatMessage(messages); return this.formatLog(LogLevel$1.error, messages); } /** * Sends a log message to the logging library under the debug log level * * Supports tagged template syntax: * ```typescript * log.withMetadata({ cacheKey }).debug`Cache hit for ${cacheKey}`; * ``` */ debug(...args) { if (!this.structuredLogger.isLevelEnabled(LogLevel$1.debug)) return; const messages = resolveMessages(args); this.structuredLogger._formatMessage(messages); return this.formatLog(LogLevel$1.debug, messages); } /** * Sends a log message to the logging library under the trace log level * * Supports tagged template syntax: * ```typescript * log.withMetadata({ functionName }).trace`Entering ${functionName}`; * ``` */ trace(...args) { if (!this.structuredLogger.isLevelEnabled(LogLevel$1.trace)) return; const messages = resolveMessages(args); this.structuredLogger._formatMessage(messages); return this.formatLog(LogLevel$1.trace, messages); } /** * Sends a log message to the logging library under the fatal log level * * Supports tagged template syntax: * ```typescript * log.withError(err).fatal`System crash: ${reason}`; * ``` */ fatal(...args) { if (!this.structuredLogger.isLevelEnabled(LogLevel$1.fatal)) return; const messages = resolveMessages(args); this.structuredLogger._formatMessage(messages); return this.formatLog(LogLevel$1.fatal, messages); } /** * All logging inputs are dropped and stops sending logs to the logging library. */ disableLogging() { this.structuredLogger.disableLogging(); return this; } /** * Enable sending logs to the logging library. */ enableLogging() { this.structuredLogger.enableLogging(); return this; } formatLog(logLevel, params) { const { muteMetadata } = this.structuredLogger._config; const hasData = muteMetadata ? false : this.hasMetadata; return this.structuredLogger._formatLog({