serialize.js 3.42 KB
/**
 * 纯函数:把一条结构化日志记录安全序列化成**一行** 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()))
}