summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/util/log.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src/util/log.ts')
-rw-r--r--packages/core/src/util/log.ts185
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
+}