diff options
Diffstat (limited to 'packages/core/src/util/log.ts')
| -rw-r--r-- | packages/core/src/util/log.ts | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/packages/core/src/util/log.ts b/packages/core/src/util/log.ts new file mode 100644 index 000000000..a61c15f7a --- /dev/null +++ b/packages/core/src/util/log.ts @@ -0,0 +1,185 @@ +import path from "path" +import fs from "fs/promises" +import { createWriteStream } from "fs" +import * as Global from "../global" +import z from "zod" +import { Glob } from "./glob" + +export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) +export type Level = z.infer<typeof Level> + +const levelPriority: Record<Level, number> = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +} +const keep = 10 + +let level: Level = "INFO" + +function shouldLog(input: Level): boolean { + return levelPriority[input] >= levelPriority[level] +} + +export type Logger = { + debug(message?: any, extra?: Record<string, any>): void + info(message?: any, extra?: Record<string, any>): void + error(message?: any, extra?: Record<string, any>): void + warn(message?: any, extra?: Record<string, any>): void + tag(key: string, value: string): Logger + clone(): Logger + time( + message: string, + extra?: Record<string, any>, + ): { + stop(): void + [Symbol.dispose](): void + } +} + +const loggers = new Map<string, Logger>() + +export const Default = create({ service: "default" }) + +export interface Options { + print: boolean + dev?: boolean + level?: Level +} + +let logpath = "" +export function file() { + return logpath +} +let write = (msg: any) => { + process.stderr.write(msg) + return msg.length +} + +export async function init(options: Options) { + if (options.level) level = options.level + void cleanup(Global.Path.log) + if (options.print) return + logpath = path.join( + Global.Path.log, + options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", + ) + await fs.truncate(logpath).catch(() => {}) + const stream = createWriteStream(logpath, { flags: "a" }) + write = async (msg: any) => { + return new Promise((resolve, reject) => { + stream.write(msg, (err) => { + if (err) reject(err) + else resolve(msg.length) + }) + }) + } +} + +async function cleanup(dir: string) { + const files = ( + await Glob.scan("????-??-??T??????.log", { + cwd: dir, + absolute: false, + include: "file", + }).catch(() => []) + ) + .filter((file) => path.basename(file) === file) + .sort() + if (files.length <= keep) return + + const doomed = files.slice(0, -keep) + await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {}))) +} + +function formatError(error: Error, depth = 0): string { + const result = error.message + return error.cause instanceof Error && depth < 10 + ? result + " Caused by: " + formatError(error.cause, depth + 1) + : result +} + +let last = Date.now() +export function create(tags?: Record<string, any>) { + tags = tags || {} + + const service = tags["service"] + if (service && typeof service === "string") { + const cached = loggers.get(service) + if (cached) { + return cached + } + } + + function build(message: any, extra?: Record<string, any>) { + const prefix = Object.entries({ + ...tags, + ...extra, + }) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + const prefix = `${key}=` + if (value instanceof Error) return prefix + formatError(value) + if (typeof value === "object") return prefix + JSON.stringify(value) + return prefix + value + }) + .join(" ") + const next = new Date() + const diff = next.getTime() - last + last = next.getTime() + return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n" + } + const result: Logger = { + debug(message?: any, extra?: Record<string, any>) { + if (shouldLog("DEBUG")) { + write("DEBUG " + build(message, extra)) + } + }, + info(message?: any, extra?: Record<string, any>) { + if (shouldLog("INFO")) { + write("INFO " + build(message, extra)) + } + }, + error(message?: any, extra?: Record<string, any>) { + if (shouldLog("ERROR")) { + write("ERROR " + build(message, extra)) + } + }, + warn(message?: any, extra?: Record<string, any>) { + if (shouldLog("WARN")) { + write("WARN " + build(message, extra)) + } + }, + tag(key: string, value: string) { + if (tags) tags[key] = value + return result + }, + clone() { + return create({ ...tags }) + }, + time(message: string, extra?: Record<string, any>) { + const now = Date.now() + result.info(message, { status: "started", ...extra }) + function stop() { + result.info(message, { + status: "completed", + duration: Date.now() - now, + ...extra, + }) + } + return { + stop, + [Symbol.dispose]() { + stop() + }, + } + }, + } + + if (service && typeof service === "string") { + loggers.set(service, result) + } + + return result +} |
