场景:用户报告"结账很慢"
📈 指标发现 P99 延迟从 200ms 跳到 3s → 确认有性能问题
🔗 追踪找到慢的 Span:支付 API 调用耗时 2.8s → 定位到具体服务
📝 日志显示支付服务返回 "Connection timeout to DB" → 找到根因
OpenTelemetry (OTel) 是 CNCF 的可观测性标准,被 90+ 厂商支持。它解决的核心问题是:不要被任何一家监控厂商锁定。
来源:opentelemetry.io(2026-05)。OTel 支持 Traces、Metrics、Logs 三种信号,覆盖 11+ 语言的 SDK。
OTel Collector 是供应商无关的遥测数据处理和导出管道——换后端只需改配置,不用改代码
// 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-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 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)
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
难以搜索、聚合、分析
{
"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"
}
可搜索、可聚合、可关联追踪
// 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 | 极详细追踪 | ❌ 通常关闭 | 函数入参出参、完整堆栈 |
http_requests_total
http_errors_total
http_request_duration_seconds
| 维度 | 含义 | 示例指标 |
|---|---|---|
| Utilization | 资源使用率 | CPU 使用率、内存使用率 |
| Saturation | 资源饱和度(排队程度) | 磁盘 I/O 等待、连接池满 |
| Errors | 错误数 | OOM kill、磁盘满 |
// 使用 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()
当请求穿过多个服务时,追踪告诉你"时间都花在哪了"。
一眼看出: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%、配额接近 |
# 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"
技术监控之外,SaaS 还需要关注业务指标——它们直接反映产品健康度。
# 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:
100 条告警/天 = 0 条告警/天。人会对噪音脱敏。
解法 只告警需要人为干预的事。删掉所有"只是提醒"级别的告警,改成仪表板。
有仪表板但没有告警规则——需要人盯着屏幕才能发现问题。
解法 关键指标必须配告警,不是"上线再看"。
"平均延迟 200ms"——但 P99 可能 10s。1% 的用户体验极差。
解法 用 P50/P95/P99 分布,不要只看 avg。
微服务架构下,靠日志 grep 找问题效率极低。
解法 至少接入基础的分布式追踪。
Datadog 功能强大但按主机收费,小团队起步 $15/主机/月 × 10 主机 = $150/月。
解法 先用 Grafana Cloud Free + Sentry Free,够用了再升级。
所有日志都是 INFO,无法区分重要性。真正的问题淹没在噪音中。
解法 严格日志级别:ERROR=需要人看、WARN=潜在问题、INFO=业务事件。