UNPKG

@qvlt/core-logger

Version:

Structured logging for web applications

303 lines (297 loc) 9.72 kB
// 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