Skip to content

日志与监控

日志系统

请求日志中间件

ts
// server/middleware/logger.ts
export default defineEventHandler((event) => {
  const start = Date.now()

  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    const status = event.node.res.statusCode
    const method = event.method
    const url = event.path

    console.log(JSON.stringify({
      type: 'request',
      method,
      url,
      status,
      duration,
      timestamp: new Date().toISOString(),
      userId: event.context.userId,
      ip: getRequestHeader(event, 'x-forwarded-for'),
      userAgent: getRequestHeader(event, 'user-agent'),
    }))
  })
})

结构化日志

ts
// server/utils/logger.ts
export const logger = {
  info: (message: string, data?: any) => {
    console.log(JSON.stringify({
      level: 'info',
      message,
      data,
      timestamp: new Date().toISOString(),
    }))
  },

  error: (message: string, error?: any) => {
    console.error(JSON.stringify({
      level: 'error',
      message,
      error: error?.message || error,
      stack: error?.stack,
      timestamp: new Date().toISOString(),
    }))
  },

  warn: (message: string, data?: any) => {
    console.warn(JSON.stringify({
      level: 'warn',
      message,
      data,
      timestamp: new Date().toISOString(),
    }))
  },
}

日志级别指南

级别使用场景示例
info正常业务流程用户登录、订单创建、缓存命中
warn可恢复的异常慢请求、缓存未命中、限流触发
error需要关注的错误数据库连接失败、支付回调验签失败

什么不该记日志?

  • ❌ 密码、Token、密钥等敏感信息
  • ❌ 完整的请求体(可能包含用户隐私)
  • ❌ 高频操作的详细日志(如每秒数百次的健康检查)

为什么用 JSON 格式?

JSON 格式便于日志收集工具(如 ELK、Loki、CloudWatch)解析和搜索。console.log('用户登录: 13800138000') 不好搜索,{"message":"用户登录","phone":"138****8000"} 更好。

使用日志

ts
// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
  try {
    const { phone } = await readBody(event)
    logger.info('用户登录', { phone: phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') })
    // ... 登录逻辑
    logger.info('登录成功', { phone: phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'), userId: user.id })
    return { token, user }
  } catch (error) {
    logger.error('登录失败', error)
    throw error
  }
})

手机号脱敏

日志中不记录完整手机号,使用正则替换为 138****8000。这是数据保护的基本要求。

错误追踪

全局错误处理

ts
// server/plugins/error-tracking.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('error', async (error, event) => {
    logger.error('服务端错误', {
      error: error.message,
      stack: error.stack,
      url: event.path,
      method: event.method,
      userId: event.context.userId,
    })

    // 上报到 Sentry(可选)
    // Sentry.captureException(error)
  })
})

客户端错误处理

ts
// app/plugins/error-handler.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('vue:error', (error) => {
    console.error('Vue Error:', error)
    // 上报到错误监控
  })

  if (import.meta.client) {
    window.addEventListener('unhandledrejection', (event) => {
      console.error('Unhandled Promise:', event.reason)
    })
  }
})

服务端 vs 客户端错误处理对比

对比项服务端客户端
触发位置server/plugins/app/plugins/
捕获的错误API 错误、数据库错误Vue 渲染错误、未捕获的 Promise
日志位置服务器终端 / 日志文件浏览器控制台
上报方式Sentry(服务端集成)Sentry(浏览器 SDK)
用户影响500 错误页面白屏 / 组件错误

性能监控

API 响应时间

ts
// server/middleware/performance.ts
export default defineEventHandler((event) => {
  const start = Date.now()

  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    if (duration > 1000) {
      logger.warn('慢请求', {
        url: event.path,
        duration,
        method: event.method,
      })
    }
  })
})

慢请求阈值参考

响应时间级别原因
< 200ms✅ 正常大部分 API 应在这个范围
200ms - 1s⚠️ 偏慢可能有 N+1 查询或缺少索引
> 1s❌ 慢请求需要优化,影响用户体验
> 5s🚨 严重可能数据库死锁或外部服务超时

健康检查 API

ts
// server/routes/health.ts
export default defineEventHandler(async () => {
  const start = Date.now()

  // 检查数据库连接
  try {
    await db.execute(sql`SELECT 1`)
  } catch {
    return { status: 'unhealthy', database: 'disconnected', uptime: process.uptime() }
  }

  // 检查 Redis 连接
  try {
    await useStorage('redis').getItem('health-check')
  } catch {
    return { status: 'unhealthy', redis: 'disconnected', uptime: process.uptime() }
  }

  return {
    status: 'ok',
    uptime: process.uptime(),
    responseTime: Date.now() - start,
    timestamp: new Date().toISOString(),
  }
})

健康检查不需要认证

/health 路径在 server/routes/ 下(不是 /api/ 前缀),不会经过认证中间件。这是正确的设计——监控工具和负载均衡器需要无认证访问。

process.uptime() 的作用

返回 Node.js 进程运行时间(秒)。如果频繁重启(uptime 很短),说明应用不稳定。

Sentry 集成(推荐)

服务端 Sentry

bash
npm install @sentry/node
ts
// server/plugins/sentry.ts
import * as Sentry from '@sentry/node'

export default defineNitroPlugin((nitroApp) => {
  Sentry.init({
    dsn: useRuntimeConfig().sentryDsn,
    environment: process.env.NODE_ENV,
    tracesSampleRate: 0.1,  // 10% 的请求追踪性能
  })

  nitroApp.hooks.hook('error', (error) => {
    Sentry.captureException(error)
  })

  nitroApp.hooks.hook('request', (event) => {
    Sentry.setUser({ id: event.context.userId })
  })
})

客户端 Sentry

bash
npm install @sentry/vue
ts
// app/plugins/sentry.client.ts
import * as Sentry from '@sentry/vue'

export default defineNuxtPlugin((nuxtApp) => {
  Sentry.init({
    app: nuxtApp.vueApp,
    dsn: useRuntimeConfig().public.sentryDsn,
    environment: process.env.NODE_ENV,
  })
})

为什么客户端插件加 .client 后缀?

sentry.client.ts 只在客户端执行。Sentry 的 Vue SDK 依赖浏览器 API,在 SSR 环境会报错。

Sentry vs 自建日志

对比项自建 console.logSentry
错误聚合需要自己实现自动去重、分组
告警通知需自己写 Webhook邮件/Slack/钉钉开箱即用
错误上下文需手动记录自动捕获面包屑、用户信息
Source Map不支持支持还原压缩代码的原始位置
费用免费免费额度 + 付费

建议:开发阶段用自建日志,上线后接 Sentry。

告警

简单告警(Webhook)

ts
// server/utils/alert.ts
export const sendAlert = async (message: string, level: 'warning' | 'critical' = 'warning') => {
  const webhookUrl = useRuntimeConfig().alertWebhookUrl
  if (!webhookUrl) return

  await $fetch(webhookUrl, {
    method: 'POST',
    body: {
      msgtype: 'markdown',
      markdown: {
        content: `## ${level === 'critical' ? '🚨' : '⚠️'} 服务告警\n\n${message}\n\n时间:${new Date().toISOString()}`,
      },
    },
  })
}

使用告警

ts
nitroApp.hooks.hook('error', async (error, event) => {
  if (error.statusCode >= 500) {
    await sendAlert(`服务端错误: ${error.message}`, 'critical')
  }
})

告警级别与触发条件

级别触发条件通知方式示例
warning可恢复的异常企业微信/钉钉慢请求 > 3s、缓存未命中率高
critical服务不可用企业微信 + 电话数据库断连、5xx 错误飙升

告警疲劳

告警太多会导致开发者忽略。建议:(1) 只对需要人为介入的问题告警;(2) 自恢复问题不发 critical;(3) 同一问题 5 分钟内不重复告警。

常见问题

生产环境日志去哪看?

部署方式查看日志命令
PM2pm2 logs flutter-api
Dockerdocker compose logs -f app
Kuberneteskubectl logs -f deployment/flutter-api

日志太多磁盘满了?

  1. 使用 logrotate 自动轮转和压缩旧日志
  2. 日志只保留最近 30 天
  3. 考虑使用日志收集服务(如 Loki、ELK)替代本地文件

console.log 在生产环境性能差吗?

console.log 是同步阻塞的(特别是在写文件时)。高并发场景下,建议使用异步日志库或 event.waitUntil() 延迟日志写入。

基于 Nuxt 4 官方文档整理编写