三個支柱: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(pgmysql2、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 → 不上(或 /health endpoint 帶簡單統計)
  • 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 消耗速度太快」——這才是用戶實際感受到的。


延伸閱讀