emitnlog
Version: 
Emit n' Log: a modern, type-safe library for logging, event notifications, and observability in JavaScript/TypeScript apps.
762 lines (749 loc) • 24.4 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)
  };
};
export { createBasicInvocationStack, createInvocationTracker, createThreadSafeInvocationStack, holdPromises, isAtStage, trackMethods, trackPromises, vaultPromises };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map