serialize.js
3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* 纯函数:把一条结构化日志记录安全序列化成**一行** JSON 字符串。
*
* 健壮性约定(见任务文档 §5.4 / §8):
* - `Error` 实例转成 `{ name, message, stack }`,不会丢成 `{}`。
* - 循环引用不抛错(用 WeakSet 检测,替换为 `'[Circular]'`)。
* - `undefined` 字段被丢弃;`null` 保留。
* - `bigint`/`function`/`symbol` 等非 JSON 原生类型被安全降级为字符串描述,绝不抛。
* - 永不抛异常:即便序列化彻底失败,也返回一个**仍带 trace_id 的**兜底 JSON 行。
*
* @param {Record<string, any>} 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<string, any>} 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<object>} 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()))
}