上手实践

日志与可观测性

Node、Edge 与 Workers 通用的结构化日志:请求ID、敏感信息脱敏与路由示例;适配 Vercel、Cloudflare 与 Node 服务器。

基础概念

  • 结构化(JSON)日志,便于查询与关联。
  • 关联ID:为每个请求统一 request_id
  • 等级:debug/info/warn/error
  • 脱敏:不要记录 cookies、tokens、密钥等。

包含内容

  • Node 与 Edge/Workers 统一的日志 API(相同用法)。
    • Node:Pino 输出一行 JSON(快速、稳定)。
    • Edge/Workers:console.log 输出一行 JSON(轻量)。
  • 中间件在响应头附加 x-request-id(不修改请求头/ Cookie)。
  • 存储 presign 路由提供示例。

关键文件

  • Node: src/lib/logger/server.ts
  • Edge: src/lib/logger/edge.ts
  • 类型: src/lib/logger/types.ts
  • 中间件: src/middleware.ts
  • 示例: src/app/api/storage/uploads/route.ts

关键文件

  • Node: src/lib/logger/server.ts
  • Edge: src/lib/logger/edge.ts
  • 类型: src/lib/logger/types.ts
  • 中间件: src/middleware.ts
  • 示例: src/app/api/storage/uploads/route.ts

查看日志

  • 本地:终端(一行 JSON)。为更清晰,可使用 pino-pretty
    • pnpm dev 2>&1 | pnpx pino-pretty
    • 保存文件:pnpm dev 2>&1 | pnpx pino-pretty | tee logs/dev.log
  • Vercel:Deployments → Functions → Logs 或 vercel logs
  • Cloudflare Workers:wrangler tail

用法(Node)

import { logger, requestIdFromHeaders } from '@/lib/logger/server'
export async function POST(req: Request){
  const rid = requestIdFromHeaders(req.headers)
  const log = logger.child({ request_id: rid, route: '/api/example' })
  log.info({ event: 'example.start' })
  // ...
  log.info({ event: 'example.ok' })
  return new Response('ok')
}

用法(Edge)

import { logger } from '@/lib/logger/edge'
export const runtime = 'edge'
export function GET(){
  logger.info({ event: 'edge.heartbeat' })
  return new Response('ok')
}

建议

  • 配置 LOG_LEVEL,根据需要调整脱敏规则。
  • 默认不写入日志文件,输出到 stdout/stderr。若需本地保存可用 teepnpm dev 2>&1 | tee logs/dev.json
  • 将服务端 console.* 替换为 logger。

用法(Node)

import { logger, requestIdFromHeaders } from '@/lib/logger/server'
export async function POST(req: Request){
  const rid = requestIdFromHeaders(req.headers)
  const log = logger.child({ request_id: rid, route: '/api/example' })
  const start = Date.now()
  try {
    log.info({ event: 'example.start' })
    // ...
    log.info({ event: 'example.ok', duration_ms: Date.now() - start })
    return new Response('ok')
  } catch (e:any) {
    log.error({ event: 'example.error', message: e?.message })
    return new Response('error', { status: 500 })
  }
}

用法(Edge)

import { logger } from '@/lib/logger/edge'
export const runtime = 'edge'
export function GET(){
  logger.info({ event: 'edge.heartbeat' })
  return new Response('ok')
}

辅助函数: withApiLogging (Node)

import { withApiLogging } from '@/lib/logger/server'
export const POST = withApiLogging(async (req) => new Response('ok'), {
  route: '/api/foo', event: 'foo.process'
})

最佳实践

  • 每个请求创建子 logger,附带 request_id;结束时记录 duration_ms
  • 文本简短、字段为主;避免记录完整请求体。

自定义脱敏

  • Node:修改 src/lib/logger/server.tsredactPaths
  • Edge:修改 src/lib/logger/edge.tsredactKeys

可选集成

  • Sentry(@sentry/nextjs)、Axiom / Better Stack / Datadog 等。

故障排查

  • 出现意外登录重定向:确认中间件未覆盖请求头/ Cookie。
  • Edge 无日志:确保 runtime='edge' 并使用 wrangler tail/Vercel 日志查看。