SJ blog
devops
A

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

可観測性(Observability)入門:ログ・メトリクス・トレース

Observabilityの3本柱であるLogs・Metrics・Tracesを解説。OpenTelemetry・Prometheus・Grafana・Jaegerを使ったモニタリングスタックの構築方法を紹介します。

一言結論

ログ・メトリクス・トレースの3本柱をOpenTelemetryで統一計装しておくことが、本番障害を「何が起きたか」「どこで起きたか」「なぜ起きたか」の全レイヤーで即座に診断できる組織の前提条件だ。

Observability の3本柱

Logs(ログ)
  何が起きたか。構造化されたイベントの記録。
  → "2026-04-08T10:00:00Z ERROR user_id=123 payment failed: insufficient funds"

Metrics(メトリクス)
  どのくらいの規模で起きているか。時系列の数値データ。
  → HTTP リクエスト数・エラー率・レスポンスタイム・CPU 使用率

Traces(トレース)
  なぜ起きたか。リクエストの処理経路を追跡。
  → API → DB クエリ → 外部API 呼び出しの各レイテンシ

OpenTelemetry — 標準化された計装

OpenTelemetry(OTel)はLogs・Metrics・Traces を統一した標準です。

// Node.js での OpenTelemetry 設定
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: "http://otel-collector:4318/v1/traces",
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();
// Express・http・pg・redis などが自動で計装される

Metrics — Prometheus + Grafana

// prom-client でメトリクスを公開
import { Counter, Histogram, Registry } from "prom-client";
import express from "express";

const registry = new Registry();

// カウンター: 単調増加する値
const httpRequests = new Counter({
  name: "http_requests_total",
  help: "Total HTTP requests",
  labelNames: ["method", "path", "status"],
  registers: [registry],
});

// ヒストグラム: レスポンスタイムの分布
const requestDuration = new Histogram({
  name: "http_request_duration_seconds",
  help: "HTTP request duration",
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
  labelNames: ["method", "path"],
  registers: [registry],
});

// ミドルウェアで計測
app.use((req, res, next) => {
  const end = requestDuration.startTimer({ method: req.method, path: req.route?.path ?? req.path });
  res.on("finish", () => {
    httpRequests.inc({ method: req.method, path: req.path, status: res.statusCode });
    end();
  });
  next();
});

// Prometheus がスクレイプするエンドポイント
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", registry.contentType);
  res.end(await registry.metrics());
});
# prometheus.yml
scrape_configs:
  - job_name: "my-app"
    static_configs:
      - targets: ["app:3000"]
    metrics_path: "/metrics"
    scrape_interval: 15s

Traces — 分散トレーシング

// カスタムスパンを作成
import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("my-app");

async function processOrder(orderId: string) {
  const span = tracer.startSpan("processOrder");
  span.setAttribute("order.id", orderId);

  try {
    const result = await tracer.startActiveSpan("fetchOrderDetails", async (childSpan) => {
      const order = await db.orders.findById(orderId);
      childSpan.setAttribute("order.amount", order.amount);
      childSpan.end();
      return order;
    });

    span.setStatus({ code: SpanStatusCode.OK });
    return result;
  } catch (err) {
    span.recordException(err);
    span.setStatus({ code: SpanStatusCode.ERROR });
    throw err;
  } finally {
    span.end();
  }
}

構造化ログ

// JSON 形式のログ(検索・集計しやすい)
import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  transport: process.env.NODE_ENV === "development"
    ? { target: "pino-pretty" }  // 開発時は読みやすく
    : undefined,                 // 本番は JSON
});

// ❌ 構造化されていないログ
console.log(`User ${userId} failed to login`);

// ✅ 構造化ログ
logger.warn({ userId, reason: "invalid_password", attempts: 3 }, "Login failed");
// → {"level":"warn","userId":"123","reason":"invalid_password","attempts":3,"msg":"Login failed"}

アラートの設計

# Prometheus Alerting Rules
groups:
  - name: app-alerts
    rules:
      - alert: HighErrorRate
        expr: |
          rate(http_requests_total{status=~"5.."}[5m])
          / rate(http_requests_total[5m]) > 0.01
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "エラー率が1%を超えています"

      - alert: SlowResponse
        expr: |
          histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
        for: 5m
        annotations:
          summary: "P95レスポンスタイムが1秒を超えています"

まとめ

ツール用途
OpenTelemetry計装の標準化(ベンダー非依存)
Prometheusメトリクス収集・アラート
Grafanaダッシュボード可視化
Jaeger / Tempo分散トレーシング
Loki / ELKログ集約・検索

観測できないシステムは運用できません。最低限「エラー率」「レスポンスタイム」「スループット」の3指標(RED メソッド)から始めることを推奨します。


参考: OpenTelemetry 公式 / Prometheus 公式