refactor(core): remove util log initialization
This commit is contained in:
parent
ce69ddb88e
commit
8123247d7a
127 changed files with 356 additions and 714 deletions
|
|
@ -1,36 +1,94 @@
|
|||
import { Cause, Effect, Logger, References } from "effect"
|
||||
import * as Log from "../util/log"
|
||||
import { appendFileSync } from "fs"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Cause, Effect, Layer, Logger, References, Schema } from "effect"
|
||||
import * as Global from "../global"
|
||||
import { ensureProcessMetadata } from "../util/opencode-process"
|
||||
|
||||
type Fields = Record<string, unknown>
|
||||
type FieldInput = object
|
||||
|
||||
export const Level = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
|
||||
identifier: "LogLevel",
|
||||
description: "Log level",
|
||||
})
|
||||
export type Level = Schema.Schema.Type<typeof Level>
|
||||
|
||||
const levelPriority: Record<Level, number> = {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
}
|
||||
|
||||
const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key)
|
||||
|
||||
export interface Handle {
|
||||
readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
||||
readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
||||
readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
||||
readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
||||
readonly with: (extra: Fields) => Handle
|
||||
readonly debug: (msg?: unknown, extra?: FieldInput) => Effect.Effect<void>
|
||||
readonly info: (msg?: unknown, extra?: FieldInput) => Effect.Effect<void>
|
||||
readonly warn: (msg?: unknown, extra?: FieldInput) => Effect.Effect<void>
|
||||
readonly error: (msg?: unknown, extra?: FieldInput) => Effect.Effect<void>
|
||||
readonly tag: (key: string, value: string) => Handle
|
||||
readonly with: (extra: FieldInput) => Handle
|
||||
readonly clone: () => Handle
|
||||
readonly time: (message: string, extra?: Fields) => { stop(): void; [Symbol.dispose](): void }
|
||||
}
|
||||
|
||||
const clean = (input?: Fields): Fields =>
|
||||
const clean = (input?: FieldInput): Fields =>
|
||||
Object.fromEntries(
|
||||
Object.entries(input ?? {})
|
||||
.filter((entry) => entry[1] !== undefined && entry[1] !== null)
|
||||
.map(([key, value]) => [normalizeKey(key), value]),
|
||||
)
|
||||
|
||||
const text = (input: unknown): string => {
|
||||
// oxlint-disable-next-line no-base-to-string
|
||||
if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
|
||||
// oxlint-disable-next-line no-base-to-string
|
||||
return input === undefined ? "" : String(input)
|
||||
const call = (run: (msg: string) => Effect.Effect<void>, base: FieldInput, msg?: unknown, extra?: FieldInput) => {
|
||||
const ann = clean({ ...base, ...extra })
|
||||
const fx = run(stringifyMessage(msg))
|
||||
return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
|
||||
}
|
||||
|
||||
const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
|
||||
const ann = clean({ ...base, ...extra })
|
||||
const fx = run(msg)
|
||||
return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
|
||||
export function file() {
|
||||
if (disabled(process.env.OPENCODE_LOG_FILE)) return ""
|
||||
return process.env.OPENCODE_LOG_FILE || path.join(Global.Path.log, "log.jsonl")
|
||||
}
|
||||
|
||||
function shouldLog(input: Level): boolean {
|
||||
return levelPriority[input] >= levelPriority[parseLevel(process.env.OPENCODE_LOG_LEVEL) ?? "INFO"]
|
||||
}
|
||||
|
||||
function write(input: { json: string; pretty: string }) {
|
||||
const target = file()
|
||||
if (target) {
|
||||
try {
|
||||
appendFileSync(target, input.json)
|
||||
} catch {}
|
||||
}
|
||||
if (truthy(process.env.OPENCODE_PRINT_LOGS)) process.stderr.write(input.pretty)
|
||||
}
|
||||
|
||||
function build(inputLevel: Level, ts: Date, message: unknown, fields: Fields): { json: string; pretty: string } {
|
||||
const metadata = ensureProcessMetadata("main")
|
||||
const service = typeof fields.service === "string" ? fields.service : undefined
|
||||
if (service) delete fields.service
|
||||
const text = stringifyMessage(message)
|
||||
const record = {
|
||||
ts: ts.toISOString(),
|
||||
level: inputLevel,
|
||||
message: text,
|
||||
run_id: metadata.runID,
|
||||
process_role: metadata.processRole,
|
||||
pid: process.pid,
|
||||
service,
|
||||
fields: Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, normalize(value)])),
|
||||
}
|
||||
const prefix = Object.entries({ service, ...record.fields })
|
||||
.filter((entry) => entry[1] !== undefined && entry[1] !== null)
|
||||
.map(([key, value]) => `${key}=${typeof value === "object" ? safeStringify(value) : value}`)
|
||||
.join(" ")
|
||||
return {
|
||||
json: safeStringify(record) + "\n",
|
||||
pretty: [inputLevel.padEnd(5), ts.toISOString().split(".")[0], prefix, text].filter(Boolean).join(" ") + "\n",
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = Logger.make((opts) => {
|
||||
|
|
@ -43,31 +101,97 @@ export const logger = Logger.make((opts) => {
|
|||
extra.cause = Cause.pretty(opts.cause)
|
||||
}
|
||||
|
||||
const svc = typeof extra.service === "string" ? extra.service : undefined
|
||||
if (svc) delete extra.service
|
||||
const log = svc ? Log.create({ service: svc }) : Log.Default
|
||||
const msg = text(opts.message)
|
||||
|
||||
switch (opts.logLevel) {
|
||||
case "Trace":
|
||||
case "Debug":
|
||||
return log.debug(msg, extra)
|
||||
if (shouldLog("DEBUG")) write(build("DEBUG", opts.date, opts.message, extra))
|
||||
return
|
||||
case "Warn":
|
||||
return log.warn(msg, extra)
|
||||
if (shouldLog("WARN")) write(build("WARN", opts.date, opts.message, extra))
|
||||
return
|
||||
case "Error":
|
||||
case "Fatal":
|
||||
return log.error(msg, extra)
|
||||
if (shouldLog("ERROR")) write(build("ERROR", opts.date, opts.message, extra))
|
||||
return
|
||||
default:
|
||||
return log.info(msg, extra)
|
||||
if (shouldLog("INFO")) write(build("INFO", opts.date, opts.message, extra))
|
||||
}
|
||||
})
|
||||
|
||||
export const layer = Logger.layer([logger], { mergeWithExisting: false })
|
||||
export const layer = Logger.layer([logger], { mergeWithExisting: false }).pipe(
|
||||
Layer.tap(() =>
|
||||
Effect.promise(async () => {
|
||||
const target = file()
|
||||
if (target) await fs.mkdir(path.dirname(target), { recursive: true })
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export const create = (base: Fields = {}): Handle => ({
|
||||
export const create = (base: FieldInput = {}): Handle => ({
|
||||
debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra),
|
||||
info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra),
|
||||
warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra),
|
||||
error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
|
||||
tag: (key, value) => create({ ...base, [key]: value }),
|
||||
with: (extra) => create({ ...base, ...extra }),
|
||||
clone: () => create({ ...base }),
|
||||
time: () => ({
|
||||
stop() {},
|
||||
[Symbol.dispose]() {},
|
||||
}),
|
||||
})
|
||||
|
||||
function truthy(value: string | undefined) {
|
||||
return value?.toLowerCase() === "1" || value?.toLowerCase() === "true"
|
||||
}
|
||||
|
||||
function disabled(value: string | undefined) {
|
||||
const lower = value?.toLowerCase()
|
||||
return lower === "0" || lower === "false" || lower === "off"
|
||||
}
|
||||
|
||||
function parseLevel(value: string | undefined): Level | undefined {
|
||||
if (value === "DEBUG" || value === "INFO" || value === "WARN" || value === "ERROR") return value
|
||||
return undefined
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function stringifyMessage(message: unknown): string {
|
||||
if (message instanceof Error) return formatError(message)
|
||||
if (message === undefined) return ""
|
||||
if (typeof message === "string") return message
|
||||
if (Array.isArray(message)) return message.map((item) => stringifyMessage(item)).join(" ")
|
||||
if (typeof message === "object") return safeStringify(message)
|
||||
return String(message)
|
||||
}
|
||||
|
||||
function normalize(value: unknown): unknown {
|
||||
if (value instanceof Error) {
|
||||
return {
|
||||
name: value.name,
|
||||
message: formatError(value),
|
||||
stack: value.stack,
|
||||
}
|
||||
}
|
||||
if (typeof value === "bigint") return value.toString()
|
||||
return value
|
||||
}
|
||||
|
||||
function safeStringify(value: unknown) {
|
||||
const seen = new WeakSet<object>()
|
||||
return JSON.stringify(value, (_, item) => {
|
||||
if (typeof item === "bigint") return item.toString()
|
||||
if (item instanceof Error) return normalize(item)
|
||||
if (typeof item === "object" && item !== null) {
|
||||
if (seen.has(item)) return "[Circular]"
|
||||
seen.add(item)
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,241 +0,0 @@
|
|||
export * as Log from "./log"
|
||||
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { appendFileSync } from "fs"
|
||||
import * as Global from "../global"
|
||||
import { Schema } from "effect"
|
||||
import { ensureProcessMetadata } from "./opencode-process"
|
||||
|
||||
export const Level = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
|
||||
identifier: "LogLevel",
|
||||
description: "Log level",
|
||||
})
|
||||
export type Level = Schema.Schema.Type<typeof Level>
|
||||
|
||||
const levelPriority: Record<Level, number> = {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
}
|
||||
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
|
||||
file?: string | false
|
||||
}
|
||||
|
||||
let logpath = ""
|
||||
export function file() {
|
||||
return logpath
|
||||
}
|
||||
type LogEntry = {
|
||||
json: string
|
||||
pretty: string
|
||||
}
|
||||
let write = (entry: LogEntry) => {
|
||||
process.stderr.write(entry.pretty)
|
||||
}
|
||||
|
||||
export async function init(options: Options) {
|
||||
level = options.level ?? parseLevel(process.env.OPENCODE_LOG_LEVEL) ?? level
|
||||
const print = options.print ?? truthy(process.env.OPENCODE_PRINT_LOGS)
|
||||
const configured = options.file ?? process.env.OPENCODE_LOG_FILE
|
||||
logpath = configured === false || disabled(configured) ? "" : configured || path.join(Global.Path.log, "log.jsonl")
|
||||
|
||||
if (logpath) await fs.mkdir(path.dirname(logpath), { recursive: true })
|
||||
|
||||
write = (entry) => {
|
||||
if (logpath) {
|
||||
try {
|
||||
appendFileSync(logpath, entry.json)
|
||||
} catch {}
|
||||
}
|
||||
if (print) process.stderr.write(entry.pretty)
|
||||
}
|
||||
}
|
||||
|
||||
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(inputLevel: Level, message: any, extra?: Record<string, any>): LogEntry {
|
||||
const ts = new Date()
|
||||
const metadata = ensureProcessMetadata("main")
|
||||
const fields = Object.fromEntries(
|
||||
Object.entries({
|
||||
...tags,
|
||||
...extra,
|
||||
})
|
||||
.filter((entry) => entry[1] !== undefined && entry[1] !== null)
|
||||
.map(([key, value]) => [key, normalize(value)]),
|
||||
)
|
||||
const service = typeof fields.service === "string" ? fields.service : undefined
|
||||
if (service) delete fields.service
|
||||
const text = stringifyMessage(message)
|
||||
const record = {
|
||||
ts: ts.toISOString(),
|
||||
level: inputLevel,
|
||||
message: text,
|
||||
run_id: metadata.runID,
|
||||
process_role: metadata.processRole,
|
||||
pid: process.pid,
|
||||
service,
|
||||
fields,
|
||||
}
|
||||
const diff = ts.getTime() - last
|
||||
last = ts.getTime()
|
||||
const prefix = Object.entries({ service, ...fields })
|
||||
.filter((entry) => entry[1] !== undefined && entry[1] !== null)
|
||||
.map(([key, value]) => `${key}=${typeof value === "object" ? safeStringify(value) : value}`)
|
||||
.join(" ")
|
||||
return {
|
||||
json: safeStringify(record) + "\n",
|
||||
pretty:
|
||||
[inputLevel.padEnd(5), ts.toISOString().split(".")[0], "+" + diff + "ms", prefix, text]
|
||||
.filter(Boolean)
|
||||
.join(" ") + "\n",
|
||||
}
|
||||
}
|
||||
const result: Logger = {
|
||||
debug(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("DEBUG")) {
|
||||
write(build("DEBUG", message, extra))
|
||||
}
|
||||
},
|
||||
info(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("INFO")) {
|
||||
write(build("INFO", message, extra))
|
||||
}
|
||||
},
|
||||
error(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("ERROR")) {
|
||||
write(build("ERROR", message, extra))
|
||||
}
|
||||
},
|
||||
warn(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("WARN")) {
|
||||
write(build("WARN", 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
|
||||
}
|
||||
|
||||
function truthy(value: string | undefined) {
|
||||
return value?.toLowerCase() === "1" || value?.toLowerCase() === "true"
|
||||
}
|
||||
|
||||
function disabled(value: string | undefined) {
|
||||
const lower = value?.toLowerCase()
|
||||
return lower === "0" || lower === "false" || lower === "off"
|
||||
}
|
||||
|
||||
function parseLevel(value: string | undefined): Level | undefined {
|
||||
if (value === "DEBUG" || value === "INFO" || value === "WARN" || value === "ERROR") return value
|
||||
return undefined
|
||||
}
|
||||
|
||||
function stringifyMessage(message: any): string {
|
||||
if (message instanceof Error) return formatError(message)
|
||||
if (message === undefined) return ""
|
||||
if (typeof message === "string") return message
|
||||
if (typeof message === "object") return safeStringify(message)
|
||||
return String(message)
|
||||
}
|
||||
|
||||
function normalize(value: any): any {
|
||||
if (value instanceof Error) {
|
||||
return {
|
||||
name: value.name,
|
||||
message: formatError(value),
|
||||
stack: value.stack,
|
||||
}
|
||||
}
|
||||
if (typeof value === "bigint") return value.toString()
|
||||
return value
|
||||
}
|
||||
|
||||
function safeStringify(value: any) {
|
||||
const seen = new WeakSet<object>()
|
||||
return JSON.stringify(value, (_, item) => {
|
||||
if (typeof item === "bigint") return item.toString()
|
||||
if (item instanceof Error) return normalize(item)
|
||||
if (typeof item === "object" && item !== null) {
|
||||
if (seen.has(item)) return "[Circular]"
|
||||
seen.add(item)
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue