/** * 纯函数:把一条结构化日志记录安全序列化成**一行** JSON 字符串。 * * 健壮性约定(见任务文档 §5.4 / §8): * - `Error` 实例转成 `{ name, message, stack }`,不会丢成 `{}`。 * - 循环引用不抛错(用 WeakSet 检测,替换为 `'[Circular]'`)。 * - `undefined` 字段被丢弃;`null` 保留。 * - `bigint`/`function`/`symbol` 等非 JSON 原生类型被安全降级为字符串描述,绝不抛。 * - 永不抛异常:即便序列化彻底失败,也返回一个**仍带 trace_id 的**兜底 JSON 行。 * * @param {Record} record 已成形的日志记录(含 time/level/service/trace_id/msg + 自定义字段) * @returns {string} 一行 JSON(不含换行) */ export function serializeLine(record) { const seen = new WeakSet() try { return JSON.stringify(record, replacer(seen)) } catch { // 最后兜底:保证 trace_id 永不丢,且依然是合法 JSON。 const traceId = record && typeof record === 'object' ? record.trace_id : undefined return JSON.stringify({ time: new Date().toISOString(), level: 'error', service: 'observability', trace_id: traceId ?? null, msg: 'log serialization failed', }) } } /** * 本地运行使用的单行可读格式。生产环境仍使用 JSON,方便日志平台检索。 * @param {Record} record * @returns {string} */ export function serializePrettyLine(record) { const fixed = new Set(['time', 'level', 'service', 'msg']) const fields = [] for (const [key, value] of Object.entries(record)) { if (fixed.has(key) || value === undefined) continue fields.push(`${key}=${prettyValue(value)}`) } const header = `${record.time} ${String(record.level).toUpperCase().padEnd(5)} ${record.service}` return fields.length > 0 ? `${header} ${record.msg} | ${fields.join(' ')}` : `${header} ${record.msg}` } /** * 构造一个 `JSON.stringify` replacer:处理 Error、循环引用、非 JSON 原生类型。 * @param {WeakSet} seen * @returns {(this: any, key: string, value: any) => any} */ function replacer(seen) { return function (key, value) { if (value instanceof Error) { return errorRecord(value) } if (typeof value === 'bigint') return value.toString() if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]` if (typeof value === 'symbol') return value.toString() if (typeof value === 'object' && value !== null) { if (seen.has(value)) return '[Circular]' seen.add(value) } return value } } /** @param {Error & { code?: unknown, sqlMessage?: unknown }} error */ function errorRecord(error) { const sqlMessage = typeof error.sqlMessage === 'string' ? error.sqlMessage : null return { name: error.name, code: typeof error.code === 'string' ? error.code : undefined, message: sqlMessage || error.message, stack: sqlMessage ? undefined : error.stack, } } /** @param {any} value */ function prettyValue(value) { if (value instanceof Error) { const error = errorRecord(value) const code = error.code ? ` code=${error.code}` : '' return `${error.name}${code} message=${JSON.stringify(error.message)}` } if (typeof value === 'string') return JSON.stringify(value) if (typeof value === 'number' || typeof value === 'boolean' || value === null) return String(value) return JSON.stringify(value, replacer(new WeakSet())) }