"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { MikroLog: () => MikroLog }); module.exports = __toCommonJS(src_exports); // src/entities/MikroLog.ts var import_node_crypto = require("crypto"); var import_aws_metadata_utils = require("aws-metadata-utils"); var MikroLog = class _MikroLog { static instance; static metadataConfig = {}; static event = {}; static context = {}; static correlationId; static debugSamplingLevel; static isDebugLogSampled; static isColdStart = true; nextLogEnrichment; constructor() { _MikroLog.metadataConfig = {}; _MikroLog.event = {}; _MikroLog.context = {}; _MikroLog.correlationId = ""; _MikroLog.debugSamplingLevel = this.initDebugSampleLevel(); _MikroLog.isDebugLogSampled = true; this.nextLogEnrichment = {}; } /** * @description This instantiates `MikroLog`. In order to be able * to "remember" event and context we use a singleton pattern to * reuse the same logical instance. * * If the `start` method receives any input, that input will * overwrite any existing metadata, event, and context. * * It will also, consequently, wipe the Lambda cold start state. * * If you want to "add" to these, you should instead call * `enrich()` and pass in your additional data there. */ static start(input) { if (!_MikroLog.instance) _MikroLog.instance = new _MikroLog(); _MikroLog.metadataConfig = input?.metadataConfig || this.metadataConfig; _MikroLog.event = input?.event || this.event; _MikroLog.context = input?.context || this.context; _MikroLog.context.isColdStart = _MikroLog.getColdStart(); _MikroLog.correlationId = input?.correlationId || this.correlationId || process.env.CORRELATION_ID || ""; return _MikroLog.instance; } /** * @description Is this a Lambda cold start? */ static getColdStart() { if (_MikroLog.isColdStart) { _MikroLog.isColdStart = false; return true; } return false; } /** * @description An emergency mechanism if you absolutely need to * reset the instance to its empty default state. */ static reset() { _MikroLog.instance = new _MikroLog(); } /** * @description Enrich MikroLog with metadata, AWS Lambda event and/or context. */ static enrich(input) { _MikroLog.metadataConfig = Object.assign( _MikroLog.metadataConfig, input.metadataConfig || {} ); _MikroLog.event = Object.assign(_MikroLog.event, input.event || {}); _MikroLog.context = Object.assign(_MikroLog.context, input.context || {}); _MikroLog.correlationId = input?.correlationId || this.correlationId || process.env.CORRELATION_ID || ""; } /** * @description If you want a one-time root-level enrichment, you can do: * * ``` * const logger = MikroLog.start(); * logger.enrichNext({ someId: '123456789abcdefghi' }); * logger.info('Ping!'); // Enrichment is present on log * logger.info('Ping!'); // Enrichment is no longer present * ``` * * You can also use nested objects: * ``` * logger.enrichNext({ myObject: { myValue: 'Something here', otherValue: 'Something else' } }); * ``` */ enrichNext(input) { this.nextLogEnrichment = input; } /** * @description Set correlation ID manually, for example for use in cross-boundary calls. * * This value will be propagated to all future logs. */ setCorrelationId(correlationId) { _MikroLog.correlationId = correlationId; } /** * @description Set sampling rate of `DEBUG` logs. */ setDebugSamplingRate(samplingPercent) { let fixedValue = samplingPercent; if (typeof samplingPercent !== "number") return _MikroLog.debugSamplingLevel; if (samplingPercent < 0) fixedValue = 0; if (samplingPercent > 100) fixedValue = 100; _MikroLog.debugSamplingLevel = fixedValue; return fixedValue; } /** * @description Check if MikroLog has sampled the last log. * Will only return true value _after_ having output an actual `DEBUG` log. */ isDebugLogSampled() { return _MikroLog.isDebugLogSampled; } /** * @description Output a `DEBUG` log. Message may be string or object. * This will respect whatever sampling rate is currently set. * @example logger.debug('My message!'); */ debug(message, httpStatusCode) { const createdLog = this.createLog({ message, level: "DEBUG", httpStatusCode: httpStatusCode || 200 }); if (this.shouldSampleLog()) this.writeLog(createdLog); return createdLog; } /** * @description Alias for `Logger.log()`. Message may be string or object. * @example logger.info('My message!'); */ info(message, httpStatusCode) { return this.log(message, httpStatusCode); } /** * @description Output an informational-level log. Message may be string or object. * @example logger.log('My message!'); */ log(message, httpStatusCode) { const createdLog = this.createLog({ message, level: "INFO", httpStatusCode: httpStatusCode || 200 }); this.writeLog(createdLog); return createdLog; } /** * @description Output a warning-level log. Message may be string or object. * @example logger.warn('My message!'); */ warn(message, httpStatusCode) { const createdLog = this.createLog({ message, level: "WARN", httpStatusCode: httpStatusCode || 200 }); this.writeLog(createdLog); return createdLog; } /** * @description Output an error-level log. Message may be string or object. * @example logger.error('My message!'); */ error(message, httpStatusCode) { const createdLog = this.createLog({ message, level: "ERROR", httpStatusCode: httpStatusCode || 400 }); this.writeLog(createdLog); return createdLog; } /** * @description Initialize the debug sample rate. * Only accepts numbers or strings that can convert to numbers. * The default is to use all `DEBUG` logs (i.e. `100` percent). */ initDebugSampleLevel() { const envValue = process.env.MIKROLOG_SAMPLE_RATE; if (envValue) { const isNumeric = !Number.isNaN(envValue) && !Number.isNaN(Number.parseFloat(envValue)); if (isNumeric) return Number.parseFloat(envValue); } return 100; } /** * @description Get dynamic metadata. */ produceDynamicMetadata() { const dynamicMetadata = this.getDynamicMetadata(); const timeNow = Date.now(); const metadata = { id: (0, import_node_crypto.randomUUID)(), timestamp: new Date(timeNow).toISOString(), timestampEpoch: `${timeNow}`, ...dynamicMetadata }; return this.filterMetadata(metadata); } /** * @description Use `aws-metadata-utils` to get dynamic metadata. * Restore manually-set `correlationId` if we have one. */ getDynamicMetadata() { const metadata = (0, import_aws_metadata_utils.getMetadata)(_MikroLog.event, _MikroLog.context); return { ...metadata, correlationId: _MikroLog.correlationId || metadata.correlationId }; } /** * @description Filter metadata from empties. */ filterMetadata(metadata) { const filteredMetadata = {}; Object.entries(metadata).forEach((entry) => { const [key, value] = entry; if (value || value === false || value === 0) filteredMetadata[key] = value; }); return filteredMetadata; } /** * @description Utility to check if a log should be sampled (written) based * on the currently set `debugSamplingLevel`. This uses a 0-100 scale. * * If the random number is lower than (or equal to) the sampling level, * then we may sample the log. */ shouldSampleLog() { const logWillBeSampled = Math.random() * 100 <= _MikroLog.debugSamplingLevel; _MikroLog.isDebugLogSampled = logWillBeSampled; return logWillBeSampled; } /** * @description Call `STDOUT` to write the log. */ writeLog(createdLog) { process.stdout.write(`${JSON.stringify(createdLog)} `); } /** * @description Create the log envelope. */ createLog(log) { const staticMetadata = _MikroLog.metadataConfig; const redactedKeys = staticMetadata.redactedKeys ? staticMetadata.redactedKeys : void 0; const maskedValues = staticMetadata.maskedValues ? staticMetadata.maskedValues : void 0; if (redactedKeys) delete staticMetadata.redactedKeys; if (maskedValues) delete staticMetadata.maskedValues; const dynamicMetadata = this.produceDynamicMetadata(); const logOutput = (() => { const output = { ...dynamicMetadata, ...staticMetadata, message: log.message, error: log.level === "ERROR", level: log.level, httpStatusCode: log.httpStatusCode, isColdStart: _MikroLog.context.isColdStart }; if (this.nextLogEnrichment && JSON.stringify(this.nextLogEnrichment) !== "{}") return Object.assign(output, this.nextLogEnrichment); return output; })(); this.nextLogEnrichment = {}; const filteredOutput = this.filterOutput( logOutput, redactedKeys, maskedValues ); return this.sortOutput(filteredOutput); } /** * @description This filters, redacts, and masks the log prior to actual output. */ filterOutput(logOutput, redactedKeys, maskedValues) { const filteredOutput = {}; const processEntry = (key, value, path = []) => { const fullPath = [...path, key].join("."); if (redactedKeys?.includes(fullPath)) return; if (maskedValues?.includes(fullPath)) { this.setNestedValue(filteredOutput, path, key, "MASKED"); return; } if (typeof value === "object" && value !== null && !Array.isArray(value)) { Object.entries(value).forEach( ([nestedKey, nestedValue]) => processEntry(nestedKey, nestedValue, [...path, key]) ); } else { if (value || value === 0 || value === false) { if (path.length) { this.setNestedValue(filteredOutput, path, key, value); } else { filteredOutput[key] = value; } } } }; Object.entries(logOutput).forEach( ([key, value]) => processEntry(key, value) ); return filteredOutput; } /** * Utility function to set a nested value in an object. */ setNestedValue(target, path, key, value) { let current = target; for (let i = 0; i < path.length; i++) { const segment = path[i]; if (!current[segment] || typeof current[segment] !== "object") { current[segment] = {}; } current = current[segment]; } current[key] = value; } /** * @description Alphabetically sort the fields in the log object. */ sortOutput(input) { const sortedOutput = {}; Object.entries(input).sort().forEach(([key, value]) => sortedOutput[key] = value); return sortedOutput; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { MikroLog });