UNPKG

emitnlog

Version:

Emit n' Log: a modern, type-safe library for logging, event notifications, and observability in JavaScript/TypeScript apps.

771 lines (757 loc) 24.6 kB
'use strict'; // src/logger/off-logger.ts var OFF_LOGGER = { get level() { return "off"; }, set level(_) { }, args: () => OFF_LOGGER, trace: () => void 0, t: () => void 0, debug: () => void 0, d: () => void 0, info: () => void 0, i: () => void 0, notice: () => void 0, n: () => void 0, warning: () => void 0, w: () => void 0, error: () => void 0, e: () => void 0, critical: () => void 0, c: () => void 0, alert: () => void 0, a: () => void 0, emergency: () => void 0, em: () => void 0, log: () => void 0 }; // src/utils/common/is-not-nullable.ts var isNotNullable = (value) => value !== void 0 && value !== null; // src/logger/level-utils.ts var toLevelWeight = (level) => { switch (level) { case "trace": return 8; case "debug": return 7; case "info": return 6; case "notice": return 5; case "warning": return 4; case "error": return 3; case "critical": return 2; case "alert": return 1; case "emergency": return 0; default: return 20; } }; var shouldEmitEntry = (level, entryLevel) => level !== "off" && toLevelWeight(entryLevel) <= toLevelWeight(level); // src/logger/prefixed-logger.ts var prefixSymbol = Symbol.for("@emitnlog/logger/prefix"); var separatorSymbol = Symbol.for("@emitnlog/logger/separator"); var dataSymbol = Symbol.for("@emitnlog/logger/data"); var withPrefix = (logger, prefix, options) => { if (logger === OFF_LOGGER) { return OFF_LOGGER; } let prefixSeparator; let messageSeparator; const data = inspectPrefixedLogger(logger); if (data) { logger = data.rootLogger; prefixSeparator = data.separator; messageSeparator = data.messageSeparator; prefix = prefix ? `${data.prefix}${prefixSeparator}${prefix}` : data.prefix; } else { prefixSeparator = options?.prefixSeparator ?? "."; messageSeparator = options?.messageSeparator ?? ": "; if (options?.fallbackPrefix) { prefix = prefix ? `${options.fallbackPrefix}${prefixSeparator}${prefix}` : options.fallbackPrefix; } } const prefixedLogger = { [prefixSymbol]: prefix, [separatorSymbol]: prefixSeparator, [dataSymbol]: { rootLogger: logger, messageSeparator }, get level() { return logger.level; }, set level(value) { logger.level = value; }, args(...args) { logger.args(...args); return prefixedLogger; }, trace(message, ...args) { if (shouldEmitEntry(logger.level, "trace")) { logger.trace(toMessageProvider(prefixedLogger, message), ...args); } }, t(strings, ...values) { if (shouldEmitEntry(logger.level, "trace")) { logger.t(prefixTemplateString(prefixedLogger, strings), ...values); } }, debug(message, ...args) { if (shouldEmitEntry(logger.level, "debug")) { logger.debug(toMessageProvider(prefixedLogger, message), ...args); } }, d(strings, ...values) { if (shouldEmitEntry(logger.level, "debug")) { logger.d(prefixTemplateString(prefixedLogger, strings), ...values); } }, info(message, ...args) { if (shouldEmitEntry(logger.level, "info")) { logger.info(toMessageProvider(prefixedLogger, message), ...args); } }, i(strings, ...values) { if (shouldEmitEntry(logger.level, "info")) { logger.i(prefixTemplateString(prefixedLogger, strings), ...values); } }, notice(message, ...args) { if (shouldEmitEntry(logger.level, "notice")) { logger.notice(toMessageProvider(prefixedLogger, message), ...args); } }, n(strings, ...values) { if (shouldEmitEntry(logger.level, "notice")) { logger.n(prefixTemplateString(prefixedLogger, strings), ...values); } }, warning(message, ...args) { if (shouldEmitEntry(logger.level, "warning")) { logger.warning(toMessageProvider(prefixedLogger, message), ...args); } }, w(strings, ...values) { if (shouldEmitEntry(logger.level, "warning")) { logger.w(prefixTemplateString(prefixedLogger, strings), ...values); } }, error(error, ...args) { if (shouldEmitEntry(logger.level, "error")) { if (error instanceof Error) { logger.error(toMessageProvider(prefixedLogger, error.message), error, ...args); } else if (error && typeof error === "object" && "error" in error) { logger.error(toMessageProvider(prefixedLogger, String(error.error)), error, ...args); } else { logger.error(toMessageProvider(prefixedLogger, error), ...args); } } }, e(strings, ...values) { if (shouldEmitEntry(logger.level, "error")) { logger.e(prefixTemplateString(prefixedLogger, strings), ...values); } }, critical(message, ...args) { if (shouldEmitEntry(logger.level, "critical")) { logger.critical(toMessageProvider(prefixedLogger, message), ...args); } }, c(strings, ...values) { if (shouldEmitEntry(logger.level, "critical")) { logger.c(prefixTemplateString(prefixedLogger, strings), ...values); } }, alert(message, ...args) { if (shouldEmitEntry(logger.level, "alert")) { logger.alert(toMessageProvider(prefixedLogger, message), ...args); } }, a(strings, ...values) { if (shouldEmitEntry(logger.level, "alert")) { logger.a(prefixTemplateString(prefixedLogger, strings), ...values); } }, emergency(message, ...args) { if (shouldEmitEntry(logger.level, "emergency")) { logger.emergency(toMessageProvider(prefixedLogger, message), ...args); } }, em(strings, ...values) { if (shouldEmitEntry(logger.level, "emergency")) { logger.em(prefixTemplateString(prefixedLogger, strings), ...values); } }, log(level, message, ...args) { if (shouldEmitEntry(logger.level, level)) { logger.log(level, toMessageProvider(prefixedLogger, message), ...args); } } }; return prefixedLogger; }; var appendPrefix = (logger, prefix) => withPrefix(logger, prefix); var isPrefixedLogger = (logger) => isNotNullable(logger) && prefixSymbol in logger && typeof logger[prefixSymbol] === "string" && dataSymbol in logger; var inspectPrefixedLogger = (logger) => isPrefixedLogger(logger) ? { rootLogger: logger[dataSymbol].rootLogger, prefix: logger[prefixSymbol], separator: logger[separatorSymbol], messageSeparator: logger[dataSymbol].messageSeparator } : void 0; var prefixTemplateString = (prefixLogger, strings) => { const prefix = prefixLogger[prefixSymbol]; const messageSeparator = prefixLogger[dataSymbol].messageSeparator; const newStrings = Array.from(strings); newStrings[0] = `${prefix}${messageSeparator}${newStrings[0]}`; const prefixedStrings = Object.assign(newStrings, { raw: Array.from(strings.raw) }); prefixedStrings.raw[0] = `${prefix}${messageSeparator}${prefixedStrings.raw[0]}`; return prefixedStrings; }; var toMessageProvider = (prefixLogger, message) => () => { const messageString = typeof message === "function" ? message() : message; const messageSeparator = prefixLogger[dataSymbol].messageSeparator; return `${prefixLogger[prefixSymbol]}${messageSeparator}${messageString}`; }; // src/utils/async/deferred-value.ts var createDeferredValue = () => { let resolve; let reject; let resolved = false; let rejected = false; const createPromise = () => new Promise((res, rej) => { resolve = (value) => { if (resolved || rejected) { return; } resolved = true; res(value); }; reject = (reason) => { if (resolved || rejected) { return; } rejected = true; rej(reason); }; }); let promise = createPromise(); const deferred = { get promise() { return promise; }, get resolved() { return resolved; }, get rejected() { return rejected; }, get settled() { return resolved || rejected; }, resolve: (value) => { resolve(value); }, reject: (reason) => { reject(reason); }, renew: () => { if (deferred.settled) { resolved = false; rejected = false; promise = createPromise(); } return deferred; } }; return deferred; }; // src/utils/common/closed-error.ts var ClosedError = class extends Error { constructor(message = "the operation was performed after its scope was closed") { super(message); Object.setPrototypeOf(this, new.target.prototype); this.name = "ClosedError"; } }; // src/notifier/implementation.ts var createEventNotifier = (options) => { const listeners = /* @__PURE__ */ new Set(); let errorHandler; let deferredEvent; const basicNotify = (event) => { if (!listeners.size && !deferredEvent) { return; } const value = typeof event === "function" ? event() : event; for (const listener of listeners) { try { void listener(value); } catch (error) { if (errorHandler) { try { errorHandler(error); } catch { } } } } if (deferredEvent) { deferredEvent.resolve(value); deferredEvent = void 0; } }; const notify = basicNotify; return { close: () => { listeners.clear(); if (deferredEvent) { deferredEvent.reject(new ClosedError("EventNotifier closed")); deferredEvent = void 0; } errorHandler = void 0; }, onEvent: (listener) => { listeners.add(listener); return { close: () => { listeners.delete(listener); } }; }, waitForEvent: () => (deferredEvent || (deferredEvent = createDeferredValue())).promise, notify, onError: (handler) => { errorHandler = handler; } }; }; // src/utils/common/generate-random-string.ts var generateRandomString = (length = 8) => { if (length < 8 || length > 128) { throw new Error("IllegalArgument: length must be a number between 8 and 128"); } const timestamp = Date.now(); return Array.from({ length }, () => { const entropy = timestamp + performance.now(); const randomIndex = Math.floor(Math.random() * entropy % 1 * UNIQUE_CHARACTERS.length); return UNIQUE_CHARACTERS.charAt(randomIndex); }).join(""); }; var UNIQUE_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // src/tracker/invocation/stack/implementation.ts var createBasicInvocationStack = (options) => { const logger = withPrefix(options?.logger ?? OFF_LOGGER, "stack.basic", { fallbackPrefix: "emitnlog.tracker" }); const stack = []; logger.d`creating stack`; return { close: () => { logger.d`closing`; stack.length = 0; }, push: (key) => { logger.t`pushing key '${key.id}'`; stack.push(key); }, peek: () => stack.at(-1), pop: () => { const key = stack.pop(); logger.t`${key ? `popped key '${key.id}'` : "no key to pop"}`; return key; } }; }; var createThreadSafeInvocationStack = (storage, options) => { const logger = withPrefix(options?.logger ?? OFF_LOGGER, "stack.thread-safe", { fallbackPrefix: "emitnlog.tracker" }); logger.d`creating stack`; return { close: () => { logger.d`closing`; storage.disable(); }, push: (key) => { logger.t`pushing key '${key.id}'`; const current = storage.getStore() ?? []; storage.enterWith([...current, key]); }, peek: () => { const current = storage.getStore(); return current?.at(-1); }, pop: () => { const current = storage.getStore(); if (!current?.length) { logger.t`no key to pop`; return void 0; } logger.t`popping key '${current.at(-1)?.id}'`; const updated = current.slice(0, -1); storage.enterWith(updated); return current.at(-1); } }; }; // src/tracker/invocation/implementation.ts var createInvocationTracker = (options) => { const trackerId = generateRandomString(); const logger = options?.logger ?? OFF_LOGGER; const invokedNotifier = createEventNotifier(); const startedNotifier = createEventNotifier(); const completedNotifier = createEventNotifier(); const erroredNotifier = createEventNotifier(); const trackerLogger = withPrefix(logger, "", { fallbackPrefix: `emitnlog.invocation-tracker.${trackerId}` }); const stack = options?.stack ?? stackFactory({ logger: trackerLogger }); let closed = false; let counter = -1; const tracker = { id: trackerId, close: () => { if (!closed) { trackerLogger.d`closing tracker`; closed = true; invokedNotifier.close(); startedNotifier.close(); completedNotifier.close(); erroredNotifier.close(); stack.close(); } }, onInvoked: invokedNotifier.onEvent, onStarted: startedNotifier.onEvent, onCompleted: completedNotifier.onEvent, onErrored: erroredNotifier.onEvent, isTracked: (value) => { const id = toTrackedTrackerId(value); return id === trackerId ? "this" : id ? "other" : false; }, track: (operation, fn, opt) => { const trackedLogger = appendPrefix(trackerLogger, `operation.${operation}`); if (closed) { trackedLogger.d`the tracker is closed`; return fn; } if (toTrackedTrackerId(fn) === trackerId) { return fn; } const trackedFn = (...args) => { const argsLength = args.length; const index = ++counter; const invocationLogger = appendPrefix(trackedLogger, String(index)); const parentKey = stack.peek(); const key = { id: `${trackerId}.${operation}.${index}`, trackerId, operation, index }; stack.push(key); const mergedTags = mergeTags(options?.tags, opt?.tags); const notifyStarted = () => { const invocation = { key, stage: { type: "started" } }; if (parentKey) { invocation.parentKey = parentKey; } if (argsLength) { invocation.args = args; } if (mergedTags?.length) { invocation.tags = mergedTags; } invokedNotifier.notify(invocation); startedNotifier.notify(invocation); }; const notifyCompleted = (duration, promiseLike, result2) => { const stage = { type: "completed", duration, result: result2 }; if (promiseLike) { stage.promiseLike = true; } const invocation = { key, stage }; if (parentKey) { invocation.parentKey = parentKey; } if (argsLength) { invocation.args = args; } if (mergedTags?.length) { invocation.tags = mergedTags; } invokedNotifier.notify(invocation); completedNotifier.notify(invocation); }; const notifyErrored = (duration, promiseLike, error) => { const stage = { type: "errored", duration, error }; if (promiseLike) { stage.promiseLike = true; } const invocation = { key, stage }; if (parentKey) { invocation.parentKey = parentKey; } if (argsLength) { invocation.args = args; } if (mergedTags?.length) { invocation.tags = mergedTags; } invokedNotifier.notify(invocation); erroredNotifier.notify(invocation); }; invocationLogger.args(args).i`starting with ${argsLength} args`; notifyStarted(); let result; const start = performance.now(); try { result = fn(...args); } catch (error) { const duration = performance.now() - start; stack.pop(); invocationLogger.args(error).e`an error was thrown '${error}'`; notifyErrored(duration, false, error); throw error; } if (!isPromiseLike(result)) { const duration = performance.now() - start; stack.pop(); invocationLogger.i`completed`; notifyCompleted(duration, false, result); return result; } return result.then( (r) => { const duration = performance.now() - start; stack.pop(); invocationLogger.i`resolved`; notifyCompleted(duration, true, r); return r; }, (error) => { const duration = performance.now() - start; stack.pop(); invocationLogger.args(error).e`rejected`; notifyErrored(duration, true, error); throw error; } ); }; trackedFn[trackedSymbol] = trackerId; return trackedFn; } }; return tracker; }; var trackedSymbol = Symbol.for("@emitnlog/tracker/tracked"); var toTrackedTrackerId = (value) => isNotNullable(value) && typeof value === "function" && trackedSymbol in value && typeof value[trackedSymbol] === "string" ? value[trackedSymbol] : void 0; var isPromiseLike = (value) => isNotNullable(value) && typeof value === "object" && "then" in value && typeof value.then === "function"; var mergeTags = (tags1, tags2) => { if (tags1 && typeof tags1 === "object" && !Array.isArray(tags1)) { tags1 = Object.keys(tags1).map((name) => ({ name, value: tags1[name] })); } if (tags2 && typeof tags2 === "object" && !Array.isArray(tags2)) { tags2 = Object.keys(tags2).map((name) => ({ name, value: tags2[name] })); } if (!tags1?.length && !tags2?.length) { return void 0; } const mergedTags = []; const map = /* @__PURE__ */ new Map(); const addTag = (tag) => { let values = map.get(tag.name); if (!values) { values = /* @__PURE__ */ new Set(); map.set(tag.name, values); } if (!values.has(tag.value)) { values.add(tag.value); mergedTags.push(tag); } }; tags1?.forEach(addTag); tags2?.forEach(addTag); return mergedTags.sort((a, b) => a.name.localeCompare(b.name) || String(a.value).localeCompare(String(b.value))); }; var stackFactory = createBasicInvocationStack; void (async () => { try { if (typeof process !== "undefined" && typeof process.versions.node === "string") { const { AsyncLocalStorage } = await import('async_hooks'); const storage = new AsyncLocalStorage(); stackFactory = (options) => { options.logger.d`creating a thread-safe stack using node:async_hooks`; const stack = createThreadSafeInvocationStack(storage, options); return stack; }; } } catch { } })(); // src/tracker/invocation/stage-invocation.ts var isAtStage = (invocation, stage) => invocation?.stage.type === stage; // src/tracker/invocation/track-methods.ts var trackMethods = (tracker, target, options) => { if (!isNotNullable(target) || !options?.trackBuiltIn && isBuiltIn(target)) { return /* @__PURE__ */ new Set(); } const selected = options?.methods?.length ? new Set(options.methods.filter((method) => isMethod(target, method))) : collectAllMethods(target, options?.includeConstructor); if (!selected.size) { return selected; } for (const method of selected) { const fn = target[method]; target[method] = tracker.track(method, fn.bind(target), { tags: options?.tags }); } return selected; }; var collectAllMethods = (notNullable, includeConstructor) => { const methodNames = /* @__PURE__ */ new Set(); let current = notNullable; while (current && current !== Object.prototype) { for (const key of Object.getOwnPropertyNames(current)) { if (isMethod(current, key) && (includeConstructor || key !== "constructor")) { methodNames.add(key); } } current = Object.getPrototypeOf(current); } return methodNames; }; var isMethod = (notNullable, key) => typeof notNullable[key] === "function"; var isBuiltIn = (target) => { const ctor = target.constructor; return ctor === Array || ctor === Map || ctor === Set || ctor === WeakMap || ctor === WeakSet; }; // src/tracker/promise/implementation.ts var trackPromises = (options) => { const promises = /* @__PURE__ */ new Set(); const logger = withPrefix(options?.logger ?? OFF_LOGGER, "promise", { fallbackPrefix: "emitnlog.promise-tracker" }); const onSettledNotifier = createEventNotifier(); return { get size() { return promises.size; }, onSettled: onSettledNotifier.onEvent, wait: async () => { if (!promises.size) { return; } logger.d`waiting for ${promises.size} promises to settle`; await Promise.allSettled(promises); }, track: (first, second, idMap, keep, forgetOnRejection) => { const label = typeof first === "string" ? first : void 0; const promise = typeof first === "string" ? second : first; if (label !== void 0 && idMap) { const existing = idMap.get(label); if (existing) { logger.d`returning existing promise for label '${label}'`; return existing; } } let trackedPromise; let start; if (typeof promise === "function") { logger.d`tracking a promise supplier${label ? ` with label '${label}'` : ""}`; start = performance.now(); try { trackedPromise = promise(); } catch (error) { trackedPromise = Promise.reject(error); } } else { logger.d`tracking a promise${label ? ` with label '${label}'` : ""}`; start = performance.now(); trackedPromise = promise; } if (!trackedPromise?.then) { trackedPromise = Promise.resolve(trackedPromise); } promises.add(trackedPromise); const finalPromise = trackedPromise.then( (result) => { promises.delete(trackedPromise); if (label !== void 0 && idMap && !keep) { idMap.delete(label); } const duration = performance.now() - start; logger.d`promise${label ? ` with label '${label}'` : ""} resolved in ${duration}ms`; const event = { duration }; if (label !== void 0) { event.label = label; } if (result !== void 0) { event.result = result; } onSettledNotifier.notify(event); return result; }, (error) => { promises.delete(trackedPromise); if (label !== void 0 && idMap && (!keep || forgetOnRejection)) { idMap.delete(label); } const duration = performance.now() - start; logger.d`promise${label ? ` with label '${label}'` : ""} rejected in ${duration}ms`; const event = { duration, rejected: true }; if (label !== void 0) { event.label = label; } if (error !== void 0) { event.result = error; } onSettledNotifier.notify(event); throw error; } ); if (label !== void 0 && idMap) { idMap.set(label, finalPromise); } return finalPromise; } }; }; var holdPromises = (options) => { const idMap = /* @__PURE__ */ new Map(); const tracker = trackPromises(options); return { get size() { return idMap.size; }, onSettled: tracker.onSettled, wait: tracker.wait, has: (id) => idMap.has(id), track: (id, supplier) => tracker.track(id, supplier, idMap) }; }; var vaultPromises = (options) => { const idMap = /* @__PURE__ */ new Map(); const tracker = trackPromises(options); return { get size() { return idMap.size; }, onSettled: tracker.onSettled, wait: tracker.wait, has: (id) => idMap.has(id), clear: () => { idMap.clear(); }, forget: (id) => idMap.delete(id), track: (id, supplier, opt) => tracker.track(id, supplier, idMap, !opt?.forget, options?.forgetOnRejection) }; }; exports.createBasicInvocationStack = createBasicInvocationStack; exports.createInvocationTracker = createInvocationTracker; exports.createThreadSafeInvocationStack = createThreadSafeInvocationStack; exports.holdPromises = holdPromises; exports.isAtStage = isAtStage; exports.trackMethods = trackMethods; exports.trackPromises = trackPromises; exports.vaultPromises = vaultPromises; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map