singularity-forge/packages/daemon/src/logger.ts
Lex Christopherson fa2bde5677 test: Implemented YAML config loader with validation/defaults and struc…
- "packages/daemon/src/config.ts"
- "packages/daemon/src/logger.ts"
- "packages/daemon/src/daemon.test.ts"

GSD-Task: S01/T02
2026-03-27 13:43:46 -06:00

88 lines
2.4 KiB
TypeScript

import { createWriteStream, mkdirSync, type WriteStream } from 'node:fs';
import { dirname } from 'node:path';
import type { LogLevel, LogEntry } from './types.js';
const LEVEL_ORDER: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
export interface LoggerOptions {
filePath: string;
level: LogLevel;
verbose?: boolean;
}
/**
* Structured JSON-lines file logger.
* Writes LogEntry objects one per line in append mode.
* The open write stream keeps the Node event loop alive (daemon keepalive).
*/
export class Logger {
private readonly stream: WriteStream;
private readonly level: number;
private readonly verbose: boolean;
constructor(opts: LoggerOptions) {
// Ensure parent directory exists
const dir = dirname(opts.filePath);
try {
mkdirSync(dir, { recursive: true });
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(`Cannot create log directory ${dir}: ${msg}`);
}
this.stream = createWriteStream(opts.filePath, { flags: 'a' });
this.level = LEVEL_ORDER[opts.level] ?? LEVEL_ORDER.info;
this.verbose = opts.verbose ?? false;
}
debug(msg: string, data?: Record<string, unknown>): void {
this.write('debug', msg, data);
}
info(msg: string, data?: Record<string, unknown>): void {
this.write('info', msg, data);
}
warn(msg: string, data?: Record<string, unknown>): void {
this.write('warn', msg, data);
}
error(msg: string, data?: Record<string, unknown>): void {
this.write('error', msg, data);
}
/** End the write stream. Resolves when the stream is fully flushed. */
close(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.stream.end(() => {
this.stream.once('close', () => resolve());
});
this.stream.once('error', reject);
});
}
private write(level: LogLevel, msg: string, data?: Record<string, unknown>): void {
if (LEVEL_ORDER[level] < this.level) return;
const entry: LogEntry = {
ts: new Date().toISOString(),
level,
msg,
...(data !== undefined ? { data } : {}),
};
const line = JSON.stringify(entry) + '\n';
this.stream.write(line);
if (this.verbose) {
const prefix = `[${entry.ts}] ${level.toUpperCase()}`;
const suffix = data ? ` ${JSON.stringify(data)}` : '';
process.stderr.write(`${prefix}: ${msg}${suffix}\n`);
}
}
}