三個支柱:Logs / Metrics / Traces
Logs:離散的事件記錄。告訴你「發生了什麼」。
2026-04-22T10:30:00Z ERROR Payment failed userId=abc orderId=xyz amount=5000
Metrics:持續測量的數值。告訴你「系統現在的狀態」。
http_request_duration_seconds{method="POST",route="/orders",status="200"} 0.23
active_connections 142
queue_size{queue="email"} 38
Traces:一個 request 跨越多個服務的完整路徑。告訴你「這個 request 在哪裡慢了」。
POST /orders [total: 230ms]
├─ authenticate [8ms]
├─ validate [2ms]
├─ orderService.create [180ms]
│ ├─ DB: INSERT orders [45ms]
│ ├─ DB: UPDATE inventory [120ms] ← 這裡慢
│ └─ queue.add email [5ms]
└─ serialize [3ms]
三個一起才完整:metrics 說「/orders 的 p99 latency 上升了」,tracing 說「是 inventory UPDATE 慢了」,log 說「那個時間點 PostgreSQL lock wait 超時」。
OpenTelemetry:統一的標準
OpenTelemetry(OTel)是 CNCF 的 observability 標準——一套 SDK,同時收 metrics、traces、logs,送到任何支援 OTel 的後端(Jaeger、Tempo、Datadog、Honeycomb)。
好處:換監控平台不需要改 code,只改 exporter。
// src/telemetry.ts(在 app.ts import 之前初始化)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, // Jaeger / Tempo
}),
metricReader: new PrometheusExporter({ port: 9464 }), // /metrics endpoint
instrumentations: [
getNodeAutoInstrumentations(), // 自動 instrument HTTP、DB、Redis
],
serviceName: 'order-service',
});
sdk.start();
// 優雅關機時 flush
process.on('SIGTERM', () => sdk.shutdown());getNodeAutoInstrumentations() 自動 instrument:
- HTTP request / response(
http/express) - DB query(
pg、mysql2、Sequelize、Prisma) - Redis 操作(
ioredis) - gRPC call
大部分情況不需要手動加 span,自動 instrument 就夠了。
Metrics:Prometheus 格式
Prometheus 是 metrics 的事實標準。暴露一個 /metrics endpoint,Prometheus server 定期來抓:
import { metrics } from '@opentelemetry/api';
const meter = metrics.getMeter('order-service');
// Counter:只增不減(請求數、錯誤數)
const requestCounter = meter.createCounter('http_requests_total', {
description: 'Total HTTP requests',
});
// Histogram:分佈(latency、request size)
const requestDuration = meter.createHistogram('http_request_duration_seconds', {
description: 'HTTP request duration in seconds',
unit: 's',
boundaries: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
// Gauge:可增可減(active connections、queue size)
const activeConnections = meter.createObservableGauge('active_connections');
activeConnections.addCallback((result) => {
result.observe(server.connections);
});
// 在 middleware 裡記錄
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
requestCounter.add(1, {
method: req.method,
route: req.route?.path ?? 'unknown',
status: res.statusCode.toString(),
});
requestDuration.record(duration, {
method: req.method,
route: req.route?.path ?? 'unknown',
});
});
next();
});常用的業務 metrics:
// 訂單相關
const ordersCreated = meter.createCounter('orders_created_total');
const orderAmount = meter.createHistogram('order_amount', { unit: 'TWD' });
// Queue 相關
const queueDepth = meter.createObservableGauge('queue_depth');
const jobDuration = meter.createHistogram('job_duration_seconds');
// 外部 API 相關
const externalApiDuration = meter.createHistogram('external_api_duration_seconds');
const externalApiErrors = meter.createCounter('external_api_errors_total');Tracing:分散式追蹤
自動 instrument 不夠的地方,手動加 span:
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service');
async function processPayment(orderId: string, amount: number) {
// 建立一個 span,記錄這個操作的詳情
return tracer.startActiveSpan('payment.process', async (span) => {
span.setAttributes({
'payment.order_id': orderId,
'payment.amount': amount,
'payment.provider': 'stripe',
});
try {
const result = await stripeClient.charge(amount);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
});
}Trace Context Propagation(跨服務傳遞 trace ID):
// OTel 自動在 HTTP header 裡帶 trace context
// W3C Trace Context: traceparent: 00-{trace-id}-{span-id}-01
// 如果要手動取 current trace ID(放進 log)
import { trace } from '@opentelemetry/api';
function getCurrentTraceId(): string | undefined {
return trace.getActiveSpan()?.spanContext().traceId;
}
// 在 log 裡加 trace ID,讓 log 和 trace 可以關聯
logger.info('Payment processed', {
orderId,
traceId: getCurrentTraceId(), // Grafana / Loki 可以從這裡跳到 Tempo
});實務:最小可觀測性配置
不是所有系統都需要完整的 OTel 配置。按規模選:
小型專案(單機):
- Logs → stdout + 檔案(winston + file transport)
- Metrics → 不上(或
/healthendpoint 帶簡單統計) - Traces → 不上
中型(多 pod、有 staging):
- Logs → Loki(Grafana Cloud 免費 tier 夠)
- Metrics → Prometheus + Grafana(k8s 上用 kube-prometheus-stack)
- Traces → Tempo(Grafana 生態,跟 Loki 整合好)
大型(多服務、有 SLA):
- 全套 OTel SDK + 自選 backend(Datadog / Honeycomb / Grafana)
Alerting(告警)
Metrics 的核心價值不是看 dashboard,是在異常時告警:
# Prometheus alerting rule
groups:
- name: order-service
rules:
- alert: HighErrorRate
expr: |
rate(http_requests_total{status=~"5.."}[5m]) /
rate(http_requests_total[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "Error rate > 5% for 2 minutes"
- alert: HighLatency
expr: |
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "p99 latency > 2s"SLO(Service Level Objective)導向的告警:不是告警「CPU > 80%」,是告警「error budget 消耗速度太快」——這才是用戶實際感受到的。
