错误监控、日志与可观测性

代码上线不是终点,而是运维的起点。没有监控的生产环境就像盲飞——用户遇到的错误你不知道,性能退化你不知道,服务宕机你不知道。可观测性(Observability)是指通过**指标(Metrics)、日志(Logs)、链路追踪(Traces)**三个维度理解系统运行状态的能力。本章从 Nuxt4 的错误监控、日志体系、性能监控到告警配置,构建完整的可观测性方案。

1. 错误监控:Sentry

1.1 为什么需要错误监控

console.error 只在开发者自己的浏览器控制台可见。生产环境中用户遇到的错误,你完全看不到——除非用户主动反馈(大部分用户不会)。

错误监控工具(Sentry)解决这个问题:自动捕获前端和服务端的错误,上报到中心平台,附带堆栈、浏览器信息、用户操作轨迹。

1.2 Nuxt4 集成 Sentry

npx nuxi module add @sentry/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@sentry/nuxt/module'],
  sentry: {
    sourceMapsUploadOptions: {
      org: 'my-org',
      project: 'my-nuxt-app',
    },
  },
  sourcemap: {
    client: 'hidden',  // 生成 sourcemap 但不暴露给用户
  },
})
// sentry.client.config.ts
import * as Sentry from '@sentry/nuxt'
 
Sentry.init({
  dsn: useRuntimeConfig().public.sentryDsn,
  tracesSampleRate: 0.1,       // 10% 的请求采集性能数据
  replaysSessionSampleRate: 0,  // Session Replay 采样率
  replaysOnErrorSampleRate: 1,  // 错误时 100% 记录回放
})
// sentry.server.config.ts
import * as Sentry from '@sentry/nuxt'
 
Sentry.init({
  dsn: useRuntimeConfig().public.sentryDsn,
  tracesSampleRate: 0.1,
})

1.3 双端错误捕获

Sentry 在 Nuxt4 中自动捕获两种错误:

客户端错误

  • 未捕获的 JavaScript 异常(throw new Error()
  • Promise rejection(fetch 失败、API 超时)
  • Vue 组件渲染错误(onErrorCaptured
  • 手动上报(Sentry.captureException(error)

服务端错误

  • Nitro server routes 中的异常
  • SSR 渲染错误
  • 中间件错误

1.4 Source Maps

生产环境的 JavaScript 是压缩混淆的——错误堆栈类似 a.js:1:2345,毫无用处。Source Maps 把压缩后的位置映射回源代码位置:

生产环境错误堆栈(无 Source Map):
TypeError: Cannot read property 'name' of undefined
    at a.js:1:2345

有 Source Map 后:
TypeError: Cannot read property 'name' of undefined
    at getUserProfile (composables/useAuth.ts:42:15)

Sentry 在构建时上传 Source Maps,线上错误自动映射回源代码。sourcemap.client: 'hidden' 确保 Source Maps 不暴露给用户(防止源码泄露)。

1.5 上下文信息

错误本身只是冰山一角。要诊断问题,还需要上下文:

// 设置用户信息
Sentry.setUser({
  id: user.value?.id,
  email: user.value?.email,
})
 
// 设置额外上下文
Sentry.setContext('video', {
  videoId: route.params.id,
  format: 'hls',
  quality: '1080p',
})
 
// 面包屑(用户操作轨迹)
Sentry.addBreadcrumb({
  category: 'video',
  message: 'User clicked play button',
  level: 'info',
})

面包屑(Breadcrumbs)自动记录用户在触发错误前的操作轨迹——页面导航、点击事件、网络请求。这让你能还原"用户做了什么导致了这个错误"。

2. 结构化日志

2.1 为什么需要结构化日志

console.log('用户登录成功') 在开发时够用,但在生产环境中毫无用处——你无法搜索、过滤、聚合纯文本日志。

结构化日志是 JSON 格式的日志,每条日志包含标准化的字段:

{
  "level": "info",
  "message": "User login successful",
  "timestamp": "2025-01-15T14:30:00Z",
  "userId": "u_123",
  "method": "oauth",
  "provider": "google",
  "duration": 342,
  "requestId": "req_abc123"
}

结构化日志可以被日志平台(Elasticsearch、Loki、Datadog)索引和查询:

  • "查找所有 Google OAuth 登录失败的记录"
  • "查找响应时间 > 5s 的 API 请求"
  • "查找用户 u_123 的所有操作"

2.2 Nitro 日志配置

Nitro 使用 consola 作为日志库,支持日志分级和格式化:

// server/utils/logger.ts
import { consola } from 'consola'
 
export const logger = consola.withTag('app')
 
// 使用
logger.info('Server started', { port: 3000 })
logger.warn('Slow query detected', { query: 'SELECT...', duration: 2500 })
logger.error('Database connection failed', { host: 'db.example.com' })

生产环境推荐使用 JSON 格式输出,便于日志平台解析:

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    logLevel: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
  },
})

2.3 请求级日志

为每个请求生成唯一 ID,所有日志携带该 ID——可以追踪一个请求的完整链路:

// server/middleware/request-id.ts
import { randomUUID } from 'crypto'
 
export default defineEventHandler((event) => {
  const requestId = getHeader(event, 'x-request-id') || randomUUID()
  event.context.requestId = requestId
  setHeader(event, 'x-request-id', requestId)
})
// server/api/videos.get.ts
export default defineEventHandler((event) => {
  const requestId = event.context.requestId
  logger.info('Fetching videos', { requestId, userId: event.context.userId })
  // ...
})

3. 日志分级策略

3.1 日志级别

级别用途生产环境
fatal系统无法继续运行✅ 必须记录
error业务逻辑错误,需要处理✅ 必须记录
warn异常但不影响主流程✅ 建议记录
info关键业务事件✅ 选择性记录
debug调试信息❌ 生产关闭
trace极详细的追踪❌ 仅开发使用

3.2 什么该记录

应该记录(info 级别)

  • 用户认证事件(登录/登出/注册)
  • 关键业务操作(下单、支付、视频上传)
  • 外部服务调用(第三方 API、支付网关)
  • 系统启动和配置

应该记录(error 级别)

  • 未预期的异常
  • 外部服务调用失败
  • 数据一致性问题

不应该记录

  • 敏感信息(密码、Token、信用卡号)
  • 大量重复的心跳日志
  • 每个请求的详细参数(info 级别太多日志会产生性能和成本问题)

3.3 日志存储方案

方案特点成本适用规模
文件 + logrotate最简单,本地存储单服务器
Loki + Grafana开源,轻量级中小规模
Elasticsearch + Kibana功能最强,全文搜索大规模
Datadog / New RelicSaaS,开箱即用很高预算充足
Cloudflare Logpush边缘日志,实时推送按量Cloudflare 用户

4. 性能监控

4.1 Core Web Vitals

Google 的 Core Web Vitals 是衡量用户体验的核心指标:

指标含义好的阈值
LCP最大内容绘制时间< 2.5s
FID / INP首次/持续交互延迟< 200ms
CLS累积布局偏移< 0.1
TTFB首字节时间< 800ms

4.2 Lighthouse CI

在 CI 中自动运行 Lighthouse 审计,防止性能退化:

# .github/workflows/lighthouse.yml
lighthouse:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: pnpm/action-setup@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: 'pnpm'
    - run: pnpm install --frozen-lockfile
    - run: pnpm build
 
    - name: Lighthouse CI
      uses: treosh/lighthouse-ci-action@v12
      with:
        configPath: './lighthouserc.json'
        uploadArtifacts: true
// lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["warn", { "minScore": 0.9 }],
        "categories:seo": ["error", { "minScore": 0.9 }],
        "first-contentful-paint": ["error", { "maxNumericValue": 2000 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }]
      }
    }
  }
}

当 Lighthouse 分数低于阈值时,CI 直接失败——性能也是质量门禁的一部分

4.3 真实用户监控(RUM)

Lighthouse 是合成测试(在标准化环境中测量),真实用户监控(RUM)采集真实用户的性能数据:

// plugins/web-vitals.client.ts
import { onCLS, onFID, onLCP, onTTFB } from 'web-vitals'
 
export default defineNuxtPlugin(() => {
  function reportMetric(metric: { name: string; value: number }) {
    // 上报到分析平台
    navigator.sendBeacon('/api/metrics', JSON.stringify({
      name: metric.name,
      value: metric.value,
      url: window.location.pathname,
      userAgent: navigator.userAgent,
    }))
  }
 
  onCLS(reportMetric)
  onFID(reportMetric)
  onLCP(reportMetric)
  onTTFB(reportMetric)
})

RUM 的价值在于看到真实用户的分布——P50 用户体验可能很好,但 P95 用户(网络差、设备旧)可能很差。

5. 告警配置

5.1 告警原则

  • 可操作:每条告警都应该有明确的处理步骤
  • 不漏报:关键错误必须触发告警
  • 不误报:频繁的无意义告警会导致"告警疲劳"——人们开始忽略所有告警
  • 分级:P0(立即处理)、P1(24 小时内处理)、P2(下个迭代处理)

5.2 Sentry 告警规则

规则 1:错误率飙升
  条件:5 分钟内同一错误出现 > 10 次
  操作:发送 Slack 通知 + PagerDuty

规则 2:新错误
  条件:出现之前未见过的错误类型
  操作:发送 Slack 通知

规则 3:关键业务错误
  条件:标签 business.critical = true
  操作:发送 Slack + 邮件 + PagerDuty

5.3 告警渠道

渠道适用场景响应时效
Slack/飞书团队协作通知工作时间内
邮件记录和追踪24 小时
PagerDuty/OpsGenie紧急事件升级立即
短信/电话P0 级别故障立即

5.4 On-Call 轮值

对于需要 24/7 可用的服务,建立 On-Call 轮值制度:

  • 每周一位工程师值班,负责处理告警
  • 非工作时间通过电话告警
  • 如果值班人员 10 分钟未响应,自动升级到下一位
  • 事后复盘每次告警,优化告警规则

6. 可观测性全景

6.1 三支柱

支柱回答的问题工具
Metrics"系统现在怎么样?"Prometheus + Grafana
Logs"发生了什么?"Loki / Elasticsearch
Traces"请求经过了哪些服务?"Sentry Tracing / Jaeger

6.2 Nuxt4 可观测性栈推荐

层级工具用途
错误监控Sentry前端 + 服务端错误捕获
性能监控Sentry Performance / web-vitalsCore Web Vitals + API 延迟
日志consola + Loki结构化日志收集
CI 性能Lighthouse CI防止性能退化
告警Sentry Alerts + Slack错误率 + 性能阈值告警

本章小结

  • Sentry 双端集成:客户端自动捕获 JS 异常和 Vue 渲染错误,服务端捕获 Nitro 和 SSR 错误,Source Maps 映射回源代码
  • 结构化日志:JSON 格式 + 请求 ID 串联链路,按级别过滤(生产环境 info 及以上)
  • 日志纪律:记录关键业务事件和错误,不记录敏感信息和大量重复日志
  • Lighthouse CI:在 CI 中自动审计性能,低于阈值阻止合并
  • RUMweb-vitals 采集真实用户数据,关注 P95 而非平均值
  • 告警:可操作 + 不漏报 + 不误报,分级处理,On-Call 轮值