UNPKG

@nowarajs/logger

Version:

Type-safe logging library for Bun with advanced TypeScript body intersection, modular sink pattern, transform streams, and immutable API design.

281 lines (277 loc) 8.24 kB
// @bun import { LOGGER_ERROR_KEYS } from "./chunk-zcfsvnjp.js"; // source/logger.ts import { InternalError } from "@nowarajs/error"; import { TypedEventEmitter } from "@nowarajs/typed-event-emitter"; // source/worker-logger.ts var workerFunction = () => { const sinks = {}; const self = globalThis; const processLogEntry = async (log) => { await Promise.all(log.sinkNames.map(async (sinkName) => { const sink = sinks[sinkName]; if (!sink) return; try { await sink.log(log.level, log.timestamp, log.object); } catch (error) { self.postMessage({ type: "SINK_LOG_ERROR", sinkName, error, object: log.object }); } })); }; self.addEventListener("message", async (event) => { switch (event.data.type) { case "REGISTER_SINK": { const { sinkName, sinkClassName, sinkClassString, sinkArgs } = event.data; try { const factory = new Function("sinkArgs", ` ${sinkClassString} return new ${sinkClassName}(...sinkArgs); `); sinks[sinkName] = factory(sinkArgs); } catch (error) { self.postMessage({ type: "REGISTER_SINK_ERROR", sinkName, error }); } break; } case "LOG_BATCH": { const { logs } = event.data; try { for (const log of logs) await processLogEntry(log); } finally { self.postMessage({ type: "BATCH_COMPLETE" }); } break; } case "CLOSE": { await Promise.all(Object.entries(sinks).map(async ([name, sink]) => { try { await sink.close?.(); } catch (error) { self.postMessage({ type: "SINK_CLOSE_ERROR", sinkName: name, error }); } })); self.postMessage({ type: "CLOSE_COMPLETE" }); break; } } }); }; // source/logger.ts class Logger extends TypedEventEmitter { _sinks; _sinkKeys = []; _worker; _maxPendingLogs; _maxMessagesInFlight; _batchSize; _batchTimeout; _autoEnd; _flushOnBeforeExit; _pendingLogs = []; _messagesInFlight = 0; _batchTimer = null; _isWriting = false; _flushResolvers = []; _closeResolver = null; _backpressureResolver = null; _handleExit = () => { this._worker.terminate(); }; _handleWorkerClose = () => { process.off("beforeExit", this._handleBeforeExit); process.off("exit", this._handleExit); }; constructor(options) { super(); const { autoEnd = true, batchSize = 50, batchTimeout = 0.1, flushOnBeforeExit = true, maxMessagesInFlight = 100, maxPendingLogs = 1e4 } = options ?? {}; this._sinks = {}; this._maxPendingLogs = maxPendingLogs; this._maxMessagesInFlight = maxMessagesInFlight; this._batchSize = batchSize; this._batchTimeout = batchTimeout; this._autoEnd = autoEnd; this._flushOnBeforeExit = flushOnBeforeExit; this._worker = new Worker(URL.createObjectURL(new Blob([`(${workerFunction.toString()})()`], { type: "application/javascript" })), { type: "module" }); this._setupWorkerMessages(); if (this._autoEnd) this._setupAutoEnd(); } registerSink(sinkName, sinkConstructor, ...sinkArgs) { if (this._sinks[sinkName]) throw new InternalError(LOGGER_ERROR_KEYS.SINK_ALREADY_ADDED); this._worker.postMessage({ type: "REGISTER_SINK", sinkName, sinkClassName: sinkConstructor.name, sinkClassString: sinkConstructor.toString(), sinkArgs }); this._sinks[sinkName] = sinkConstructor; this._sinkKeys.push(sinkName); return this; } error(object, sinkNames = this._sinkKeys) { this._enqueue("ERROR", object, sinkNames); } warn(object, sinkNames = this._sinkKeys) { this._enqueue("WARN", object, sinkNames); } info(object, sinkNames = this._sinkKeys) { this._enqueue("INFO", object, sinkNames); } debug(object, sinkNames = this._sinkKeys) { this._enqueue("DEBUG", object, sinkNames); } log(object, sinkNames = this._sinkKeys) { this._enqueue("LOG", object, sinkNames); } async flush() { if (this._pendingLogs.length === 0 && this._messagesInFlight === 0) return; return new Promise((resolve) => { this._flushResolvers.push(resolve); if (!this._isWriting && this._pendingLogs.length > 0) { this._isWriting = true; this._processPendingLogs(); } }); } async close() { await this.flush(); return new Promise((resolve) => { this._closeResolver = resolve; this._worker.postMessage({ type: "CLOSE" }); }); } _enqueue(level, object, sinkNames) { if (this._sinkKeys.length === 0) throw new InternalError(LOGGER_ERROR_KEYS.NO_SINKS_PROVIDED, { level, object }); if (this._pendingLogs.length >= this._maxPendingLogs) return; this._pendingLogs.push({ sinkNames: sinkNames ?? this._sinkKeys, level, timestamp: Date.now(), object }); if (this._pendingLogs.length >= this._batchSize) { if (this._batchTimer !== null) { clearTimeout(this._batchTimer); this._batchTimer = null; } this._triggerProcessing(); } else if (this._batchTimeout > 0 && this._batchTimer === null) { this._batchTimer = setTimeout(() => { this._batchTimer = null; this._triggerProcessing(); }, this._batchTimeout); } } _triggerProcessing() { if (this._isWriting) return; this._isWriting = true; this._processPendingLogs(); } async _processPendingLogs() { while (this._pendingLogs.length > 0) { if (this._messagesInFlight >= this._maxMessagesInFlight) await new Promise((resolve) => { this._backpressureResolver = resolve; }); const batch = this._pendingLogs.splice(0, this._batchSize); this._messagesInFlight++; this._worker.postMessage({ type: "LOG_BATCH", logs: batch }); } this._isWriting = false; this.emit("drained"); } _releaseBatch() { this._messagesInFlight--; if (this._backpressureResolver !== null) { this._backpressureResolver(); this._backpressureResolver = null; } } _setupWorkerMessages() { this._worker.addEventListener("message", (event) => { switch (event.data.type) { case "BATCH_COMPLETE": this._releaseBatch(); if (this._messagesInFlight === 0 && this._pendingLogs.length === 0 && this._flushResolvers.length > 0) { for (const resolve of this._flushResolvers) resolve(); this._flushResolvers.length = 0; } break; case "SINK_LOG_ERROR": this.emit("sinkError", new InternalError(LOGGER_ERROR_KEYS.SINK_LOG_ERROR, event.data)); this._releaseBatch(); break; case "SINK_CLOSE_ERROR": this.emit("sinkError", new InternalError(LOGGER_ERROR_KEYS.SINK_CLOSE_ERROR, event.data)); break; case "REGISTER_SINK_ERROR": this.emit("registerSinkError", new InternalError(LOGGER_ERROR_KEYS.REGISTER_SINK_ERROR, event.data)); break; case "CLOSE_COMPLETE": this._worker.terminate(); if (this._closeResolver) { this._closeResolver(); this._closeResolver = null; } break; } }); } _setupAutoEnd() { process.on("beforeExit", this._handleBeforeExit); process.on("exit", this._handleExit); this._worker.addEventListener("close", this._handleWorkerClose); } _handleBeforeExit = () => { if (this._flushOnBeforeExit) this.flush().then(() => this.close()).catch((error) => { this.emit("onBeforeExitError", new InternalError(LOGGER_ERROR_KEYS.BEFORE_EXIT_FLUSH_ERROR, { error })); }); else this.close().catch((error) => { this.emit("onBeforeExitError", new InternalError(LOGGER_ERROR_KEYS.BEFORE_EXIT_CLOSE_ERROR, { error })); }); }; } export { Logger };