@alexpricedev/log-digest
v0.1.0
Published
In-memory log buffer with pluggable digest delivery for Bun apps
Maintainers
Readme
@alexpricedev/log-digest
In-memory log buffer with a pluggable digest sink. Buffers error / warn / info entries as they happen, then — on an interval you control — hands them off to a sink of your choosing (email, Slack, webhook, anywhere). Zero runtime dependencies. Bun only.
Install
bun add @alexpricedev/log-digestQuick start
import {
logError,
logInfo,
startDigestScheduler,
type DigestSink,
} from "@alexpricedev/log-digest";
const emailSink: DigestSink = {
async send({ format, summary, period }) {
await sendEmail({
to: "[email protected]",
subject: `Log digest — ${period} — ${summary.errorCount} errors`,
html: format.html,
text: format.text,
});
},
};
startDigestScheduler({ sink: emailSink });
logInfo("boot", "server started");
logError("payments", "charge failed", { orderId: "abc123" });Entries are also mirrored to console.log / console.warn / console.error as they happen, so you still see them in your normal stdout/stderr stream.
How it works
- Every
log*call appends aLogEntryto an in-memory buffer. - Every
intervalMs(default: 1 hour) the scheduler callsdrainLogs()and hands the result to your sink. formatDigest()renders a styled HTML table + a plain-text summary, both available onpayload.format.- If your sink throws, the error and the logs that would have been sent are written to
console.error. The scheduler keeps running.
API
Logging
log(level, category, message, data?): void
logError(category, message, data?): void
logWarn(category, message, data?): void
logInfo(category, message, data?): voidBuffer
drainLogs(): LogEntry[] // returns + clears the buffer
getBufferSize(): numberFormatting
formatDigest(logs: LogEntry[]): { html: string; text: string }Scheduler
startDigestScheduler({
sink, // DigestSink
intervalMs?: number, // default 3_600_000 (1h)
startupDelayMs?: number, // default 5000 (ms after start for a first flush; 0 disables)
}): void
stopDigestScheduler(): void
processDigest(sink: DigestSink): Promise<void> // manual flushSink contract
interface DigestSink {
send(payload: DigestPayload): Promise<void>;
}
interface DigestPayload {
logs: LogEntry[];
summary: { errorCount: number; warnCount: number; infoCount: number };
period: string; // e.g. "2026-04-21 18:00 UTC"
format: { html: string; text: string };
}Example sinks
Slack webhook
const slackSink: DigestSink = {
async send({ summary, period, format }) {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `Log digest — ${period}\n${summary.errorCount} errors, ${summary.warnCount} warnings, ${summary.infoCount} info\n\n${format.text}`,
}),
});
},
};HTTP webhook (raw payload)
const httpSink: DigestSink = {
async send(payload) {
await fetch("https://hooks.example.com/logs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
},
};Testing in your app
The buffer is module-level singleton state. In tests, call drainLogs() in beforeEach/afterEach for isolation. Pass a recording sink to processDigest to assert on payload contents without starting the scheduler.
License
MIT
