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
JavaScript
;
// 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