aboutsummaryrefslogtreecommitdiff
path: root/deno/mail-relay/log.ts
blob: ce27ecaba2beaad05305772d683fa09c8015d46a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import { join } from "@std/path";
import { toWritableStream, Writer } from "@std/io";

import "./better-js.ts";

export interface LogOptions {
  time?: Date;
  error?: boolean;
}

export type LogFile = Pick<Deno.FsFile, "writable"> & Disposable;

export class Log {
  #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 });
    }
  }

  infoOrError(isError: boolean, ...args: unknown[]) {
    this[isError ? "error" : "info"].call(this, ...args);
  }

  info(...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 log = new Log();
export default log;