loglayer
Version:
A modern logging library with a fluent API for specifying log messages, metadata and errors
1,373 lines (1,361 loc) • 42 kB
JavaScript
import { PluginCallbackType, PluginCallbackType as PluginCallbackType$1 } from "@loglayer/plugin";
import { LogLevel, LogLevel as LogLevel$1 } from "@loglayer/shared";
import { DefaultContextManager, MockContextManager } from "@loglayer/context-manager";
import { DefaultLogLevelManager, MockLogLevelManager } from "@loglayer/log-level-manager";
import { BaseTransport, LogLevelPriority, 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
}, 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 };
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
}, loglayer);
if (result) {
if (!initialData.data) initialData.data = {};
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
}, 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;
/**
* 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();
this.pluginManager = new PluginManager([...config.plugins || [], ...mixinRegistry.pluginsToInit]);
if (!this._config.errorFieldName) this._config.errorFieldName = "err";
if (!this._config.copyMsgOnOnlyError) this._config.copyMsgOnOnlyError = false;
this._initializeTransports(this._config.transport);
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;
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;
}
/**
* 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;
}
}
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) {
this.contextManager.clearContext(keys);
return this;
}
getContext() {
return this.contextManager.getContext();
}
/**
* 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
});
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;
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
};
this._formatLog(config);
}
/**
* Sends a log message to the logging library under an info log level.
*
* The logging library may or may not support multiple message parameters and only
* the first parameter would be used.
*
* @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs}
*/
info(...messages) {
if (!this.isLevelEnabled(LogLevel$1.info)) return;
this._formatMessage(messages);
this._formatLog({
logLevel: LogLevel$1.info,
params: messages
});
}
/**
* Sends a log message to the logging library under the warn log level
*
* @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs}
*/
warn(...messages) {
if (!this.isLevelEnabled(LogLevel$1.warn)) return;
this._formatMessage(messages);
this._formatLog({
logLevel: LogLevel$1.warn,
params: messages
});
}
/**
* Sends a log message to the logging library under the error log level
*
* @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs}
*/
error(...messages) {
if (!this.isLevelEnabled(LogLevel$1.error)) return;
this._formatMessage(messages);
this._formatLog({
logLevel: LogLevel$1.error,
params: messages
});
}
/**
* Sends a log message to the logging library under the debug log level
*
* @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs}
*/
debug(...messages) {
if (!this.isLevelEnabled(LogLevel$1.debug)) return;
this._formatMessage(messages);
this._formatLog({
logLevel: LogLevel$1.debug,
params: messages
});
}
/**
* Sends a log message to the logging library under the trace log level
*
* @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs}
*/
trace(...messages) {
if (!this.isLevelEnabled(LogLevel$1.trace)) return;
this._formatMessage(messages);
this._formatLog({
logLevel: LogLevel$1.trace,
params: messages
});
}
/**
* Sends a log message to the logging library under the fatal log level
*
* @see {@link https://loglayer.dev/logging-api/basic-logging.html | Basic Logging Docs}
*/
fatal(...messages) {
if (!this.isLevelEnabled(LogLevel$1.fatal)) return;
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;
const formatLogConf = {
logLevel: logEntry.logLevel,
params: logEntry.messages,
metadata: logEntry.metadata,
err: logEntry.error,
context: logEntry.context
};
this._formatMessage(logEntry.messages);
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();
}
_formatMessage(messages = []) {
const { prefix } = this._config;
if (prefix && typeof messages[0] === "string") messages[0] = `${prefix} ${messages[0]}`;
}
_formatLog({ logLevel, params = [], metadata = null, err, context = null }) {
const { errorSerializer, errorFieldInMetadata, muteContext, contextFieldName, metadataFieldName, errorFieldName } = this._config;
const contextData = context !== null ? context : this.contextManager.getContext();
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 (this.pluginManager.hasPlugins(PluginCallbackType$1.onBeforeDataOut)) {
d = this.pluginManager.runOnBeforeDataOut({
data: hasObjData ? d : void 0,
logLevel,
error: err,
metadata,
context: contextData
}, this);
if (d && !hasObjData) hasObjData = true;
}
if (this.pluginManager.hasPlugins(PluginCallbackType$1.onBeforeMessageOut)) params = this.pluginManager.runOnBeforeMessageOut({
messages: [...params],
logLevel
}, 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
}, this);
if (this.hasMultipleTransports) {
const transportPromises = this._config.transport.filter((transport) => transport.enabled).map(async (transport) => {
const currentLogLevel = logLevel;
if (this.pluginManager.hasPlugins(PluginCallbackType$1.shouldSendToLogger)) {
if (!this.pluginManager.runShouldSendToLogger({
messages: [...params],
data: hasObjData ? d : void 0,
logLevel: currentLogLevel,
transportId: transport.id,
error: err,
metadata,
context: contextData
}, this)) return;
}
return transport._sendToLogger({
logLevel: currentLogLevel,
messages: [...params],
data: hasObjData ? d : void 0,
hasData: hasObjData,
error: err,
metadata,
context: contextData
});
});
Promise.all(transportPromises).catch((err$1) => {
if (this._config.consoleDebug) console.error("[LogLayer] Error executing transports:", err$1);
});
} else {
if (!this.singleTransport?.enabled) return;
if (this.pluginManager.hasPlugins(PluginCallbackType$1.shouldSendToLogger)) {
if (!this.pluginManager.runShouldSendToLogger({
messages: [...params],
data: hasObjData ? d : void 0,
logLevel,
transportId: this.singleTransport.id,
error: err,
metadata,
context: contextData
}, this)) return;
}
this.singleTransport._sendToLogger({
logLevel,
messages: [...params],
data: hasObjData ? d : void 0,
hasData: hasObjData,
error: err,
metadata,
context: contextData
});
}
}
};
//#endregion
//#region src/MockLogBuilder.ts
/**
* A mock implementation of the ILogBuilder interface that does nothing.
* Useful for writing unit tests.
*/
var MockLogBuilder = class {
debug(..._messages) {}
error(..._messages) {}
info(..._messages) {}
trace(..._messages) {}
warn(..._messages) {}
fatal(..._messages) {}
enableLogging() {
return this;
}
disableLogging() {
return this;
}
withMetadata(_metadata) {
return this;
}
withError(_error) {
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(..._messages) {}
warn(..._messages) {}
error(..._messages) {}
debug(..._messages) {}
trace(..._messages) {}
fatal(..._messages) {}
raw(_rawEntry) {}
getLoggerInstance(_id) {}
errorOnly(_error, _opts) {}
metadataOnly(_metadata, _logLevel) {}
addPlugins(_plugins) {}
removePlugin(_id) {}
enablePlugin(_id) {}
disablePlugin(_id) {}
withPrefix(_prefix) {
return this;
}
withContext(_context) {
return this;
}
withError(_error) {
return this.mockLogBuilder.withError(_error);
}
withMetadata(_metadata) {
return this.mockLogBuilder.withMetadata(_metadata);
}
getContext() {
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$1) {
/**
* Mixin extends the LogBuilder prototype
*/
LogLayerMixinAugmentType$1["LogBuilder"] = "LogBuilder";
/**
* Mixin extends the LogLayer prototype
*/
LogLayerMixinAugmentType$1["LogLayer"] = "LogLayer";
return LogLayerMixinAugmentType$1;
}({});
//#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 LogLayerMixinAugmentType.LogLayer:
if (mixinToAdd.onConstruct) mixinRegistry.logLayerHandlers.push({ onConstruct: mixinToAdd.onConstruct });
mixinToAdd.augment(LogLayer.prototype);
mixinToAdd.augmentMock(MockLogLayer.prototype);
break;
case LogLayerMixinAugmentType.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;
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;
}
/**
* Sends a log message to the logging library under an info log level.
*/
info(...messages) {
if (!this.structuredLogger.isLevelEnabled(LogLevel$1.info)) return;
this.structuredLogger._formatMessage(messages);
this.formatLog(LogLevel$1.info, messages);
}
/**
* Sends a log message to the logging library under the warn log level
*/
warn(...messages) {
if (!this.structuredLogger.isLevelEnabled(LogLevel$1.warn)) return;
this.structuredLogger._formatMessage(messages);
this.formatLog(LogLevel$1.warn, messages);
}
/**
* Sends a log message to the logging library under the error log level
*/
error(...messages) {
if (!this.structuredLogger.isLevelEnabled(LogLevel$1.error)) return;
this.structuredLogger._formatMessage(messages);
this.formatLog(LogLevel$1.error, messages);
}
/**
* Sends a log message to the logging library under the debug log level
*
* The logging library may or may not support multiple message parameters and only
* the first parameter would be used.
*/
debug(...messages) {
if (!this.structuredLogger.isLevelEnabled(LogLevel$1.debug)) return;
this.structuredLogger._formatMessage(messages);
this.formatLog(LogLevel$1.debug, messages);
}
/**
* Sends a log message to the logging library under the trace log level
*
* The logging library may or may not support multiple message parameters and only
* the first parameter would be used.
*/
trace(...messages) {
if (!this.structuredLogger.isLevelEnabled(LogLevel$1.trace)) return;
this.structuredLogger._formatMessage(messages);
this.formatLog(LogLevel$1.trace, messages);
}
/**
* Sends a log message to the logging library under the fatal log level
*
* The logging library may or may not support multiple message parameters and only
* the first parameter would be used.
*/
fatal(...messages) {
if (!this.structuredLogger.isLevelEnabled(LogLevel$1.fatal)) return;
this.structuredLogger._formatMessage(messages);
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;
this.structuredLogger._formatLog({
logLevel,
params,
metadata: hasData ? this.metadata : null,
err: this.err
});
}
};
//#endregion
//#region src/TestLoggingLibrary.ts
/**
* A test logging library that can be used to test LogLayer plugins and transports.
* It is meant to be used with the TestTransport.
*/
var TestLoggingLibrary = class {
/**
* An array of log lines that have been logged.
*/
lines;
constructor() {
this.lines = [];
}
info(...params) {
this.addLine(LogLevel$1.info, params);
}
warn(...params) {
this.addLine(LogLevel$1.warn, params);
}
error(...params) {
this.addLine(LogLevel$1.error, params);
}
debug(...params) {
this.addLine(LogLevel$1.debug, params);
}
trace(...params) {
this.addLine(LogLevel$1.trace, params);
}
fatal(...params) {
this.addLine(LogLevel$1.fatal, params);
}
addLine(logLevel, params) {
this.lines.push({
level: logLevel,
data: params
});
}
/**
* Get the last line that was logged. Returns null if no lines have been logged.
*/
getLastLine() {
if (!this.lines.length) return null;
return this.lines[this.lines.length - 1];
}
/**
* Pops the last line that was logged. Returns null if no lines have been logged.
*/
popLine() {
return this.lines.pop();
}
/**
* Clears all lines that have been logged.
*/
clearLines() {
this.lines = [];
}
};
//#endregion
//#region src/transports/BlankTransport.ts
/**
* A transport that allows users to quickly create custom transports by providing their own `shipToLogger` function.
*
* This is useful for creating simple custom transports without having to create a completely new transport class from scratch.
*
* @example
* ```typescript
* import { LogLayer, BlankTransport } from 'loglayer';
*
* const log = new LogLayer({
* transport: new BlankTransport({
* shipToLogger: ({ logLevel, messages, data, hasData }) => {
* // Your custom logging logic here
* console.log(`[${logLevel}]`, ...messages, data && hasData ? data : '');
* return messages;
* }
* })
* });
* ```
*/
var BlankTransport = class extends LoggerlessTransport {
shipToLoggerFn;
constructor(config) {
super(config);
this.shipToLoggerFn = config.shipToLogger;
}
shipToLogger(params) {
return this.shipToLoggerFn(params);
}
};
//#endregion
//#region src/transports/ConsoleTransport.ts
/**
* Transport for use with a console logger.
*/
var ConsoleTransport = class extends BaseTransport {
appendObjectData;
logLevel;
messageField;
dateField;
levelField;
dateFn;
levelFn;
stringify;
messageFn;
constructor(params) {
super(params);
this.appendObjectData = params.appendObjectData || false;
this.logLevel = params.level ?? "trace";
this.messageField = params.messageField;
this.dateField = params.dateField;
this.levelField = params.levelField;
this.dateFn = params.dateFn;
this.levelFn = params.levelFn;
this.stringify = params.stringify || false;
this.messageFn = params.messageFn;
}
shipToLogger(params) {
const { logLevel, data, hasData } = params;
let { messages } = params;
if (LogLevelPriority[logLevel] < LogLevelPriority[this.logLevel]) return;
if (this.messageFn) messages = [this.messageFn(params)];
if (this.messageField) {
const messageText = messages.join(" ");
const logObject = {
...data || {},
[this.messageField]: messageText,
...this.dateField && { [this.dateField]: this.dateFn ? this.dateFn() : (/* @__PURE__ */ new Date()).toISOString() },
...this.levelField && { [this.levelField]: this.levelFn ? this.levelFn(logLevel) : logLevel }
};
messages = [this.stringify ? JSON.stringify(logObject) : logObject];
} else if (this.dateField || this.levelField) {
const logObject = {
...data || {},
...this.dateField && { [this.dateField]: this.dateFn ? this.dateFn() : (/* @__PURE__ */ new Date()).toISOString() },
...this.levelField && { [this.levelField]: this.levelFn ? this.levelFn(logLevel) : logLevel }
};
if (this.stringify) messages.push(JSON.stringify(logObject));
else messages.push(logObject);
} else if (data && hasData) if (this.appendObjectData) messages.push(data);
else messages.unshift(data);
switch (logLevel) {
case LogLevel$1.info:
this.logger.info(...messages);
break;
case LogLevel$1.warn:
this.logger.warn(...messages);
break;
case LogLevel$1.error:
this.logger.error(...messages);
break;
case LogLevel$1.trace:
this.logger.trace(...messages);
break;
case LogLevel$1.debug:
this.logger.debug(...messages);
break;
case LogLevel$1.fatal:
this.logger.error(...messages);
break;
}
return messages;
}
};
//#endregion
//#region src/transports/TestTransport.ts
/**
* Transport used for testing purposes.
*/
var TestTransport = class extends BaseTransport {
shipToLogger({ logLevel, messages, data, hasData }) {
if (data && hasData) messages.unshift(data);
switch (logLevel) {
case LogLevel$1.info:
this.logger.info(...messages);
break;
case LogLevel$1.warn:
this.logger.warn(...messages);
break;
case LogLevel$1.error:
this.logger.error(...messages);
break;
case LogLevel$1.trace:
this.logger.trace(...messages);
break;
case LogLevel$1.debug:
this.logger.debug(...messages);
break;
case LogLevel$1.fatal:
this.logger.fatal(...messages);
break;
}
return messages;
}
};
//#endregion
export { BlankTransport, ConsoleTransport, LogBuilder, LogLayer, LogLayerMixinAugmentType, LogLevel, MockLogBuilder, MockLogLayer, PluginCallbackType, TestLoggingLibrary, TestTransport, useLogLayerMixin };