オブザーバビリティ
オブザーバビリティは本番運用の目です。メトリクスは傾向を示し, ログは原因を特定し, トレースは根本原因を突き止めます。この3本柱はすべて不可欠です。
1. 学ぶこと
- Micrometerメトリクス収集:Counter / Gauge / Timer / DistributionSummary
- Prometheusスクレイプ設定とPromQLクエリアラートルール
- Grafanaダッシュボード可視化:JVM / HTTP / データベースメトリクスダッシュボード
- OpenTelemetry分散トレーシングとSpan/Context伝播
- BobはOrderFlowのSLOダッシュボードを構築:P99 < 100ms / エラー率 < 0.1% / 可用性 > 99.9%
2. SREエンジニアの実話
(1) ペインポイント: オンライン問題のブラックボックストラブルシューティング
OrderFlowで本番エラーが発生すると, Bobはアプリケーションログの海を探し回るしかありません。どのAPIが遅いのか?どのサービスに問題があるのか?リクエストがどのマイクロサービスを通過したのか?全く分かりません。AliceとBobは1つの本番問題のトラブルシューティングに4時間費やし, そのうち3時間は問題の場所を「推測」するのに使われます。
(2) オブザーバビリティ3本柱のアプローチ
graph LR
A["オブザーバビリティ<br/>3本柱"] --> B["メトリクス<br/>何が起きた?<br/>Prometheus"]
A --> C["ログ<br/>なぜ起きた?<br/>ELK / Loki"]
A --> D["トレース<br/>どこで起きた?<br/>Jaeger / Zipkin"]
メトリクスで問題を特定し, ログで原因を分析し, トレースで根本原因を突き止めます。
(3) 成果
Bobがオブザーバビリティシステムを構築した後, GrafanaダッシュボードがP99レイテンシとエラー率をリアルタイム表示し, Prometheusアラートが自動通知をトリガーし, OpenTelemetryトレースデータが30秒以内に根本原因を特定。問題解決の平均時間が4時間から15分に短縮されました。
3. Micrometerデータ収集
(1) 4種類のメトリクス
| 型 | 意味 | 増加のみ | 典型的シナリオ |
|---|---|---|---|
| Counter | カウンタ (増加のみ) | はい | 総リクエスト数, 注文作成数 |
| Gauge | 現在値 (増減可能) | いいえ | 現在のコネクション数, キュー長 |
| Timer | 所要時間分布 | いいえ | リクエスト応答時間 |
| DistributionSummary | 分布統計 | いいえ | リクエストボディサイズ分布 |
(1) ▶ サンプル:カスタムビジネスメトリクス
@Service
public class OrderMetrics {
private final Counter orderCreatedCounter;
private final Counter orderCancelledCounter;
private final Timer orderCreationTimer;
private final Gauge pendingOrdersGauge;
public OrderMetrics(MeterRegistry registry, OrderRepository orderRepo) {
this.orderCreatedCounter = Counter.builder("orderflow.orders.created")
.description("Total orders created")
.tag("service", "orderflow")
.register(registry);
this.orderCancelledCounter = Counter.builder("orderflow.orders.cancelled")
.description("Total orders cancelled")
.register(registry);
this.orderCreationTimer = Timer.builder("orderflow.orders.creation.duration")
.description("Order creation duration")
.publishPercentiles(0.5, 0.95, 0.99)
.publishPercentileHistogram()
.register(registry);
this.pendingOrdersGauge = Gauge.builder("orderflow.orders.pending",
orderRepo, repo -> repo.countByStatus("PENDING"))
.description("Current pending orders count")
.register(registry);
}
public void recordOrderCreated() {
orderCreatedCounter.increment();
}
public Timer.Sample startCreationTimer() {
return Timer.start(orderCreationTimer);
}
public void recordCreationComplete(Timer.Sample sample) {
sample.stop(orderCreationTimer);
}
}
出力:
// 実行成功
(2) ▶ サンプル:サービスでのメトリクス使用
@Service
public class OrderServiceImpl implements OrderService {
private final OrderMetrics metrics;
@Override
@Transactional
public Order createOrder(CreateOrderRequest request) {
Timer.Sample sample = metrics.startCreationTimer();
try {
Order order = doCreateOrder(request);
metrics.recordOrderCreated();
return order;
} finally {
metrics.recordCreationComplete(sample);
}
}
}
出力:
// 実行成功
4. Prometheus統合
(1) ▶ サンプル:Prometheus依存関係と設定
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
出力:
// 実行成功
# application.yml
management:
endpoints:
web:
exposure:
include: health,prometheus,metrics
metrics:
tags:
application: ${spring.application.name}
export:
prometheus:
enabled: true
(2) ▶ サンプル:Prometheusスクレイプ設定
# prometheus.yml
scrape_configs:
- job_name: 'orderflow'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
static_configs:
- targets: ['orderflow:8080']
出力:
監視設定が読み込まれました
Prometheus targets: 3 active
Grafana dashboard: ready
(3) ▶ サンプル:PromQLクエリとアラート
# PromQLクエリ
# 注文APIのP99レイテンシ
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{uri=~"/api/v1/orders.*"}[5m])) by (le, uri))
# エラー率 (5xxレスポンス)
sum(rate(http_server_requests_seconds_total{status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_total[5m]))
# 1分あたりの注文数
rate(orderflow_orders_created_total[1m]) * 60
出力:
設定が正常に適用されました
# alert_rules.yml
groups:
- name: orderflow
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_server_requests_seconds_total{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_total[5m])) > 0.001
for: 5m
labels:
severity: critical
annotations:
summary: "OrderFlow error rate exceeds 0.1%"
- alert: HighP99Latency
expr: |
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le)) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "OrderFlow P99 latency exceeds 100ms"
5. Grafanaダッシュボード
(1) 主要ダッシュボードメトリクス
| ダッシュボード | メトリクス | PromQL |
|---|---|---|
| リクエストレート | QPS | sum(rate(http_server_requests_seconds_total[5m])) |
| P50/P95/P99レイテンシ | 応答時間分布 | histogram_quantile(0.99, ...) |
| エラー率 | 5xx比率 | rate(...{status=~"5.."})/rate(...) |
| JVMヒープメモリ | メモリ使用量 | jvm_memory_used_bytes{area="heap"} |
| GCポーズ | GC時間 | rate(jvm_gc_pause_seconds_sum[5m]) |
| HikariCPアクティブコネクション | コネクションプール使用量 | hikaricp_connections_active |
| 注文作成レート | ビジネスメトリクス | rate(orderflow_orders_created_total[1m]) |
(1) ▶ サンプル:GrafanaダッシュボードJSONスニペット
{
"dashboard": {
"title": "OrderFlow Observability",
"panels": [
{
"title": "Request Rate (QPS)",
"type": "timeseries",
"targets": [{
"expr": "sum(rate(http_server_requests_seconds_total{application=\"orderflow-service\"}[5m]))"
}]
},
{
"title": "P99 Latency",
"type": "timeseries",
"targets": [{
"expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{application=\"orderflow-service\"}[5m])) by (le))"
}]
},
{
"title": "Error Rate",
"type": "gauge",
"targets": [{
"expr": "sum(rate(http_server_requests_seconds_total{application=\"orderflow-service\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_total{application=\"orderflow-service\"}[5m]))"
}],
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{"value": 0, "color": "green"},
{"value": 0.001, "color": "red"}
]
}
}
}
}
]
}
}
出力:
{
"dashboard": {
"title": "OrderFlow Observability",
"panels": [
{
"title": "Request Rate (QPS)",
"type": "timeseries",
"targets": [
{
"expr": "sum(rate(http_server_requests_seconds_total{application=\"orderflow-service\"}[5m]))"
}
]
},
{
"title": "P99 Latency",
"type": "timeseries",
"targets": [
{
"expr": "histogram_quantile(0.99, sum(rate(http_server_request
6. OpenTelemetryトレース
(1) 分散トレーシングの概念
graph LR
A["Client"] --> B["API Gateway<br/>Trace: abc123<br/>Span 1"]
B --> C["Order Service<br/>Span 2<br/>parent: Span 1"]
C --> D["Product Service<br/>Span 3<br/>parent: Span 2"]
C --> E["Database<br/>Span 4<br/>parent: Span 2"]
| 概念 | 意味 |
|---|---|
| Trace | 単一リクエストの完全な経路 |
| Span | チェーン内の1操作 |
| Context | Span間を伝播するトレースコンテキスト |
| SpanId | 現在のSpanの一意識別子 |
| TraceId | トレース全体の一意識別子 |
(1) ▶ サンプル:OpenTelemetry依存関係
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
出力:
// 実行成功
# application.yml
otel:
exporter:
otlp:
endpoint: http://otel-collector:4317
resource:
attributes:
service.name: orderflow-service
traces:
exporter: otlp
(2) ▶ サンプル:カスタムSpan
@Service
public class OrderService {
private final Tracer tracer;
public OrderService(Tracer tracer) {
this.tracer = tracer;
}
public Order createOrder(CreateOrderRequest request) {
Span span = tracer.spanBuilder("create-order")
.setAttribute("product.id", request.productId())
.setAttribute("quantity", request.quantity())
.startSpan();
try (Scope scope = span.makeCurrent()) {
Order order = doCreateOrder(request);
span.setAttribute("order.id", order.getId());
return order;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
出力:
// 実行成功
7. 総合サンプル:OrderFlow SLOダッシュボード
# docker-compose.observability.yml
version: "3.9"
services:
prometheus:
image: prom/prometheus:latest
ports: ["9090:9090"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./alert_rules.yml:/etc/prometheus/alert_rules.yml
grafana:
image: grafana/grafana:latest
ports: ["3000:3000"]
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
volumes:
- grafana-data:/var/lib/grafana
otel-collector:
image: otel/opentelemetry-collector:latest
ports: ["4317:4317", "4318:4318"]
volumes:
- ./otel-collector-config.yml:/etc/otelcol/config.yaml
jaeger:
image: jaegertracing/all-in-one:latest
ports: ["16686:16686"]
app:
build: .
ports: ["8080:8080"]
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_SERVICE_NAME: orderflow-service
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: health,prometheus,metrics
| SLOメトリクス | 目標 | アラート閾値 | PromQL |
|---|---|---|---|
| P99レイテンシ | < 100 ms | 5分間 > 100 ms | histogram_quantile(0.99, ...) |
| エラー率 | < 0.1% | 5分間 > 0.1% | rate(5xx)/rate(all) |
| 可用性 | > 99.9% | 5分間 < 99.9% | 1 - rate(5xx)/rate(all) |
| 注文スループット | > 1,000/分 | 10分間 < 500/分 | rate(orders_created)[1m]*60 |
❓ よくある質問
publishPercentilesとhistogram_quantileの違いは?publishPercentilesはアプリケーション側でパーセンタイルを計算 (Prometheusのストレージを節約, 複数インスタンス間では精度が低下)。histogram_quantileはPrometheus側で計算 (複数インスタンスの集約をサポート, より高精度)。本番環境ではPrometheus側での計算を推奨します。📖 まとめ
- 4種のMicrometerメトリクス:Counter (カウント), Gauge (現在値), Timer (経過時間), DistributionSummary (分布)
- Prometheusが
/actuator/prometheusをスクレイプ, PromQLクエリ + アラートルール - GrafanaダッシュボードでHTTP, JVM, コネクションプール, ビジネスメトリクスを表示
- OpenTelemetryが分散トレーシングデータを収集;Jaegerが保存と可視化
- SLOダッシュボード:P99 < 100ms / エラー率 < 0.1% / 可用性 > 99.9%
- 3本柱連携:メトリクスで問題を特定 → トレースで位置を特定 → ログで根本原因を分析
📝 練習問題
-
基本問題 (難易度:⭐): OrderFlowにMicrometer + Prometheusを設定し,
/actuator/prometheusエンドポイントを公開し, 3つのカスタムビジネスメトリクス (注文作成数, 注文処理時間, 保留中の注文数)を定義してください。 -
応用問題 (難易度:⭐⭐): Prometheusのデータ収集を設定し, Grafanaダッシュボードを構築してHTTP QPS, P99レイテンシ, エラー率, JVMヒープメモリを表示してください。2つのPrometheusアラートルール (高エラー率と高レイテンシ)を書いてください。
-
チャレンジ (難易度:⭐⭐⭐): OpenTelemetryとJaegerを統合して分散トレーシングを実装してください。OrderServiceにカスタムSpanを追加して注文フロー全体 (Controller → Service → Repository)をトレースし, Jaeger UIでトレース詳細を確認してください。



