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