aboutsummaryrefslogtreecommitdiff
path: root/deno/base/log.ts
diff options
context:
space:
mode:
Diffstat (limited to 'deno/base/log.ts')
-rw-r--r--deno/base/log.ts164
1 files changed, 164 insertions, 0 deletions
diff --git a/deno/base/log.ts b/deno/base/log.ts
new file mode 100644
index 0000000..1a4942d
--- /dev/null
+++ b/deno/base/log.ts
@@ -0,0 +1,164 @@
+import { join } from "@std/path";
+
+import { toFileNameString } from "./date.ts";
+
+export type LogLevel = "error" | "warn" | "info";
+
+export interface LogEntry {
+ content: [unknown, ...unknown[]];
+ level?: LogLevel;
+ cause?: unknown;
+}
+
+export interface LogEntryBuilder {
+ withLevel(level: LogLevel): LogEntryBuilder;
+ withCause(cause: unknown): LogEntryBuilder;
+ setError(error: boolean): LogEntryBuilder;
+ write(): void;
+}
+
+export interface ExternalLogStream extends Disposable {
+ stream: WritableStream;
+}
+
+export class Logger {
+ #indentSize = 2;
+ #externalLogDir?: string;
+
+ #contextStack: { depth: number; level: LogLevel }[] = [
+ { depth: 0, level: "info" },
+ ];
+
+ get #context() {
+ return this.#contextStack.at(-1)!;
+ }
+
+ get indentSize() {
+ return this.#indentSize;
+ }
+
+ set indentSize(value: number) {
+ this.#indentSize = value;
+ }
+
+ get externalLogDir() {
+ return this.#externalLogDir;
+ }
+
+ set externalLogDir(value: string | undefined) {
+ this.#externalLogDir = value;
+ if (value != null) {
+ Deno.mkdirSync(value, {
+ recursive: true,
+ });
+ }
+ }
+
+ write(entry: LogEntry): void {
+ const { content, level, cause } = entry;
+ const [message, ...rest] = content;
+ console[level ?? this.#context.level](
+ " ".repeat(this.#indentSize * this.#context.depth) + String(message),
+ ...(cause != null ? [cause, ...rest] : rest),
+ );
+ }
+
+ push(entry: LogEntry): Disposable {
+ this.write(entry);
+ this.#contextStack.push({
+ depth: this.#context.depth + 1,
+ level: entry.level ?? this.#context.level,
+ });
+ return {
+ [Symbol.dispose]: () => {
+ this.#contextStack.pop();
+ },
+ };
+ }
+
+ info(message: unknown, ...args: unknown[]) {
+ this.write({ level: "info", content: [message, ...args] });
+ }
+
+ warn(message: unknown, ...args: unknown[]) {
+ this.write({ level: "warn", content: [message, ...args] });
+ }
+
+ error(message: unknown, ...args: unknown[]) {
+ this.write({ level: "error", content: [message, ...args] });
+ }
+
+ builder(message: unknown, ...args: unknown[]): LogEntryBuilder {
+ const entry: LogEntry = {
+ content: [message, ...args],
+ level: "info",
+ cause: undefined,
+ };
+ const builder: LogEntryBuilder = {
+ withCause: (cause) => {
+ entry.cause = cause;
+ return builder;
+ },
+ withLevel: (level) => {
+ entry.level = level;
+ return builder;
+ },
+ setError: (error) => {
+ if (error) entry.level = "error";
+ return builder;
+ },
+ write: () => {
+ this.write(entry);
+ },
+ };
+ return builder;
+ }
+
+ async createExternalLogStream(
+ name: string,
+ options?: {
+ noTime?: boolean;
+ },
+ ): Promise<ExternalLogStream> {
+ if (name.includes("/")) {
+ throw new Error(`External log stream's name (${name}) contains '/'.`);
+ }
+ if (this.#externalLogDir == null) {
+ throw new Error("External log directory is not set.");
+ }
+
+ const logPath = join(
+ this.#externalLogDir,
+ options?.noTime === true
+ ? name
+ : `${name}-${toFileNameString(new Date())}`,
+ );
+
+ const file = await Deno.open(logPath, {
+ read: false,
+ write: true,
+ append: true,
+ create: true,
+ });
+ return {
+ stream: file.writable,
+ [Symbol.dispose]: file[Symbol.dispose].bind(file),
+ };
+ }
+
+ async createExternalLogStreamsForProgram(
+ program: string,
+ ): Promise<{ stdout: WritableStream; stderr: WritableStream } & Disposable> {
+ const stdout = await this.createExternalLogStream(`${program}-stdout`);
+ const stderr = await this.createExternalLogStream(`${program}-stderr`);
+ return {
+ stdout: stdout.stream,
+ stderr: stderr.stream,
+ [Symbol.dispose]: () => {
+ stdout[Symbol.dispose]();
+ stderr[Symbol.dispose]();
+ },
+ };
+ }
+}
+