diff options
Diffstat (limited to 'services/docker/mail-server/relay/logger.ts')
-rw-r--r-- | services/docker/mail-server/relay/logger.ts | 115 |
1 files changed, 115 insertions, 0 deletions
diff --git a/services/docker/mail-server/relay/logger.ts b/services/docker/mail-server/relay/logger.ts new file mode 100644 index 0000000..dc6acfe --- /dev/null +++ b/services/docker/mail-server/relay/logger.ts @@ -0,0 +1,115 @@ +import { join } from "@std/path"; +import { toWritableStream, Writer } from "@std/io"; + +import "./util.ts"; + +export interface LogOptions { + time?: Date; + error?: boolean; +} + +export type LogFile = Pick<Deno.FsFile, "writable"> & Disposable; + +export class Logger { + #path: string | null = null; + + #wrapWriter(writer: Writer): LogFile { + return { + writable: toWritableStream(writer, { autoClose: false }), + [Symbol.dispose]() {}, + }; + } + + #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 }); + } + } + + 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, + create: true, + }); + } + + async openLogForProgram( + program: string, + 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](); + }, + }; + } +} + +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); |