はじめに
可観測性 = トレース + メトリクス + ロギング。マイクロサービスでは、リクエストは複数のサービスを横断します。OpenTelemetry (OTel) は分散トレースを自動的に収集し、リクエストの過程をシステム全体で追跡できるようにします。
可観測性の 3 つの柱
┌─────────────────────────────────────────────────┐
│ Observability │
├────────────────┬────────────────┬────────────────┤
│ Tracing │ Metrics │ Logging │
│ (Request flow) │ (Aggregated) │ (Events) │
├────────────────┼────────────────┼────────────────┤
│ Jaeger/Tempo │ Prometheus │ Loki/ELK │
│ Zipkin │ Grafana │ Fluentd │
└────────────────┴────────────────┴────────────────┘
↑ All powered by OpenTelemetry
分散トレーシングのセットアップ
依存関係
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
構成
# application.properties
quarkus.otel.enabled=true
quarkus.otel.exporter.otlp.endpoint=http://localhost:4317
quarkus.otel.exporter.otlp.protocol=grpc
# Service name (quan trọng cho tracing)
quarkus.otel.resource.attributes=service.name=product-service,service.version=1.0.0
# Sample rate (1.0 = 100%, production nên giảm)
quarkus.otel.traces.sampler=parentbased_traceidratio
quarkus.otel.traces.sampler.arg=1.0
%prod.quarkus.otel.traces.sampler.arg=0.1
# Propagation
quarkus.otel.propagators=tracecontext,baggage
自動的に計測される
Quarkus OTel は以下を自動的にトレースします。
- REST エンドポイント (受信リクエスト)
- REST クライアント (発信)
- gRPC サーバー/クライアント
- Kafka プロデューサー/コンシューマー
- JDBC/Hibernate クエリ
- CDI Bean
イェーガーで痕跡を確認
# docker-compose.yml
services:
jaeger:
image: jaegertracing/all-in-one:1.53
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
environment:
COLLECTOR_OTLP_ENABLED: true
アクセス: http://localhost:16686 → サービスを選択 → トレースを表示
カスタム スパン
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
@ApplicationScoped
public class ProductService {
@Inject
Tracer tracer;
// Annotation-based
@WithSpan("ProductService.findById")
public ProductDTO getById(
@SpanAttribute("product.id") Long id) {
Product product = productRepo.findByIdOptional(id)
.orElseThrow(() ->
new ResourceNotFoundException("Product", id));
return ProductDTO.from(product);
}
// Programmatic
public List<ProductDTO> search(String keyword) {
Span span = tracer.spanBuilder("product.search")
.setAttribute("search.keyword", keyword)
.startSpan();
try (var scope = span.makeCurrent()) {
List<Product> results =
productRepo.searchFullText(keyword);
span.setAttribute("search.results.count",
results.size());
return results.stream()
.map(ProductDTO::from).toList();
} catch (Exception e) {
span.setStatus(StatusCode.ERROR,
e.getMessage());
span.recordException(e);
throw e;
} finally {
span.end();
}
}
}
トレースコンテキストの伝播
Order Service が Product Service を呼び出すと、トレース ID が自動的に伝播されます。
[Browser] → [Order Service] → [Product Service] → [PostgreSQL]
│ │ │ │
│ Trace: abc123 │ │
│ Span: order-create │ │
│ │ │ │
│ │── REST Client ─→ │ │
│ │ traceparent: │ │
│ │ abc123 │ │
│ │ │── DB Query ─→ │
│ │ │ Span: SELECT │
マイクロメーターのメトリクス
依存関係
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
組み込みメトリクス
Quarkus は独自のメトリクスを次の場所で公開しています。 /q/metrics:
curl http://localhost:8081/q/metrics
# HTTP metrics
http_server_requests_seconds_count{method="GET",uri="/api/v1/products",status="200"} 150
http_server_requests_seconds_sum{method="GET",uri="/api/v1/products",status="200"} 12.5
# JVM metrics
jvm_memory_used_bytes{area="heap"} 134217728
jvm_threads_live_threads 25
# DB Connection Pool
agroal_active_count{datasource="default"} 5
agroal_available_count{datasource="default"} 15
カスタムメトリクス
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.Gauge;
@ApplicationScoped
public class ProductService {
private final Counter productCreatedCounter;
private final Counter productViewCounter;
private final Timer searchTimer;
@Inject
public ProductService(MeterRegistry registry,
ProductRepository productRepo) {
this.productCreatedCounter = Counter.builder(
"products.created.total")
.description("Total products created")
.register(registry);
this.productViewCounter = Counter.builder(
"products.views.total")
.description("Total product views")
.tag("type", "detail")
.register(registry);
this.searchTimer = Timer.builder("products.search.time")
.description("Product search duration")
.register(registry);
// Gauge — current value
Gauge.builder("products.active.count",
productRepo, repo -> repo.count("status", "ACTIVE"))
.description("Number of active products")
.register(registry);
}
public ProductDTO getById(Long id) {
productViewCounter.increment();
// ...
}
@Transactional
public ProductDTO create(CreateProductRequest req) {
// ...
productCreatedCounter.increment();
return ProductDTO.from(product);
}
public List<ProductDTO> search(String keyword) {
return searchTimer.record(() -> {
// actual search logic
return productRepo.searchFullText(keyword)
.stream().map(ProductDTO::from).toList();
});
}
}
時間指定の注釈
import io.micrometer.core.annotation.Timed;
import io.micrometer.core.annotation.Counted;
@Timed(value = "order.creation.time",
description = "Time to create an order")
@Counted(value = "order.created.count",
description = "Orders created")
@Transactional
public OrderDTO createOrder(CreateOrderRequest request) {
// ...
}
プロメテウス + グラファナ スタック
# docker-compose.yml
services:
prometheus:
image: prom/prometheus:v2.49.0
ports: ["9090:9090"]
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:10.3.0
ports: ["3001:3000"]
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
volumes:
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning
プロメテウス.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'product-service'
metrics_path: /q/metrics
static_configs:
- targets: ['host.docker.internal:8081']
- job_name: 'order-service'
metrics_path: /q/metrics
static_configs:
- targets: ['host.docker.internal:8082']
- job_name: 'payment-service'
metrics_path: /q/metrics
static_configs:
- targets: ['host.docker.internal:8083']
構造化ログ - JSON
# JSON logging cho production
%prod.quarkus.log.console.json=true
%prod.quarkus.log.console.json.additional-field.service.value=product-service
%prod.quarkus.log.console.json.additional-field.environment.value=${ENV:dev}
# Correlation via Trace ID
quarkus.log.console.format=%d{HH:mm:ss} %-5p traceId=%X{traceId} [%c{2.}] (%t) %s%e%n
演習
- OpenTelemetry 拡張機能を追加し、Jaeger へのエクスポートを構成します
- カスタム スパンを作成します。
@WithSpanおよびプログラマティックトレーサー - マイクロメーターのメトリクスを追加: カウンター、タイマー、ゲージ
- Docker Compose を使用して Prometheus + Grafana スタックをデプロイする
- リクエスト レート、エラー レート、レイテンシ (RED メトリクス) を表示する Grafana ダッシュボードを作成します。
- 注文サービス → 製品サービス → DB を経由する分散トレースを作成します。
概要
- OpenTelemetry — 分散トレーシング、自動インストルメンテーションの標準
- Jaeger/Tempo トレースを視覚化します — サービス間のリクエストの過程を確認します
@WithSpan+ プログラマティックTracerカスタムスパンの場合- Micrometer は次の場所でメトリクスを公開します
/q/metrics→プロメテウス削り - RED メトリクス: レート、エラー、期間 - 最も重要なダッシュボード
- 構造化された JSON ログ + ログ集約のためのトレース ID の関連付け
次の記事: キャッシュ、ヘルスチェック、API ゲートウェイ。