opencode/packages/opencode/src/util/log.ts

188 lines
5.1 KiB
TypeScript
Raw Normal View History

2025-06-01 02:41:00 +08:00
import path from "path"
import fs from "fs/promises"
2026-02-20 00:32:32 +08:00
import { createWriteStream } from "fs"
import { Global } from "../global"
import z from "zod"
import { Glob } from "./glob"
2025-07-09 23:00:03 +08:00
2025-05-18 09:31:42 +08:00
export namespace Log {
2025-11-08 09:59:02 +08:00
export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
2025-07-09 23:00:03 +08:00
export type Level = z.infer<typeof Level>
const levelPriority: Record<Level, number> = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
}
const keep = 10
2025-07-09 23:00:03 +08:00
2025-07-20 01:35:42 +08:00
let level: Level = "INFO"
2025-07-09 23:00:03 +08:00
2025-07-20 01:35:42 +08:00
function shouldLog(input: Level): boolean {
return levelPriority[input] >= levelPriority[level]
2025-07-09 23:00:03 +08:00
}
2025-07-09 21:16:10 +08:00
export type Logger = {
2025-07-09 23:00:03 +08:00
debug(message?: any, extra?: Record<string, any>): void
2025-07-09 21:16:10 +08:00
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" })
2025-06-10 03:00:48 +08:00
export interface Options {
print: boolean
dev?: boolean
2025-07-09 23:00:03 +08:00
level?: Level
2025-06-01 02:41:00 +08:00
}
2025-05-19 02:28:08 +08:00
2025-06-10 03:00:48 +08:00
let logpath = ""
export function file() {
return logpath
}
2025-12-12 03:55:08 +08:00
let write = (msg: any) => {
process.stderr.write(msg)
return msg.length
}
2025-06-10 03:00:48 +08:00
export async function init(options: Options) {
2025-07-20 01:35:42 +08:00
if (options.level) level = options.level
2025-07-31 21:35:57 +08:00
cleanup(Global.Path.log)
if (options.print) return
logpath = path.join(
2025-07-31 21:35:57 +08:00
Global.Path.log,
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
)
await fs.truncate(logpath).catch(() => {})
2026-02-20 00:32:32 +08:00
const stream = createWriteStream(logpath, { flags: "a" })
2025-11-06 09:14:31 +08:00
write = async (msg: any) => {
2026-02-20 00:32:32 +08:00
return new Promise((resolve, reject) => {
stream.write(msg, (err) => {
if (err) reject(err)
else resolve(msg.length)
})
})
2025-06-01 02:41:00 +08:00
}
2025-05-19 02:28:08 +08:00
}
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(() => {})))
}
2025-09-01 06:11:04 +08:00
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()
2025-05-18 09:31:42 +08:00
export function create(tags?: Record<string, any>) {
2025-06-01 02:41:00 +08:00
tags = tags || {}
2025-05-18 09:31:42 +08:00
2025-07-09 21:16:10 +08:00
const service = tags["service"]
if (service && typeof service === "string") {
const cached = loggers.get(service)
if (cached) {
return cached
}
}
2025-05-19 02:13:04 +08:00
function build(message: any, extra?: Record<string, any>) {
const prefix = Object.entries({
...tags,
...extra,
})
2025-05-31 04:39:45 +08:00
.filter(([_, value]) => value !== undefined && value !== null)
2025-08-22 12:27:49 +08:00
.map(([key, value]) => {
const prefix = `${key}=`
2025-09-01 06:11:04 +08:00
if (value instanceof Error) return prefix + formatError(value)
2025-08-22 12:27:49 +08:00
if (typeof value === "object") return prefix + JSON.stringify(value)
return prefix + value
})
2025-06-01 02:41:00 +08:00
.join(" ")
const next = new Date()
const diff = next.getTime() - last
last = next.getTime()
2025-11-08 09:59:02 +08:00
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
2025-05-19 02:13:04 +08:00
}
2025-07-09 21:16:10 +08:00
const result: Logger = {
2025-07-09 23:00:03 +08:00
debug(message?: any, extra?: Record<string, any>) {
if (shouldLog("DEBUG")) {
write("DEBUG " + build(message, extra))
2025-07-09 23:00:03 +08:00
}
},
2025-05-18 09:31:42 +08:00
info(message?: any, extra?: Record<string, any>) {
2025-07-09 23:00:03 +08:00
if (shouldLog("INFO")) {
write("INFO " + build(message, extra))
2025-07-09 23:00:03 +08:00
}
2025-05-19 02:13:04 +08:00
},
error(message?: any, extra?: Record<string, any>) {
2025-07-09 23:00:03 +08:00
if (shouldLog("ERROR")) {
write("ERROR " + build(message, extra))
2025-07-09 23:00:03 +08:00
}
2025-05-18 09:31:42 +08:00
},
2025-06-01 06:42:43 +08:00
warn(message?: any, extra?: Record<string, any>) {
2025-07-09 23:00:03 +08:00
if (shouldLog("WARN")) {
write("WARN " + build(message, extra))
2025-07-09 23:00:03 +08:00
}
2025-06-01 06:42:43 +08:00
},
2025-05-18 09:31:42 +08:00
tag(key: string, value: string) {
2025-06-01 02:41:00 +08:00
if (tags) tags[key] = value
return result
2025-05-18 09:31:42 +08:00
},
clone() {
2025-06-01 02:41:00 +08:00
return Log.create({ ...tags })
2025-05-18 09:31:42 +08:00
},
2025-06-16 01:33:24 +08:00
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()
},
}
},
2025-06-01 02:41:00 +08:00
}
2025-05-18 09:31:42 +08:00
2025-07-09 21:16:10 +08:00
if (service && typeof service === "string") {
loggers.set(service, result)
}
2025-06-01 02:41:00 +08:00
return result
2025-05-18 09:31:42 +08:00
}
}