diff options
Diffstat (limited to 'services/docker/mail-server/relay/logger.ts')
-rw-r--r-- | services/docker/mail-server/relay/logger.ts | 127 |
1 files changed, 96 insertions, 31 deletions
diff --git a/services/docker/mail-server/relay/logger.ts b/services/docker/mail-server/relay/logger.ts index 160d70a..dc6acfe 100644 --- a/services/docker/mail-server/relay/logger.ts +++ b/services/docker/mail-server/relay/logger.ts @@ -1,32 +1,83 @@ -import * as path from "@std/path"; +import { join } from "@std/path"; +import { toWritableStream, Writer } from "@std/io"; -import { createSingleton, generateTimeStringForFileName } from "./util.ts"; +import "./util.ts"; + +export interface LogOptions { + time?: Date; + error?: boolean; +} + +export type LogFile = Pick<Deno.FsFile, "writable"> & Disposable; export class Logger { - constructor(public readonly path: string) {} + #path: string | null = null; - warn(message: string) { - console.log(message); + #wrapWriter(writer: Writer): LogFile { + return { + writable: toWritableStream(writer, { autoClose: false }), + [Symbol.dispose]() {}, + }; } - generateLogFilePath( - prefix: string = "", - suffix: string = "", - instant?: Temporal.Instant | Date, - ): string { - return path.join( - this.path, - `${prefix}-${generateTimeStringForFileName(instant)}-${suffix}`, - ); + #stdoutWrapper: LogFile = this.#wrapWriter(Deno.stdout); + #stderrWrapper: LogFile = this.#wrapWriter(Deno.stderr); + + constructor() { + } + + get path() { + return this.#path; + } + + set path(path) { + this.#path = path; + if (path != null) { + Deno.mkdirSync(path, { recursive: true }); + } } - openLogFile( - prefix: string = "", - suffix: string = "", - instant?: Temporal.Instant | Date, - ): Promise<Deno.FsFile> { - const logPath = this.generateLogFilePath(prefix, suffix, instant); - return Deno.open(logPath, { + log(...args: unknown[]) { + console.log(...args); + } + + warn(...args: unknown[]) { + console.warn(...args); + } + + error(...args: unknown[]) { + console.error(...args); + } + + #extractOptions(options?: LogOptions): Required<LogOptions> { + return { + time: options?.time ?? new Date(), + error: options?.error ?? false, + }; + } + + async openLog( + prefix: string, + suffix: string, + options?: LogOptions, + ): Promise<LogFile> { + if (prefix.includes("/")) { + throw new Error(`Log file prefix ${prefix} contains '/'.`); + } + if (suffix.includes("/")) { + throw new Error(`Log file suffix ${suffix} contains '/'.`); + } + + const { time, error } = this.#extractOptions(options); + if (this.#path == null) { + return error ? this.#stderrWrapper : this.#stdoutWrapper; + } + + const logPath = join( + this.#path, + `${prefix}-${time.toFileNameString()}-${suffix}`, + ); + return await Deno.open(logPath, { read: false, write: true, append: true, @@ -34,17 +85,31 @@ export class Logger { }); } - async logProgramOutput( - process: Deno.ChildProcess, + async openLogForProgram( program: string, - instant?: Temporal.Instant | Date, - ): Promise<void> { - const stdoutFile = await this.openLogFile(program, "stdout", instant); - const stderrFile = await this.openLogFile(program, "stderr", instant); - - process.stdout.pipeTo(stdoutFile.writable); - process.stderr.pipeTo(stderrFile.writable); + options?: Omit<LogOptions, "error">, + ): Promise<{ stdout: LogFile; stderr: LogFile } & Disposable> { + const stdout = await this.openLog(program, "stdout", { + ...options, + error: false, + }); + const stderr = await this.openLog(program, "stderr", { + ...options, + error: true, + }); + return { + stdout, + stderr, + [Symbol.dispose]: () => { + stdout[Symbol.dispose](); + stderr[Symbol.dispose](); + }, + }; } } -export const [getLogger, setLogger] = createSingleton<Logger>("Logger"); +const logger = new Logger(); +export default logger; +export const log = logger.log.bind(logger); +export const warn = logger.warn.bind(logger); +export const error = logger.error.bind(logger); |