核心概念

最佳实践

适配器
安全指南、数据消毒和生产提示。了解不应记录的内容以及如何保护敏感数据。

本指南介绍 evlog 的安全最佳实践和生产注意事项。

不应记录的内容

宽事件功能强大,因为它能捕获全面的上下文信息。但这也容易导致意外记录敏感数据。永远不要记录:

类别示例风险
凭证密码、API 密钥、令牌、密钥账户泄露
支付数据完整卡号、CVV、银行账户PCI 合规违规
个人信息(PII)社保号、护照号、驾驶证号隐私法律违规(GDPR、CCPA)
健康数据医疗记录、诊断信息HIPAA 违规
身份验证信息会话令牌、JWT、刷新令牌会话劫持
日志通常可被整个团队访问,并可能存储在第三方服务中。请将它们视为半公开信息。

自动脱敏

保护个人身份信息(PII)的最简单方法是启用内置的自动脱敏功能:

nuxt.config.ts
evlog: {
  redact: true,
}

这会自动在所有宽事件输出和任何数据发送前,屏蔽信用卡号(****1111)、电子邮件(a***@***.com)、IP 地址、电话号码、JWT、Bearer 令牌和 IBAN。完整的配置参考请参见自动脱敏

自动脱敏是安全网,不能替代谨慎的日志记录习惯。应始终优先选择显式字段选择,并结合 redact: true 实现纵深防御。

消毒模式

手动字段选择

最安全的方法是显式选择要记录的字段:

server/api/user/update.post.ts
import { useLogger } from 'evlog'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const body = await readBody(event)

  // ❌ 永远不要记录完整的请求体
  // log.set({ body })

  // ✅ 显式选择安全字段
  log.set({
    user: {
      id: body.id,
      email: maskEmail(body.email),
      // password: body.password ← 永远不要包含
    },
  })
})

辅助函数

创建用于消毒常见数据类型的实用函数:

server/utils/sanitize.ts
/** 掩码邮箱:john.doe@example.com → j***.d**@e***.com */
export function maskEmail(email: string): string {
  const [local, domain] = email.split('@')
  if (!domain) return '***'
  const [domainName, tld] = domain.split('.')
  return `${local[0]}***@${domainName[0]}***.${tld}`
}

/** 掩码银行卡号:4242424242424242 → ****4242 */
export function maskCard(card: string): string {
  return `****${card.slice(-4)}`
}

/** 截断长 ID 以提升可读性 */
export function truncateId(id: string, length = 8): string {
  if (id.length <= length) return id
  return `${id.slice(0, length)}...`
}

/** 移除对象中的敏感字段 */
export function sanitize<T extends Record<string, unknown>>(
  obj: T,
  sensitiveKeys: string[] = ['password', 'token', 'secret', 'apiKey', 'authorization']
): Partial<T> {
  const result = { ...obj }
  for (const key of sensitiveKeys) {
    if (key in result) {
      delete result[key]
    }
  }
  return result
}

使用示例:

server/api/checkout.post.ts
import { useLogger } from 'evlog'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const { user, card } = await readBody(event)

  log.set({
    user: {
      id: user.id,
      email: maskEmail(user.email),
    },
    payment: {
      last4: maskCard(card.number),
      // ❌ 永远不要记录:卡号、CVV、过期时间
    },
  })
})

排水钩子过滤

作为最后一道防线,在发送到外部服务前过滤敏感数据:

server/plugins/evlog-sanitize.ts
const SENSITIVE_KEYS = ['password', 'token', 'secret', 'apiKey', 'authorization', 'cookie']

function deepSanitize(obj: Record<string, unknown>): Record<string, unknown> {
  const result: Record<string, unknown> = {}

  for (const [key, value] of Object.entries(obj)) {
    // 检查键名是否包含敏感关键词(不区分大小写)
    if (SENSITIVE_KEYS.some(k => key.toLowerCase().includes(k))) {
      result[key] = '[REDACTED]'
    } else if (value && typeof value === 'object' && !Array.isArray(value)) {
      // 递归消毒嵌套对象
      result[key] = deepSanitize(value as Record<string, unknown>)
    } else {
      result[key] = value
    }
  }

  return result
}

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', (ctx) => {
    // 在发送到外部服务前消毒事件
    ctx.event = deepSanitize(ctx.event) as typeof ctx.event
  })
})
排水钩子消毒是安全网,不能替代谨慎的日志记录习惯。始终在源头进行消毒。

生产检查清单

在部署到生产环境前,请确认:

日志配置

  • 已设置服务名称(env.service
  • 已配置高流量路由的采样率
  • 已设置外部服务(Axis、Loki 等)的日志排水
  • 生产环境中已禁用美化输出(pretty: false

数据安全

  • 已启用自动脱敏(redact: true
  • 日志中不包含密码或密钥
  • 不记录完整的信用卡号(仅保留后 4 位)
  • 不记录 API 密钥或令牌
  • 个人信息已脱敏或省略(电子邮件、电话号码)
  • 会话令牌未被记录
  • 请求体被选择性记录(而非 log.set({ body })

错误处理

  • 错误使用 createError() 并包含结构化字段
  • 错误信息中不包含敏感数据
  • 生产环境中堆栈跟踪不暴露内部路径

字段命名约定

在代码库中使用一致且分组的字段名称:

server/api/checkout.post.ts
// ✅ 良好 — 分组且描述清晰
log.set({
  user: { id, plan, accountAge },
  cart: { items, total, currency },
  payment: { method, provider, last4 },
})

// ❌ 不良 — 平铺且缩写
log.set({
  uid: '123',
  n: 3,
  t: 9999,
  pm: 'card',
})

推荐字段结构

类别字段
userid, plan, role, accountAge
requestmethod, path, requestId, traceId
cart / orderitems, total, currency, coupon
paymentmethod, provider, last4, status
outcomestatus, duration, error

采样策略

在规模较大时,日志量可能变得昂贵。请明智地使用采样:

nuxt.config.ts
export default defineNuxtConfig({
  evlog: {
    sampling: {
      // 头部采样:每个级别按随机百分比采样
      rates: {
        info: 10,    // 10% 的成功日志
        warn: 50,    // 50% 的警告日志
        debug: 0,    // 生产环境中不记录调试日志
        error: 100,  // 始终保留错误日志
      },
      // 尾部采样:根据结果强制保留
      keep: [
        { duration: 1000 },           // 慢请求(≥1s)
        { status: 400 },              // 客户端/服务器错误
        { path: '/api/payments/**' }, // 关键路径
      ],
    },
  },
})
使用 $production 覆盖配置,以便在开发环境中保留完整日志,而在生产环境中进行采样。请参见Nuxt 框架指南

下一步