Files
inkreach-agent/apps/desktop/src/main/logger.ts
T

143 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 统一日志基座(主进程):
* - 结构化行:`<ISO时间> <LEVEL> [<scope>] <message> k=v ...`,文件按天滚动
* (userData/logs/main-YYYY-MM-DD.log,保留 14 天)
* - 级别:dev 默认 debug、打包默认 info;ASO_LOG_LEVEL=debug|info|warn|error 可覆盖
* - 子 logger:getLogger(scope) —— 各模块用固定 scope(plugin-host / agent-service / ipc / renderer /
* browser),插件日志经 plugin-host 注入 scope=plugin 且带 pluginId 字段
* - 全局兜底:uncaughtException / unhandledRejection / will-quit 落盘
*
* Agent 自诊断约定:日志文件路径固定,pi 侧可直接读文件排查(见 docs/plugins/06-self-diagnosis.md)。
*/
import { app } from 'electron'
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'
import { join } from 'node:path'
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
const LEVEL_ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 }
const RETAIN_DAYS = 14
const MAX_LINE = 8 * 1024
let level: LogLevel = 'info'
let logDir: string | null = null
/** 当天文件名(含日期)缓存;跨天自动切换 */
let fileDay = ''
let fileReady = false
/** 文件不可用时保留最近日志(进程内仍可读) */
const memoryTail: string[] = []
function resolveLevel(): LogLevel {
const env = process.env.ASO_LOG_LEVEL
if (env === 'debug' || env === 'info' || env === 'warn' || env === 'error') return env
return app.isPackaged ? 'info' : 'debug'
}
function ensureFile(now: Date): string | null {
const day = now.toISOString().slice(0, 10)
if (fileReady && fileDay === day && logDir) return join(logDir, `main-${day}.log`)
try {
if (!logDir) logDir = join(app.getPath('userData'), 'logs')
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true })
pruneOldLogs(logDir, day)
fileDay = day
fileReady = true
return join(logDir, `main-${day}.log`)
} catch {
return null
}
}
function pruneOldLogs(dir: string, today: string): void {
try {
for (const name of readdirSync(dir)) {
const m = /^main-(\d{4}-\d{2}-\d{2})\.log$/.exec(name)
if (!m) continue
const ageDays = (Date.now() - statSync(join(dir, name)).mtimeMs) / 86_400_000
if (m[1] !== today && (ageDays > RETAIN_DAYS || m[1] > today)) unlinkSync(join(dir, name))
}
} catch {
// 清理失败不影响写日志
}
}
function fmtValue(v: unknown): string {
if (v === undefined) return 'undefined'
if (v === null) return 'null'
if (v instanceof Error) return v.stack ? v.stack.replace(/\s+/g, ' ').slice(0, 600) : v.message
if (typeof v === 'object') {
try {
return JSON.stringify(v).slice(0, 400)
} catch {
return '[object]'
}
}
return String(v).slice(0, 400)
}
function write(level_: LogLevel, scope: string, message: string, kv?: Record<string, unknown>): void {
if (LEVEL_ORDER[level_] < LEVEL_ORDER[level]) return
const now = new Date()
const parts = [
now.toISOString(),
level_.toUpperCase().padEnd(5),
`[${scope}]`,
message.slice(0, 500)
]
if (kv) {
for (const [k, v] of Object.entries(kv)) parts.push(`${k}=${fmtValue(v)}`)
}
const line = parts.join(' ').slice(0, MAX_LINE)
// 控制台镜像(dev 终端可读)
// eslint-disable-next-line no-console
;(level_ === 'error' ? console.error : level_ === 'warn' ? console.warn : console.log)(line)
memoryTail.push(line)
if (memoryTail.length > 500) memoryTail.shift()
const file = ensureFile(now)
if (file) {
try {
appendFileSync(file, line + '\n', 'utf8')
} catch {
fileReady = false
}
}
}
export interface Logger {
debug(message: string, kv?: Record<string, unknown>): void
info(message: string, kv?: Record<string, unknown>): void
warn(message: string, kv?: Record<string, unknown>): void
error(message: string, kv?: Record<string, unknown>): void
}
export function getLogger(scope: string): Logger {
return {
debug: (message, kv) => write('debug', scope, message, kv),
info: (message, kv) => write('info', scope, message, kv),
warn: (message, kv) => write('warn', scope, message, kv),
error: (message, kv) => write('error', scope, message, kv)
}
}
/** 最近日志(内存尾部;诊断 IPC 预留) */
export function recentLines(count = 100): string[] {
return memoryTail.slice(-count)
}
export function initLogger(): void {
level = resolveLevel()
const log = getLogger('logger')
process.on('uncaughtException', (err) => {
log.error('uncaughtException', { err })
})
process.on('unhandledRejection', (reason) => {
log.error('unhandledRejection', { err: reason })
})
app.on('will-quit', () => {
log.info('app quit')
})
log.info('logger init', { level, userData: app.getPath('userData') })
}