(Init): Added shit

This commit is contained in:
2026-05-28 23:46:40 +00:00
parent a5250706cb
commit 8410600c63
46 changed files with 3898 additions and 228 deletions

View File

@@ -0,0 +1,202 @@
/**
* Logger sink implementations.
*
* - `ConsoleSink`: synchronous writes to stdout/stderr, optionally
* ANSI-colorized for `console` format or compact JSON for `json` format.
* - `RotatingFileSink`: asynchronous, append-only JSON lines with size-based
* rotation. Writes are serialized through a promise queue so concurrent
* `write()` calls cannot interleave.
*
* Both sinks survive transient errors (logged to stderr) — a failing log
* sink must never crash the host process.
*/
import { dirname } from "@std/path";
import { ensureDir } from "@std/fs";
import {
bold,
cyan,
gray,
green,
magenta,
red,
yellow,
} from "@std/fmt/colors";
import {
LOG_LEVEL_ORDER,
type LogLevel,
type LogRecord,
type LogSink,
} from "./types.ts";
const encoder = new TextEncoder();
const LEVEL_COLOR: Record<LogLevel, (s: string) => string> = {
debug: cyan,
info: green,
warn: yellow,
error: red,
fatal: (s) => bold(magenta(s)),
};
// =====================================================================
// ConsoleSink
// =====================================================================
export class ConsoleSink implements LogSink {
constructor(private readonly format: "console" | "json") {}
write(record: LogRecord): void {
const line = this.format === "json"
? JSON.stringify(record) + "\n"
: this.formatConsole(record) + "\n";
const useStderr = LOG_LEVEL_ORDER[record.level] >= LOG_LEVEL_ORDER.warn;
const writer = useStderr ? Deno.stderr : Deno.stdout;
try {
writer.writeSync(encoder.encode(line));
} catch {
// Never propagate sink failures to user code.
}
}
flush(): Promise<void> {
return Promise.resolve();
}
private formatConsole(record: LogRecord): string {
const { time, level, logger, msg, ...rest } = record;
const ts = gray(time);
const lvl = LEVEL_COLOR[level](level.toUpperCase().padEnd(5));
const name = cyan(`[${logger}]`);
let fields = "";
const keys = Object.keys(rest);
if (keys.length > 0) {
const parts: string[] = [];
for (const k of keys) {
parts.push(`${k}=${formatValue(rest[k])}`);
}
fields = " " + gray(parts.join(" "));
}
return `${ts} ${lvl} ${name} ${msg}${fields}`;
}
}
function formatValue(v: unknown): string {
if (v === null) return "null";
if (v === undefined) return "undefined";
switch (typeof v) {
case "string":
return /\s/.test(v) ? JSON.stringify(v) : v;
case "number":
case "boolean":
case "bigint":
return String(v);
case "function":
return "[function]";
}
if (v instanceof Error) {
return `${v.name}: ${v.message}`;
}
try {
return JSON.stringify(v);
} catch {
return String(v);
}
}
// =====================================================================
// RotatingFileSink
// =====================================================================
export class RotatingFileSink implements LogSink {
private queue: Promise<void> = Promise.resolve();
private readonly maxBytes: number;
private readonly maxBackups: number;
private dirEnsured = false;
constructor(
private readonly path: string,
options: { maxBytes?: number; maxBackups?: number } = {},
) {
this.maxBytes = options.maxBytes ?? 10 * 1024 * 1024;
this.maxBackups = options.maxBackups ?? 5;
}
write(record: LogRecord): void {
const line = JSON.stringify(record) + "\n";
this.queue = this.queue.then(() => this.doWrite(line)).catch((err) => {
const msg = err instanceof Error ? err.message : String(err);
try {
Deno.stderr.writeSync(
encoder.encode(`[logger] RotatingFileSink write failed: ${msg}\n`),
);
} catch {
// give up
}
});
}
async flush(): Promise<void> {
await this.queue;
}
private async doWrite(line: string): Promise<void> {
if (!this.dirEnsured) {
await ensureDir(dirname(this.path));
this.dirEnsured = true;
}
try {
const stat = await Deno.stat(this.path);
if (stat.size + line.length > this.maxBytes) {
await this.rotate();
}
} catch (err) {
if (!(err instanceof Deno.errors.NotFound)) throw err;
}
await Deno.writeTextFile(this.path, line, { append: true });
}
private async rotate(): Promise<void> {
// Remove the oldest backup if it would be pushed past the retention limit.
if (this.maxBackups > 0) {
const oldest = `${this.path}.${this.maxBackups}`;
try {
await Deno.remove(oldest);
} catch (err) {
if (!(err instanceof Deno.errors.NotFound)) throw err;
}
}
// Shift .{i} -> .{i+1} for i in [maxBackups-1 .. 1].
for (let i = this.maxBackups - 1; i >= 1; i--) {
const src = `${this.path}.${i}`;
const dst = `${this.path}.${i + 1}`;
try {
await Deno.rename(src, dst);
} catch (err) {
if (!(err instanceof Deno.errors.NotFound)) throw err;
}
}
// Rotate the active log to .1 (only if there is at least one backup slot).
if (this.maxBackups > 0) {
try {
await Deno.rename(this.path, `${this.path}.1`);
} catch (err) {
if (!(err instanceof Deno.errors.NotFound)) throw err;
}
} else {
// No retention — just truncate.
try {
await Deno.remove(this.path);
} catch (err) {
if (!(err instanceof Deno.errors.NotFound)) throw err;
}
}
}
}