@qvlt/core-logger
Version: 
Structured logging for web applications
303 lines (297 loc) • 9.72 kB
JavaScript
// src/logger.ts
var levelOrder = { debug: 10, info: 20, warn: 30, error: 40 };
var Logger = class {
  constructor(init) {
    this.maxQueue = 1e4;
    // optional cap
    this.queue = [];
    this._flushTimer = null;
    this.app = init.app;
    this.env = init.env;
    this.ver = init.ver;
    this.level = init.level ?? (this.env === "production" ? "info" : "debug");
    const clamp = (n) => n < 0 ? 0 : n > 1 ? 1 : n;
    this.sample = {
      debug: 1,
      info: 1,
      warn: 1,
      error: 1,
      ...init.sample ? Object.fromEntries(Object.entries(init.sample).map(([k, v]) => [k, clamp(v)])) : {}
    };
    this.defaultCtx = init.defaultCtx ?? {};
    this.maxBatch = init.maxBatch ?? 20;
    this.flushIntervalMs = init.flushIntervalMs ?? 5e3;
    this.sessionId = Math.random().toString(36).slice(2);
    this.transports = Array.isArray(init.transport) ? init.transport : [init.transport];
    if (typeof window !== "undefined" && window.addEventListener) {
      this._onBeforeUnload = () => this.flush();
      window.addEventListener("beforeunload", this._onBeforeUnload);
    }
    const setInt = typeof setInterval !== "undefined" ? setInterval : null;
    if (setInt) this._flushTimer = setInt(() => this.flush(), this.flushIntervalMs);
    if (typeof process !== "undefined" && typeof process.on === "function") {
      this._onProcessExit = () => this.destroy();
      process.on("beforeExit", this._onProcessExit);
      process.on("SIGINT", this._onProcessExit);
      process.on("SIGTERM", this._onProcessExit);
    }
  }
  // Convenience methods for the main logger
  debug(event, ctx) {
    this.log("debug", event, ctx);
  }
  info(event, ctx) {
    this.log("info", event, ctx);
  }
  warn(event, ctx) {
    this.log("warn", event, ctx);
  }
  error(event, ctx, error) {
    this.log("error", event, ctx, error);
  }
  async time(event, f, c) {
    const t0 = typeof performance !== "undefined" ? performance.now() : Date.now();
    try {
      return await f();
    } finally {
      const dur = Math.round((typeof performance !== "undefined" ? performance.now() : Date.now()) - t0);
      this.log("info", `${event}.done`, { durationMs: dur, ...c ?? {} });
    }
  }
  child(component, extra) {
    return {
      log: (lvl, event, ctx, err) => this.log(lvl, event, { component, ...extra ?? {}, ...ctx ?? {} }, err),
      debug: (e, c) => this.log("debug", e, { component, ...extra ?? {}, ...c ?? {} }),
      info: (e, c) => this.log("info", e, { component, ...extra ?? {}, ...c ?? {} }),
      warn: (e, c) => this.log("warn", e, { component, ...extra ?? {}, ...c ?? {} }),
      error: (e, c, err) => this.log("error", e, { component, ...extra ?? {}, ...c ?? {} }, err),
      child: (childComponent, childExtra) => this.child(`${component}.${childComponent}`, { ...extra ?? {}, ...childExtra ?? {} }),
      time: async (event, f, c) => {
        const t0 = typeof performance !== "undefined" ? performance.now() : Date.now();
        try {
          return await f();
        } finally {
          const dur = Math.round((typeof performance !== "undefined" ? performance.now() : Date.now()) - t0);
          this.log("info", `${event}.done`, { component, durationMs: dur, ...extra ?? {}, ...c ?? {} });
        }
      }
    };
  }
  log(lvl, event, ctx, error) {
    if (levelOrder[lvl] < levelOrder[this.level]) return;
    if (Math.random() > (this.sample[lvl] ?? 1)) return;
    let component;
    let traceId;
    if (ctx && typeof ctx === "object" && "component" in ctx && typeof ctx.component === "string") {
      component = ctx.component;
      const { component: _ignored, ...rest } = ctx;
      ctx = rest;
    }
    if (ctx && typeof ctx === "object" && "traceId" in ctx && typeof ctx.traceId === "string") {
      traceId = ctx.traceId;
      const { traceId: _traceId, ...rest } = ctx;
      ctx = rest;
    }
    const err = error ? {
      message: error?.message ?? String(error),
      stack: error?.stack,
      name: error?.name,
      code: error?.code
    } : void 0;
    const ev = {
      ts: Date.now(),
      lvl,
      app: this.app,
      env: this.env,
      ver: this.ver,
      component,
      event,
      ctx: sanitizeCtx({ ...this.defaultCtx, ...ctx ?? {} }),
      err,
      sessionId: this.sessionId,
      traceId
    };
    this.queue.push(ev);
    if (this.queue.length > this.maxQueue) this.queue.splice(0, this.queue.length - this.maxQueue);
    if (this.queue.length >= this.maxBatch) this.flush();
  }
  flush() {
    if (this.queue.length === 0 || !this.transports?.length) return;
    const batch = this.queue.splice(0, this.queue.length);
    for (const t of this.transports) {
      try {
        void t.write(batch);
      } catch {
      }
      try {
        void t.flush?.();
      } catch {
      }
    }
  }
  destroy() {
    if (this._flushTimer != null) {
      clearInterval(this._flushTimer);
      this._flushTimer = null;
    }
    this.flush();
    for (const t of this.transports) {
      try {
        t.flush?.();
      } catch {
      }
      try {
        t.destroy?.();
      } catch {
      }
    }
    if (typeof window !== "undefined" && this._onBeforeUnload) {
      window.removeEventListener("beforeunload", this._onBeforeUnload);
      this._onBeforeUnload = void 0;
    }
    if (typeof process !== "undefined" && typeof process.off === "function" && this._onProcessExit) {
      process.off("beforeExit", this._onProcessExit);
      process.off("SIGINT", this._onProcessExit);
      process.off("SIGTERM", this._onProcessExit);
      this._onProcessExit = void 0;
    }
  }
  setDefaultContext(patch) {
    this.defaultCtx = { ...this.defaultCtx, ...patch };
  }
  setLevel(level) {
    this.level = level;
    return this;
  }
  setTransports(t) {
    this.transports = Array.isArray(t) ? t : [t];
    return this;
  }
};
function sanitizeCtx(ctx) {
  const seen = /* @__PURE__ */ new WeakSet();
  const cap = (v) => {
    if (v == null) return v;
    if (typeof v === "string") return v.length > 4e3 ? v.slice(0, 4e3) + "\u2026" : v;
    if (typeof v !== "object") return v;
    if (seen.has(v)) return "[Circular]";
    seen.add(v);
    if (Array.isArray(v)) return v.slice(0, 50).map(cap);
    const out = {};
    for (const k of Object.keys(v).slice(0, 100))
      out[k] = cap(v[k]);
    return out;
  };
  return cap(ctx);
}
// src/initialization.ts
var loggerInstance = null;
var teardown = null;
async function initializeLogger(config) {
  if (teardown) {
    teardown();
  }
  loggerInstance = new Logger(config);
  teardown = () => {
    loggerInstance?.destroy?.();
    loggerInstance = null;
  };
}
function shutdownLogger() {
  teardown?.();
  loggerInstance = null;
}
function setDefaultLogContext(patch) {
  if (!loggerInstance) throw new Error("Logger not initialized.");
  loggerInstance.setDefaultContext(patch);
}
function getLogger(component) {
  if (!loggerInstance) {
    const createLogMethod = (level) => (event, context, error) => {
      const consoleMethod = console[level];
      if (consoleMethod) {
        const prefix = component ? `[${component}]` : "";
        consoleMethod(`${prefix} ${event}`, context ?? "", error ?? "");
      }
    };
    const fallbackLogger = {
      debug: createLogMethod("debug"),
      info: createLogMethod("info"),
      warn: createLogMethod("warn"),
      error: createLogMethod("error"),
      log: (level, event, context, error) => createLogMethod(level)(event, context, error),
      child: (childComponent) => getLogger(childComponent),
      time: async (event, f, c) => {
        const t0 = typeof performance !== "undefined" ? performance.now() : Date.now();
        try {
          return await f();
        } finally {
          const dur = Math.round((typeof performance !== "undefined" ? performance.now() : Date.now()) - t0);
          createLogMethod("info")(`${event}.done`, { durationMs: dur, ...c ?? {} });
        }
      }
    };
    return fallbackLogger;
  }
  return component ? loggerInstance.child(component) : loggerInstance;
}
function installGlobalGetLogger() {
  if (typeof globalThis !== "undefined") {
    globalThis.getLogger = getLogger;
  }
}
// src/util/devConsole.ts
function devConsole(ev) {
  const { lvl, event, component, ctx, err } = ev;
  const base = `${component ?? "app"} ${event}`;
  const args = [base];
  if (ctx && Object.keys(ctx).length > 0) args.push(ctx);
  if (err) args.push(err);
  if (lvl === "debug") {
    (console.debug || console.log)(...args);
  } else {
    console[lvl](...args);
  }
}
// src/transport/ConsoleTransport.ts
var ConsoleTransport = class {
  write(batch) {
    for (const ev of batch) devConsole(ev);
  }
};
// src/transport/HttpTransport.ts
var HttpTransport = class {
  constructor(endpoint) {
    this.endpoint = endpoint;
  }
  async write(batch) {
    const body = JSON.stringify(batch);
    if (typeof navigator !== "undefined" && "sendBeacon" in navigator && body.length < 6e5) {
      try {
        const ok = navigator.sendBeacon(
          this.endpoint,
          new Blob([body], { type: "application/json" })
        );
        if (ok) return;
      } catch {
      }
    }
    await fetch(this.endpoint, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body
    }).catch(() => {
    });
  }
};
// src/transport/StdoutTransport.ts
var StdoutTransport = class {
  write(batch) {
    for (const ev of batch) {
      process.stdout.write(JSON.stringify(ev) + "\n");
    }
  }
};
export { ConsoleTransport, HttpTransport, Logger, StdoutTransport, getLogger, initializeLogger, installGlobalGetLogger, setDefaultLogContext, shutdownLogger };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map