📊 监控:日志 / 指标 / 追踪 / 告警

你不能改善你无法衡量的东西——可观测性是 SaaS 运维的生命线
📑 目录

可观测性三支柱

📝
日志 (Logs)
离散事件记录
出了什么问题?
📈
指标 (Metrics)
聚合数值时间序列
趋势如何?
🔗
追踪 (Traces)
请求链路追踪
问题在哪?
三种信号的协作关系

场景:用户报告"结账很慢"

📈 指标发现 P99 延迟从 200ms 跳到 3s → 确认有性能问题

🔗 追踪找到慢的 Span:支付 API 调用耗时 2.8s → 定位到具体服务

📝 日志显示支付服务返回 "Connection timeout to DB" → 找到根因

OpenTelemetry:统一标准

OpenTelemetry (OTel) 是 CNCF 的可观测性标准,被 90+ 厂商支持。它解决的核心问题是:不要被任何一家监控厂商锁定

来源:opentelemetry.io(2026-05)。OTel 支持 Traces、Metrics、Logs 三种信号,覆盖 11+ 语言的 SDK。

OTel 架构
应用代码 OTel SDK (自动/手动埋点) OTel Collector 后端 (Grafana/Datadog/Jaeger/...)

OTel Collector 是供应商无关的遥测数据处理和导出管道——换后端只需改配置,不用改代码

Next.js 接入 OTel 示例

// next.config.js
module.exports = {
  experimental: {
    instrumentationHook: true,
  },
}

// instrumentation.ts (Next.js 14+)
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { NodeSDK } = await import('@opentelemetry/sdk-node')
    const { OTLPTraceExporter } = await import(
      '@opentelemetry/exporter-trace-otlp-http'
    )
    const { Resource } = await import('@opentelemetry/resources')
    const { ATTR_SERVICE_NAME } = await import(
      '@opentelemetry/semantic-conventions'
    )

    const sdk = new NodeSDK({
      resource: new Resource({
        [ATTR_SERVICE_NAME]: 'my-saas-web',
      }),
      traceExporter: new OTLPTraceExporter({
        url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
      }),
    })
    sdk.start()
  }
}

OTel Collector 配置

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024
  filter:
    error_mode: ignore
    traces:
      span:
        - 'attributes["http.route"] == "/health"'

exporters:
  # 导出到 Grafana Tempo (追踪)
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true
  # 导出到 Prometheus (指标)
  prometheus:
    endpoint: 0.0.0.0:8889
  # 导出到 Loki (日志)
  loki:
    endpoint: http://loki:3100/loki/api/v1/push

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, filter]
      exporters: [otlp/tempo]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [loki]

平台对比:Grafana / Datadog / Sentry / 自建

维度 Grafana Cloud Datadog Sentry 自建 (Grafana 栈)
定位 全栈可观测性平台 全栈可观测性 + 安全 错误追踪 + 性能 完全自控
日志 ✅ Loki ✅ 强大 ❌ 不提供 ✅ Loki
指标 ✅ Prometheus/Mimir ✅ 强大 ⚠️ 有限 ✅ Prometheus
追踪 ✅ Tempo ✅ APM ✅ 分布式追踪 ✅ Tempo/Jaeger
错误追踪 ⚠️ 需配置 ✅ APM 错误追踪 ✅✅ 最强 ⚠️ 需自建
告警 ✅ 统一告警 ✅ 智能告警 ✅ 错误告警 ✅ Alertmanager
免费额度 10K series + 50GB logs + 50GB traces 5 主机免费试用 5K errors/月 + 1 auth 无限(自建)
起步价 $0 → $19/月 + 用量 $15/主机/月起 $0 → $26/月 (Team) VPS $20-50/月
OTel 支持 ✅ 原生 ✅ 支持 ✅ 支持 ✅ 原生
供应商锁定 开源组件 专有格式

来源:grafana.com/pricing(2026-05)、datadoghq.com/pricing(2026-05)、sentry.io/pricing(2026-05)

💡 性价比推荐:小团队用 Grafana Cloud Free + Sentry Free,覆盖全部三个支柱 + 错误追踪。等业务增长后再升级 Grafana Pro。

日志:结构化日志与聚合

为什么必须结构化日志

❌ 非结构化日志
2026-05-18 10:23:45 ERROR - Payment failed for user 123
2026-05-18 10:23:45 INFO - Retrying payment...
2026-05-18 10:23:46 ERROR - Payment failed again

难以搜索、聚合、分析

✅ 结构化日志 (JSON)
{
  "ts": "2026-05-18T10:23:45.123Z",
  "level": "error",
  "msg": "Payment failed",
  "userId": "123",
  "paymentId": "pay_abc",
  "error": "Connection timeout",
  "duration_ms": 5000,
  "traceId": "abc123def456",
  "spanId": "789ghi"
}

可搜索、可聚合、可关联追踪

结构化日志实现(Next.js + pino)

// lib/logger.ts
import pino from 'pino'

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  // 生产环境用 JSON,开发环境用 pretty
  transport: process.env.NODE_ENV !== 'production'
    ? { target: 'pino-pretty' }
    : undefined,
})

// 使用
logger.info({ userId: '123', action: 'checkout' }, 'Checkout started')
logger.error({ err, userId: '123', paymentId: 'pay_abc' }, 'Payment failed')

// 关联 Trace ID(OTel 集成)
import { trace, context } from '@opentelemetry/api'

function getLoggerWithTrace() {
  const span = trace.getSpan(context.active())
  const spanContext = span?.spanContext()
  return logger.child({
    traceId: spanContext?.traceId,
    spanId: spanContext?.spanId,
  })
}

日志级别策略

级别用途生产是否开启示例
error需要立即处理的问题✅ 始终支付失败、数据库连接断开
warn潜在问题,不影响主流程✅ 始终请求慢、重试成功、配额接近
info关键业务事件✅ 始终用户注册、订单完成、部署
debug调试信息⚠️ 按需请求参数、SQL 查询
trace极详细追踪❌ 通常关闭函数入参出参、完整堆栈

指标:RED / USE 方法论

RED 方法(适用于请求驱动型服务/API)

📊
Rate
请求速率
每秒多少请求
http_requests_total
Errors
错误率
失败请求占比
http_errors_total
⏱️
Duration
延迟分布
P50/P95/P99
http_request_duration_seconds

USE 方法(适用于基础设施/资源)

维度含义示例指标
Utilization资源使用率CPU 使用率、内存使用率
Saturation资源饱和度(排队程度)磁盘 I/O 等待、连接池满
Errors错误数OOM kill、磁盘满

Prometheus 指标类型

// 使用 prom-client (Node.js)
import client from 'prom-client'

// Counter:只增不减(请求数、错误数)
const httpRequestsTotal = new client.Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'route', 'status'],
})

// Histogram:分布(延迟、大小)
const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration',
  labelNames: ['method', 'route'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
})

// Gauge:可增可减(当前连接数、队列长度)
const activeConnections = new client.Gauge({
  name: 'active_connections',
  help: 'Current active connections',
})

// 使用
httpRequestsTotal.inc({ method: 'GET', route: '/api/users', status: 200 })
httpRequestDuration.observe({ method: 'GET', route: '/api/users' }, 0.234)
activeConnections.inc()

分布式追踪

当请求穿过多个服务时,追踪告诉你"时间都花在哪了"。

🔗 一个请求的追踪链路
GET /checkout — 3.2s
auth-service: verifyToken — 12ms ✅
cart-service: getCart — 45ms ✅
payment-service: charge — 2.8s ⚠️
stripe-api: createCharge — 2.5s ⚠️
db: insert transaction — 300ms
email-service: sendReceipt — 80ms ✅

一眼看出:Stripe API 调用是瓶颈

手动埋点示例

import { trace } from '@opentelemetry/api'

const tracer = trace.getTracer('my-saas')

async function checkout(userId: string, cartId: string) {
  return tracer.startActiveSpan('checkout', async (span) => {
    span.setAttribute('userId', userId)
    span.setAttribute('cartId', cartId)

    try {
      const cart = await getCart(cartId)  // 自动传播 trace context
      const charge = await chargePayment(userId, cart.total)
      await createOrder(userId, cart, charge)
      span.setStatus({ code: 1 }) // OK
      return { success: true }
    } catch (err) {
      span.setStatus({ code: 2, message: err.message }) // ERROR
      span.recordException(err)
      throw err
    } finally {
      span.end()
    }
  })
}

告警策略

告警分级

严重程度含义通知方式响应时间示例
P1 Critical 服务完全不可用 📱 电话 + 短信 + PagerDuty 5 分钟内 主站点 5xx > 50%
P2 Warning 性能退化或部分故障 💬 Slack/飞书 + 邮件 30 分钟内 P99 延迟 > 3s、错误率 > 5%
P3 Info 需要关注但不紧急 📧 邮件 / Slack 下一个工作日 磁盘使用 > 80%、配额接近

好的告警 vs 坏的告警

✅ 好的告警
  • 基于用户体验的 SLO("1% 用户受影响")
  • 可操作——收到后知道该做什么
  • 低噪音——每周 < 5 次
  • 包含上下文(哪个服务、多久、影响谁)
  • 有 runbook(操作手册)链接
❌ 坏的告警
  • 基于资源阈值("CPU > 80%")——可能正常
  • 告警疲劳——每小时 10+ 次,大家忽略
  • 没有上下文——"Error rate high" 哪个服务?
  • 无法操作——收到也不知道怎么办
  • 重复告警——同一条告警发 5 次

Grafana 告警规则示例

# Prometheus/Grafana Alert Rule
# 基于 SLO 的错误率告警
- alert: HighErrorRate
  expr: |
    (
      sum(rate(http_requests_total{status=~"5.."}[5m]))
      /
      sum(rate(http_requests_total[5m]))
    ) > 0.05
  for: 3m
  labels:
    severity: warning
  annotations:
    summary: "High error rate on {{ $labels.service }}"
    description: |
      Error rate is {{ $value | humanizePercentage }}
      for service {{ $labels.service }}.
      SLO target: < 1% errors.
      Runbook: https://wiki/internal/runbooks/high-error-rate
    dashboard: "https://grafana.example.com/d/api-overview"
💡 SLO 告警公式:用"错误预算消耗速率"而非简单阈值告警。例如:30 天 SLO 99.9%,当前错误预算已消耗 14.4%(= 过去 1 小时消耗了 30 天预算的 14.4%),意味着按此速率 7 小时内 SLO 就会被打破。这种告警比"错误率 > 1%"更有预测性。

SaaS 业务监控

技术监控之外,SaaS 还需要关注业务指标——它们直接反映产品健康度。

💰 收入指标
  • MRR:月度经常性收入
  • ARR:年度经常性收入 = MRR × 12
  • ARPU:每用户平均收入
  • Churn Rate:月流失率
  • LTV:客户生命周期价值
👥 用户指标
  • DAU/MAU:日/月活跃用户
  • Signups:每日新注册
  • Activation Rate:注册→激活转化率
  • Feature Adoption:功能采用率
  • Session Duration:会话时长

技术 + 业务指标仪表板布局

错误率
0.3%
P99 延迟
1.2s
在线用户
1,234
今日注册
+56
今日收入
$2,340
MRR
$45K

实战:从零搭建监控

阶段 1:Day 1 基础($0)

# compose.yaml — 添加监控栈
services:
  # ... 你的应用服务 ...

  # OTel Collector
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    ports:
      - "4317:4317"  # gRPC
      - "4318:4318"  # HTTP
    volumes:
      - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml

  # Prometheus
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  # Grafana
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    volumes:
      - grafanadata:/var/lib/grafana

  # Loki (日志)
  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"

volumes:
  grafanadata:

阶段 2:成长期($0-50/月)

阶段 3:规模化($200+/月)

反模式

❌ 告警疲劳

100 条告警/天 = 0 条告警/天。人会对噪音脱敏。

解法 只告警需要人为干预的事。删掉所有"只是提醒"级别的告警,改成仪表板。

❌ 监控但不告警

有仪表板但没有告警规则——需要人盯着屏幕才能发现问题。

解法 关键指标必须配告警,不是"上线再看"。

❌ 只看平均值

"平均延迟 200ms"——但 P99 可能 10s。1% 的用户体验极差。

解法 用 P50/P95/P99 分布,不要只看 avg。

❌ 没有追踪只有日志

微服务架构下,靠日志 grep 找问题效率极低。

解法 至少接入基础的分布式追踪。

❌ 过早 Datadog

Datadog 功能强大但按主机收费,小团队起步 $15/主机/月 × 10 主机 = $150/月。

解法 先用 Grafana Cloud Free + Sentry Free,够用了再升级。

❌ 日志级别全是 INFO

所有日志都是 INFO,无法区分重要性。真正的问题淹没在噪音中。

解法 严格日志级别:ERROR=需要人看、WARN=潜在问题、INFO=业务事件。

🔗 相关内容

🔄 CI/CD

部署后的健康检查

🐳 容器化

容器日志与指标采集

📈 产品分析

用户行为 vs 技术指标

🏗️ 架构模式

微服务的监控挑战

⚡ 缓存

缓存命中率监控

🤖 Agent 架构

AI Agent 的可观测性