copilot-api / src /lib /logger.ts
imspsycho's picture
Initial upload from Google Colab
98c9143 verified
Raw
History Blame Contribute Delete
5.45 kB
import consola, { type ConsolaInstance } from "consola"
import fs from "node:fs"
import path from "node:path"
import util from "node:util"
import { PATHS } from "./paths"
import { registerProcessCleanup } from "./process-cleanup"
import { requestContext } from "./request-context"
import { state } from "./state"
const LOG_RETENTION_DAYS = 7
const LOG_RETENTION_MS = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000
const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000
const LOG_DIR = path.join(PATHS.APP_DIR, "logs")
const FLUSH_INTERVAL_MS = 1000
const MAX_BUFFER_SIZE = 100
const logStreams = new Map<string, fs.WriteStream>()
const logBuffers = new Map<string, Array<string>>()
let runtimeInitialized = false
let flushInterval: ReturnType<typeof setInterval> | undefined
let cleanupInterval: ReturnType<typeof setInterval> | undefined
const ensureLogDirectory = () => {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true })
}
}
const cleanupOldLogs = () => {
if (!fs.existsSync(LOG_DIR)) {
return
}
const now = Date.now()
for (const entry of fs.readdirSync(LOG_DIR)) {
const filePath = path.join(LOG_DIR, entry)
let stats: fs.Stats
try {
stats = fs.statSync(filePath)
} catch {
continue
}
if (!stats.isFile()) {
continue
}
if (now - stats.mtimeMs > LOG_RETENTION_MS) {
try {
fs.rmSync(filePath)
} catch {
continue
}
}
}
}
const formatArgs = (args: Array<unknown>) =>
args
.map((arg) =>
typeof arg === "string" ? arg : (
util.inspect(arg, { depth: null, colors: false })
),
)
.join(" ")
const sanitizeName = (name: string) => {
const normalized = name
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, "-")
.replaceAll(/^-+|-+$/g, "")
return normalized === "" ? "handler" : normalized
}
const maybeUnref = (timer: ReturnType<typeof setInterval>) => {
timer.unref()
}
const flushBuffer = (filePath: string) => {
const buffer = logBuffers.get(filePath)
if (!buffer || buffer.length === 0) {
return
}
const stream = getLogStream(filePath)
const content = buffer.join("\n") + "\n"
stream.write(content, (error) => {
if (error) {
console.warn("Failed to write handler log", error)
}
})
logBuffers.set(filePath, [])
}
const flushAllBuffers = () => {
for (const filePath of logBuffers.keys()) {
flushBuffer(filePath)
}
}
const cleanup = () => {
if (flushInterval) {
clearInterval(flushInterval)
flushInterval = undefined
}
if (cleanupInterval) {
clearInterval(cleanupInterval)
cleanupInterval = undefined
}
flushAllBuffers()
for (const stream of logStreams.values()) {
stream.end()
}
logStreams.clear()
logBuffers.clear()
}
const initializeLoggerRuntime = () => {
if (runtimeInitialized) {
return
}
runtimeInitialized = true
ensureLogDirectory()
cleanupOldLogs()
flushInterval = setInterval(flushAllBuffers, FLUSH_INTERVAL_MS)
maybeUnref(flushInterval)
cleanupInterval = setInterval(cleanupOldLogs, CLEANUP_INTERVAL_MS)
maybeUnref(cleanupInterval)
registerProcessCleanup(cleanup)
}
const getLogStream = (filePath: string): fs.WriteStream => {
initializeLoggerRuntime()
let stream = logStreams.get(filePath)
if (!stream || stream.destroyed) {
stream = fs.createWriteStream(filePath, { flags: "a" })
logStreams.set(filePath, stream)
stream.on("error", (error: unknown) => {
console.warn("Log stream error", error)
logStreams.delete(filePath)
})
}
return stream
}
const appendLine = (filePath: string, line: string) => {
let buffer = logBuffers.get(filePath)
if (!buffer) {
buffer = []
logBuffers.set(filePath, buffer)
}
buffer.push(line)
if (buffer.length >= MAX_BUFFER_SIZE) {
flushBuffer(filePath)
}
}
type DebugLogger = Pick<ConsolaInstance, "debug">
export const debugLazy = (
logger: DebugLogger,
factory: () => [unknown, ...Array<unknown>],
): void => {
if (!state.verbose) {
return
}
logger.debug(...factory())
}
export const debugJson = (
logger: DebugLogger,
label: string,
value: unknown,
): void => {
debugLazy(logger, () => [label, JSON.stringify(value)])
}
export const debugJsonTail = (
logger: DebugLogger,
label: string,
{ value, tailLength = 400 }: { value: unknown; tailLength?: number },
): void => {
debugLazy(logger, () => [label, JSON.stringify(value).slice(-tailLength)])
}
export const createHandlerLogger = (name: string): ConsolaInstance => {
const sanitizedName = sanitizeName(name)
const instance = consola.withTag(name)
if (state.verbose) {
instance.level = 5
}
instance.setReporters([])
instance.addReporter({
log(logObj) {
initializeLoggerRuntime()
const context = requestContext.getStore()
const traceId = context?.traceId
const date = logObj.date
const dateKey = date.toLocaleDateString("sv-SE")
const timestamp = date.toLocaleString("sv-SE", { hour12: false })
const filePath = path.join(LOG_DIR, `${sanitizedName}-${dateKey}.log`)
const message = formatArgs(logObj.args as Array<unknown>)
const traceIdStr = traceId ? ` [${traceId}]` : ""
const line = `[${timestamp}] [${logObj.type}] [${logObj.tag || name}]${traceIdStr}${
message ? ` ${message}` : ""
}`
appendLine(filePath, line)
},
})
return instance
}