100日チャレンジ day40 (マイクロサービスにおけるOpenTelemetryとGrafana Stackでの可視化)
昨日
100日チャレンジに感化されたので、アレンジして自分でもやってみます。
やりたいこと
- 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
- 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
- 生成aiをつかったバイブコーティングになれる
- 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)
できたもの

Grafana Stack と OpenTelemetry を学ぶ
承知いたしました。それでは、この OpenTelemetry と Grafana Stack を使ったGoマイクロサービス風アプリケーションの学習プロジェクトを Day40 として進めます。
プロジェクト名は day40_otel_grafana_go に変更します。
改訂版プロジェクト概要 (Day40):
-
プロジェクト名:
day40_otel_grafana_go -
UI兼Gateway Service (Go):
- ポート
8080で動作。 -
net/httpとhtml/templateを使用。 -
/uiでシンプルな操作ページを提供。ボタンを押すと、後述の各マイクロサービスを呼び出す。 - Otel で計装。
- ポート
-
Product Service (Go):
- ポート
8081で動作。商品情報の取得や検証を行う架空のサービス。Otel で計装。
- ポート
-
Inventory Service (Go):
- ポート
8082で動作。在庫の確認や引き当てを行う架空のサービス。Otel で計装。
- ポート
-
Order Service (Go):
- ポート
8083で動作。注文の確定処理を行う架空のサービス。Otel で計装。
- ポート
-
OpenTelemetry (Otel) 計装:
- トレース: Go SDK を使用し、OTLPエクスポーター経由で Grafana Tempo に送信。
-
メトリクス: Go SDK を使用し、各サービスが
/metricsを公開、Prometheus がスクレイプ。 -
ログ: Go標準の
logまたはslogを使用し、JSON形式で標準出力。Promtail が収集し Grafana Loki に送信。
-
Grafana Stack (Docker Compose):
- Grafana, Loki, Tempo, Prometheus, Promtail。
具体的な作業順序 (Day40):
-
プロジェクト初期化:
-
templateディレクトリをコピーしてday40_otel_grafana_goを作成。 - Next.js 関連のファイルを削除。
-
package.jsonのnameをday40_otel_grafana_goに変更。 -
day40_otel_grafana_go直下にREADME.mdを作成・初期化。 - 各Goサービス用ディレクトリ (
gateway_service,product_service,inventory_service,order_service) をday40_otel_grafana_go/配下に作成。 - 各サービスディレクトリ内で
go mod init github.com/lirlia/100day_challenge_backend/day40_otel_grafana_go/xxx_serviceを実行。 -
day40_otel_grafana_go/ルートにgo.workファイルを作成し、各サービスモジュールを登録 (go work init ./gateway_service ./product_service ./inventory_service ./order_service)。
-
-
Grafana Stack (Docker Compose) のセットアップ:
-
day40_otel_grafana_go/docker-compose.ymlを作成 (Grafana, Loki, Tempo, Prometheus, Promtail)。 -
day40_otel_grafana_go/prometheus/prometheus.ymlを作成。 -
day40_otel_grafana_go/promtail/promtail-config.ymlを作成。
-
-
各Goマイクロサービスの基本的なHTTPサーバー実装 (Otel計装なし):
-
gateway_service:-
/uiエンドポイント (HTMLフォームを提供)。 -
/execute-orderエンドポイント (他のサービスを呼び出すロジック、最初は固定レスポンスでも可)。
-
-
product_service,inventory_service,order_service:- 簡単なHTTPエンドポイントを1つずつ作成 (例:
/products,/inventory,/orders)。固定レスポンスを返す。
- 簡単なHTTPエンドポイントを1つずつ作成 (例:
-
-
サービス間連携の実装:
-
gateway_serviceの/execute-orderから、他の各サービスのエンドポイントをHTTPで呼び出すように実装。
-
-
OpenTelemetry 計装 (トレース):
- 各GoサービスにOtel SDK (
go.opentelemetry.io/otel,go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc等) を追加。 - トレースプロバイダーとOTLPエクスポーター (Tempo向け) を初期化する共通関数を作成 (例:
common/tracing/tracing.go)。 -
otelhttpを使用して各サービスのHTTPサーバーとクライアントを計装。 - 手動でカスタムスパンを追加してみる (例: 特定の処理区間)。
- 各GoサービスにOtel SDK (
-
OpenTelemetry 計装 (メトリクス):
- 各Goサービスに Otel SDK (メトリクス関連) を追加。
- Prometheusエクスポーター (
go.opentelemetry.io/otel/exporters/prometheus) をセットアップし、各サービスが/metricsエンドポイントを公開するようにする。 - 基本的なHTTPメトリクス (リクエスト数、レイテンシ) を収集。カスタムメトリクスも試す。
-
OpenTelemetry 計装 (ログ):
- 各Goサービスで、リクエスト情報や処理結果を含むログをJSON形式で標準出力 (
slogのNewJSONHandler(os.Stdout, ...)など)。 - (PromtailがDockerログを収集する設定になっていれば、特別なOtelログエクスポーターは不要な場合が多い)
- 各Goサービスで、リクエスト情報や処理結果を含むログをJSON形式で標準出力 (
-
動作確認とGrafanaでの可視化:
- 全サービスとGrafana Stackを
docker-compose up -dで起動。 -
gateway_serviceの/uiにアクセスし、操作を実行。 - Grafanaで以下を確認:
- Tempo: 分散トレースが正しく表示されるか。サービス間の呼び出し関係が追えるか。
- Prometheus: 各サービスのメトリクスが収集され、クエリできるか。
- Loki: 各サービスのログが収集され、クエリできるか。
- Grafana で簡単なダッシュボードを作成。
- 全サービスとGrafana Stackを
-
テストとドキュメント化:
-
curlコマンドなどで各エンドポイントをテスト。 -
README.mdにセットアップ手順、アーキテクチャ図、各サービスの説明、Otel設定のポイント、Grafanaでの確認方法などを記述。 -
.cursor/rules/knowledge.mdcを更新。
-
いくつか手順についてレビューです、手順に反映してください
- Grafana Stack (Docker Compose) のセットアップができたら起動確認を実施して
- Go のマイクロサービスができたらその都度、API テストを行ってください
- サービス間の連携も問題なくできるかテストを行ってください
- トレース、メトリクス、ログを一気に対応していますが、トレース実装→投げているかのテスト→受け取れているかのテスト をし確認できてから次に進むようにしてください
- 各工程ごとにテストを行い、成功を確認したらコミットし作業を進めてください
上記の内容で改善した手順を出力してください。その後私が ok といったら作業を始めてください
はい、承知いたしました。ご指摘いただいたレビュー点を反映し、より詳細で段階的な作業手順に改善します。
改訂版・詳細作業順序 (Day40):
-
プロジェクト初期化:
-
templateディレクトリをコピーしてday40_otel_grafana_goを作成。 - Next.js 関連のファイルを削除。
-
package.jsonのnameをday40_otel_grafana_goに変更。 -
day40_otel_grafana_go直下にREADME.mdを作成・初期化。 - 各Goサービス用ディレクトリ (
gateway_service,product_service,inventory_service,order_service) をday40_otel_grafana_go/配下に作成。 - 各サービスディレクトリ内で
go mod init github.com/lirlia/100day_challenge_backend/day40_otel_grafana_go/xxx_serviceを実行。 -
day40_otel_grafana_go/ルートにgo.workファイルを作成し、各サービスモジュールを登録。 - テスト: ディレクトリ構造、ファイルが正しく作成されたか確認。
-
コミット:
day40: step 1/11 Project initialization
-
-
Grafana Stack (Docker Compose) のセットアップ:
-
day40_otel_grafana_go/docker-compose.ymlを作成 (Grafana, Loki, Tempo, Prometheus, Promtail)。 -
day40_otel_grafana_go/prometheus/prometheus.ymlを作成。 -
day40_otel_grafana_go/promtail/promtail-config.ymlを作成。 -
テスト:
docker-compose up -dを実行し、各コンテナがエラーなく起動することを確認。Grafana (通常http://localhost:3000) にアクセスできることを確認。 -
コミット:
day40: step 2/11 Grafana Stack setup and basic startup test
-
-
Gateway Service 基本実装:
-
gateway_service/main.goを作成。 - HTTPサーバーを起動 (ポート
8080)。 -
/uiエンドポイント: 簡単なHTMLフォーム(例: 注文ボタン)をhtml/templateで提供。 -
/execute-orderエンドポイント: 最初は固定レスポンス(例: "Order processing started")を返す。 -
テスト:
go run ./gateway_serviceで起動。ブラウザで/uiにアクセスし表示を確認。curl localhost:8080/execute-orderでレスポンスを確認。 -
コミット:
day40: step 3/11 Implement basic Gateway Service
-
-
Product Service 基本実装:
-
product_service/main.goを作成。 - HTTPサーバーを起動 (ポート
8081)。 -
/productsエンドポイント: 固定レスポンス(例:{"id": "prod123", "name": "Awesome Product"}) を返す。 -
テスト:
go run ./product_serviceで起動。curl localhost:8081/productsでレスポンスを確認。 -
コミット:
day40: step 4/11 Implement basic Product Service
-
-
Inventory Service 基本実装:
-
inventory_service/main.goを作成。 - HTTPサーバーを起動 (ポート
8082)。 -
/inventoryエンドポイント: 固定レスポンス(例:{"productId": "prod123", "stock": 100}) を返す。 -
テスト:
go run ./inventory_serviceで起動。curl localhost:8082/inventoryでレスポンスを確認。 -
コミット:
day40: step 5/11 Implement basic Inventory Service
-
-
Order Service 基本実装:
-
order_service/main.goを作成。 - HTTPサーバーを起動 (ポート
8083)。 -
/ordersエンドポイント: 固定レスポンス(例:{"orderId": "order789", "status": "created"}) を返す。 -
テスト:
go run ./order_serviceで起動。curl localhost:8083/ordersでレスポンスを確認。 -
コミット:
day40: step 6/11 Implement basic Order Service
-
-
サービス間連携の実装とテスト:
-
gateway_serviceの/execute-orderハンドラを修正。- HTTPクライアントを使用し、
product_service(http://localhost:8081/products)、inventory_service(http://localhost:8082/inventory)、order_service(http://localhost:8083/orders) を順に呼び出す。 - 各サービスからのレスポンスをログに出力(または単純に連結してレスポンスとして返す)。
- HTTPクライアントを使用し、
-
テスト: 全サービスを個別のターミナルで起動 (
go run ./xxx_service)。gateway_serviceの/uiから操作を実行、またはcurl localhost:8080/execute-orderを実行。gateway_serviceのログで各サービス呼び出しが成功していることを確認。 -
コミット:
day40: step 7/11 Implement and test service-to-service communication
-
-
OpenTelemetry 計装 (トレース):
- Otel SDK (
go.opentelemetry.io/otel, OTLP gRPC exporterなど) を各Goプロジェクトのgo.modに追加。 - 共通のトレース初期化関数 (
common/tracing/tracing.goなどを作成し、各サービスから利用できるようにする)。-
TracerProviderの設定、OTLP gRPCエクスポーター (Tempoのtempo:4317を指す) の設定。
-
- 各サービスの
main関数でトレースプロバイダーを初期化。 -
otelhttpを使用して、各サービスのHTTPサーバーと、gateway_service内のHTTPクライアントを計装。 -
テスト (送信): 全サービスとDocker Composeスタックを起動。
gateway_serviceの/uiから操作を実行。エラーが出ないことを確認。 - テスト (受信): Grafana (Tempoデータソース) でトレースが収集され、サービス間の呼び出しが正しく表示されるか確認。
-
コミット:
day40: step 8/11 Implement and test OpenTelemetry Tracing
- Otel SDK (
-
OpenTelemetry 計装 (メトリクス):
- Otel SDK (メトリクス関連、Prometheusエクスポーター) を各Goプロジェクトの
go.modに追加。 - 各サービスの
main関数でPrometheusエクスポーターをセットアップし、/metricsエンドポイントを公開。 -
otelhttpによる基本的なHTTPメトリクスが収集されるようにする。 -
テスト (送信): 全サービスとDocker Composeスタックを起動。数回
/uiから操作。各サービスの/metricsエンドポイントにcurlでアクセスし、メトリクスが出力されることを確認。 -
テスト (受信): Grafana (Prometheusデータソース) でメトリクス (例:
http_server_duration_seconds_count) が収集され、クエリできるか確認。 -
コミット:
day40: step 9/11 Implement and test OpenTelemetry Metrics
- Otel SDK (メトリクス関連、Prometheusエクスポーター) を各Goプロジェクトの
-
OpenTelemetry 計装 (ログ):
- 各Goサービスで
log/slogを使用し、構造化ログ (JSON形式) を標準出力に出力するように変更。- リクエストID、メソッド、パス、ステータスコード、処理時間などを含める。
- Otel トレースIDとスパンIDをログに含める (
slog.Attrやotel.GetTraceMetadata(ctx)を利用)。
-
promtail-config.ymlでDockerコンテナの標準出力を収集し、Lokiに送信する設定を確認・調整。- LokiがログからトレースIDをパースできるように設定 (derived fields)。
-
テスト (送信): 全サービスとDocker Composeスタックを起動。
/uiから操作。各サービスのコンテナログ (例:docker logs <container_name>) でJSON形式のログが出力され、トレースIDが含まれていることを確認。 - テスト (受信): Grafana (Lokiデータソース) でログが収集され、クエリできるか確認。トレースIDでログをフィルタリングできるか、Trace to Logs連携 (Tempoのトレースから関連ログへジャンプ) が機能するか確認。
-
コミット:
day40: step 10/11 Implement and test OpenTelemetry Logging and Trace-to-Logs
- 各Goサービスで
-
ドキュメント作成と最終確認:
-
README.mdの更新:- アーキテクチャ図 (簡易でOK)。
- セットアップ手順 (Goモジュール、Docker Compose)。
- 各サービスの起動方法。
- Otel設定の主要なポイント。
- Grafanaでのトレース、メトリクス、ログの確認方法。
- 学んだこと、詰まった点など。
-
.cursor/rules/knowledge.mdcを更新。 - 不要なファイルやコードを削除。
- テスト: 全体を通して動作確認。
-
コミット:
day40: step 11/11 Final documentation and cleanup
-
こちらの改善された手順でよろしいでしょうか?
o3レビュー後
はい、承知いたしました。さらに詳細なレビューありがとうございます!
いただいたフィードバックを全て反映し、より実践的で学びの多い手順に改善しました。
改訂版・詳細作業順序 (Day40) v3:
-
プロジェクト初期化:
-
templateディレクトリをコピーしてday40_otel_grafana_goを作成。 - Next.js 関連ファイルを削除。
package.jsonのnameを変更。 -
day40_otel_grafana_go/README.mdを作成・初期化。 - Goサービス用ディレクトリ (
gateway_service,product_service,inventory_service,order_service) と、共通処理用ディレクトリ (internal/pkg/otel,internal/pkg/httpclient) をday40_otel_grafana_go/配下に作成。 - 各サービスディレクトリと
internal/pkg内のモジュールでgo mod init。 - ルートに
go.workファイルを作成し、全モジュールを登録。 - テスト: ディレクトリ構造、ファイル確認。
-
コミット:
day40: step 1/12 Project initialization and common module structure
-
-
Grafana Stack (Docker Compose) セットアップと設定:
-
day40_otel_grafana_go/docker-compose.ymlを作成 (Grafana, Loki, Tempo, Prometheus, Promtail)。 -
day40_otel_grafana_go/prometheus/prometheus.ymlを作成。-
scrape_configsセクションに、各Goサービス (gateway_service:8080,product_service:8081等) の/metricsエンドポイントをstatic_configsで明示的に指定。
-
-
day40_otel_grafana_go/promtail/promtail-config.ymlを作成。 -
day40_otel_grafana_go/grafana/provisioning/datasources/datasources.ymlを作成:- Loki, Tempo, Prometheus のデータソースを定義。
-
Loki:
derivedFieldsを設定し、ログメッセージ内のtraceIDからTempoへのリンクを自動生成。 -
Tempo:
tracesToLogsを設定し、Lokiデータソースを指定してトレースからログへのリンクを有効化。
-
docker-compose.ymlでGrafanaのプロビジョニングディレクトリをマウント。 -
テスト:
docker-compose up -dで全コンテナ起動確認。Grafana UIで各データソースが自動設定され、Loki/Tempoの連携設定が反映されているか確認。 -
コミット:
day40: step 2/12 Grafana Stack setup with provisioning and enhanced configs
-
-
共通Otelパッケージ実装 (
internal/pkg/otel/otel.go):-
InitTracerProvider(serviceName, serviceVersion, environment string) (*sdktrace.TracerProvider, error)関数:-
resource.NewWithAttributesでsemconv.ServiceNameKey,semconv.ServiceVersionKey,semconv.DeploymentEnvironmentKeyを設定。 - OTLP gRPCトレースエクスポーター (Tempo宛) を設定。
-
sdktrace.NewTracerProviderを作成して返す。
-
-
InitMeterProvider(serviceName, serviceVersion, environment string) (*sdkmetric.MeterProvider, error)関数:- 同様にリソースを設定。
- Prometheusエクスポーター (
prometheusexporter.New()) を設定。 -
sdkmetric.NewMeterProviderを作成して返す。
-
GetTracer()とGetMeter()(グローバルなTracerProviderとMeterProviderから取得、または初期化時に返す)。 - テスト: 単体テストは難しいが、後のサービス実装時に呼び出してエラーが出ないことを確認。
-
コミット:
day40: step 3/12 Implement common OpenTelemetry initialization package
-
-
共通HTTPクライアントパッケージ実装 (
internal/pkg/httpclient/client.go):- Otel計装済みの
http.Clientを作成するファクトリ関数。-
otelhttp.NewTransport(http.DefaultTransport)を使用。
-
- (オプション)
WithRetryのようなミドルウェア/ラッパーを追加する準備 (今回は詳細実装はスキップ可、ただし考慮した設計に)。 - テスト: 同上。
-
コミット:
day40: step 4/12 Implement common HTTP client package with Otel
- Otel計装済みの
-
Gateway Service 基本実装とOtel計装:
-
gateway_service/main.go:-
otel.InitTracerProvider,otel.InitMeterProviderを呼び出し初期化。 - PrometheusエクスポーターのHTTPハンドラを
/metricsに登録。 - HTTPサーバー (
net/http) をotelhttp.NewHandlerでラップ (ポート8080)。 -
/uiエンドポイント:- シンプルなHTMLフォーム。「通常リクエスト」「エラー発生リクエスト」「長時間リクエスト」など、異なるシナリオを試せるボタンを配置。
-
/execute-orderエンドポイント (引数でシナリオを指定できるようにする):-
context.WithTimeoutを使用してリクエスト全体のタイムアウトを設定。 - 共通HTTPクライアントを使用して他のサービスを呼び出す。
- レスポンスをHTMLまたはJSONで返す。
-
-
- ログは
log/slogを使用し、リクエストスコープでトレースID/スパンIDを含むようにヘルパー関数を検討 (最初は手動でも可)。 -
テスト:
-
go run ./gateway_serviceで起動。 -
/metricsアクセス確認。 -
/uiページ表示と各ボタンの動作確認 (他サービスはまだないのでエラーになるが、ゲートウェイ自体のトレース・メトリクスは確認)。 - Grafana Tempo/Prometheusでゲートウェイのトレース・メトリクス確認。
-
-
コミット:
day40: step 5/12 Implement Gateway Service with Otel and enhanced UI
-
-
Product Service 基本実装とOtel計装:
-
product_service/main.go:-
otel.InitTracerProvider,otel.InitMeterProviderを呼び出し初期化。 - PrometheusエクスポーターのHTTPハンドラを
/metricsに登録。 - HTTPサーバーを
otelhttp.NewHandlerでラップ (ポート8081)。 -
/productsエンドポイント: 固定レスポンス。context.Contextを引数に取り、ctx.Done()をチェックするような処理を意識。
-
- ログは
log/slog。 -
テスト:
-
go run ./product_serviceで起動。 -
/metricsアクセス確認。 - Grafana Tempo/Prometheusでサービスのトレース・メトリクス確認 (単体リクエスト時)。
-
-
コミット:
day40: step 6/12 Implement Product Service with Otel
-
-
Inventory Service 基本実装とOtel計装: (Product Serviceと同様の手順)
- ポート
8082、エンドポイント/inventory。 - テスト: 同様。
-
コミット:
day40: step 7/12 Implement Inventory Service with Otel
- ポート
-
Order Service 基本実装とOtel計装: (Product Serviceと同様の手順)
- ポート
8083、エンドポイント/orders。 - テスト: 同様。
-
コミット:
day40: step 8/12 Implement Order Service with Otel
- ポート
-
サービス間連携の実装とテスト (トレース中心):
-
gateway_serviceの/execute-orderで、各サービス (Product, Inventory, Order) を順次HTTP呼び出し。- 各呼び出しに
ctxを渡す。 - エラーハンドリングを実装 (最低限、ログ出力)。リトライは今回はスキップしても良いが、失敗するケースもUIから作れるようにする。
- 各呼び出しに
-
テスト (複数サービス起動):
- 全サービスを起動 (Makefile や
docker-composeでGoアプリも管理できるようにすると楽になるが、今回は手動でも可。airなどのライブリロードツールを使うと便利)。 -
gateway_serviceの/uiから「通常リクエスト」を実行。 - Grafana Tempo で分散トレースを確認。リクエストが全サービスを通過し、親子関係が正しく表示されるか。各スパンに
service.name等のリソース属性が正しく付与されているか。
- 全サービスを起動 (Makefile や
-
コミット:
day40: step 9/12 Implement and test service-to-service calls with distributed tracing
-
-
メトリクス確認とダッシュボード作成準備:
-
テスト:
-
/uiから複数回リクエストを送信。 - Grafana Prometheus で各サービスのHTTPメトリクス (
http_server_duration_seconds_count,http_server_duration_seconds_bucketなど) が収集されていることを確認。 - PromQLで簡単なクエリ (例: サービス毎のリクエスト数、平均レイテンシ) を試す。
-
- 簡単なGrafanaダッシュボードを作成 (手動):
- リクエスト数、エラーレート、レイテンシ (95パーセンタイルなど) をサービス毎に表示。
-
コミット:
day40: step 10/12 Verify metrics collection and prepare for dashboarding
-
テスト:
-
ログ収集とTrace-to-Logs連携確認:
- 各サービスの
log/slogハンドラで、OtelのトレースIDとスパンIDをログフィールドに含めるように実装 (例:slog.HandlerOptions{ AddSource: true, ReplaceAttr: func(...) }を使ってカスタム属性追加)。-
otel.GetTraceMetadata(ctx)のようなヘルパーを検討するか、otel/baggageやotel/propagationを活用してコンテキスト経由で伝播させる。
-
-
テスト:
-
/uiからリクエストを実行。 - Grafana Loki で各サービスのログが収集され、トレースID/スパンIDが含まれているか確認。
- Grafana Tempoのトレース詳細画面から、関連するログへ正しくジャンプできるか確認 (LokiのderivedFields設定が重要)。
- Lokiから特定のトレースIDでログを検索できるか確認。
-
-
コミット:
day40: step 11/12 Implement structured logging with trace correlation and test Trace-to-Logs
- 各サービスの
-
ドキュメント作成、Makefile/起動スクリプト整備、最終確認:
-
README.md更新 (アーキテクチャ、セットアップ、起動方法、Otel設定、Grafana確認方法など)。 - 全サービスを一度に起動・停止するための
Makefileまたはシェルスクリプト (run_all.sh,stop_all.sh) を作成。- 例:
run_all: gateway product inventory order
gateway: ; (cd gateway_service && go run .)(バックグラウンド実行など工夫)
- 例:
-
.cursor/rules/knowledge.mdc更新。 - 不要ファイル削除。
- テスト: 作成したスクリプトで全サービスを起動・停止できるか。全体を通しての動作確認。
-
コミット:
day40: step 12/12 Final documentation, startup scripts, and cleanup
-
こちらの詳細な手順で進めてよろしいでしょうか? OKであれば、ステップ1から開始します。
grafana 起動した

お、loki にデータ入ってる!

Day40 - GoマイクロサービスとGrafana StackによるOpenTelemetry - 引き継ぎプロンプト
背景
Go言語でマイクロサービス群(Gateway, Product, Inventory, Order)を構築し、OpenTelemetry (Otel) を用いて計装(instrumentation)を行っています。目的は、トレース、メトリクス、ログをローカルのGrafana Stack(Docker Composeで起動するTempo, Loki, Prometheus, Grafana)に送信し、可観測性の概念とツールについて学習することです。
ここまでの進捗
-
プロジェクト設定:
day40_otel_grafana_goディレクトリを作成し、Next.js関連ファイルを削除。Goモジュール(gateway_service,product_service,inventory_service,order_service,internal/pkg/otel,internal/pkg/httpclient)をセットアップし、go.workで管理するようにしました。 -
Grafana Stack:
docker-compose.ymlを用いてGrafana, Loki, Tempo, Prometheus, Promtailを設定し、起動しました。PrometheusはGoサービスのメトリクスを(host.docker.internal経由で)スクレイプし、Promtailはログを収集します。Grafanaのデータソース(Prometheus, Loki, Tempo)はプロビジョニングされ、基本的なLokiからTempoへのトレース連携(derivedFields)も設定済みです。途中で発生した設定の問題も修正しました。 -
共通パッケージ:
-
internal/pkg/otel: TracerProvider(OTLP gRPCでlocalhost:4317へ送信)とMeterProvider(Prometheusエクスポーター)を初期化する共通関数を実装しました。グローバルなプロバイダーとプロパゲーター(TraceContext+Baggage)を設定します。 -
internal/pkg/httpclient: Otel計装済みのHTTPクライアント(otelhttp.NewTransportを使用)を作成するファクトリ関数を実装しました。
-
-
Goマイクロサービス:
- 4つのサービスすべてについて、基本的なHTTPサーバー(
gatewayは8080,productは8081,inventoryは8082,orderは8083)を実装しました。 - 各サービスは共通パッケージを使用してOtelを初期化します。
- 各サービスは
/metricsエンドポイントをpromhttp.Handler()経由で公開します。 - 各サービスは
otelhttp.NewHandlerを使用してサーバーサイドのリクエストハンドリングをラップしています。 - Gateway (
/) は、異なるシナリオを実行するボタンを持つシンプルなHTML UIを提供します。 - Gateway (
/execute-order) は、Otel計装済みHTTPクライアントを使用して、Product, Inventory, Orderサービスを順次呼び出します。エラー/遅延シナリオも部分的に実装されています。 - 全サービスのビルドが完了し、個別に実行可能です。
- 4つのサービスすべてについて、基本的なHTTPサーバー(
現在の状況と問題点
Goサービス4つすべてが(ホストマシン上でバックグラウンドで)実行中です。Grafana StackはDocker内で実行中です。
Gateway UI (http://localhost:8080/) から「Execute Normal Order」フローを実行すると、リクエストは成功し、UIには後続サービスからの応答が表示されます。
しかし、Grafana Tempoで確認すると、gateway-service, product-service, inventory-service, order-serviceのトレースが、単一のTrace IDの下で繋がらず、それぞれ独立した別のトレースとして表示されてしまいます。 これは、トレースコンテキストがサービス間で正しく伝播されていないことを示しています。
これを解決するために、以下の修正を試みました:
- Goサービス内のOtelエンドポイント指定を、ホストからDockerへの通信に適した
localhost:4317に変更しました。 -
gateway_service/callService内の手動クライアントスパン作成を削除し、otelhttp.Transportによる自動的なコンテキスト伝播に完全に依存するようにしました。
これらの変更にもかかわらず、トレースは依然として分断されたままです。
次のステップ(最優先)
ステップ9の再訪: トレースコンテキスト伝播の修正
-
調査: なぜ
otelhttpがGatewayクライアントと後続サービスサーバー間で自動的にtraceparentヘッダー(または同等のもの)を伝播していないのか原因を特定します。-
仮説1:
internal/pkg/otel/otel.go内のotel.SetTextMapPropagator(...)設定は正しく機能しているか? -
仮説2:
gateway_service内でコンテキスト(context.Context)はリクエストチェーン(handleExecuteOrder->callService->http.NewRequestWithContext)を通じて正しく渡されており、otelhttp.Transportが入力スパンコンテキストにアクセスできているか? -
仮説3: サーバー側(後続サービス)の
otelhttp.NewHandlerがコンテキストを抽出する際に問題があるか?
-
仮説1:
-
デバッグ:
gateway_service/callServiceのhttpClient.Do前 にログを追加し、送信リクエストヘッダー(traceparentがあるか)を出力します。後続サービスのハンドラ(例:product_service/handleGetProduct)内にもログを追加し、受信リクエストヘッダーを出力します。これでヘッダーが送受信されているか確認できます。 - 修正: 調査結果に基づきコードを修正します。コンテキストの適切な使用、プロパゲーター設定の確認、あるいは標準的なメカニズムが壊れているように見える場合はライブラリバージョンの互換性などを確認する必要があるかもしれません。
- テスト: 全サービスを再起動し、Gateway UIからリクエストを送信します。Tempoでトレースが単一の繋がったグラフとして(4つのサービスすべてを含む形で)表示されることを確認します。
残りのステップ(トレース修正後)
- ステップ10: Prometheus/Grafanaでのメトリクス収集を確認し、基本的なダッシュボードを作成します。
-
ステップ11: トレース相関(
trace_id,span_idを含む)を持つ構造化ログ(slog)を実装し、GrafanaでのTrace-to-Logs連携をテストします。 -
ステップ12: ドキュメント(
README.md,.cursor/rules/knowledge.mdc)を完成させ、起動スクリプト(Makefile)を作成し、クリーンアップを行います。
この情報をもとに、まずはトレースコンテキスト伝播の問題解決に焦点を当てて作業を続けてください。
trace みれた!!
検索するときに trace id をそのまま入れないとみれないというトラップで無限に時間を溶かした

素晴らしいです!🎉
スクリーンショットで、TraceQL タブにTrace ID (3f55743d75d3cfd090ba2b1b54002a4c) を直接入力した結果、期待通りに gateway-service, product-service, inventory-service, order-service のすべてのスパンが正しく親子関係を保ち、単一のトレースとして表示されているのが確認できました!
問題の原因は、Grafana Explore画面でのTrace IDの検索方法 (UIの操作方法) にありました。
- Tempoは正しくトレースデータを収集・保存していました。
- Goの各サービスにおけるOpenTelemetryの計装やコンテキスト伝播も正しく機能していました。
-
TraceQLタブでTrace IDを直接指定することで、意図したトレースを正確に表示させることができました。
これで、当初の目標であった「トレースコンテキスト伝播の修正」は完了し、トレースがGrafana Tempoで正しく繋がって表示されることが確認できました。
残りのステップ:
これで安心して、プロジェクトの残りのステップに進むことができます。
-
ステップ10: Prometheus/Grafanaでのメトリクス収集を確認し、基本的なダッシュボードを作成します。
- 各Goサービスは既に
/metricsエンドポイントを公開しており、Prometheusはこれらをスクレイプする設定になっているはずです。GrafanaでPrometheusデータソースを使い、基本的なメトリクス(例: HTTPリクエスト数、レイテンシなど)を表示するダッシュボードを作成します。
- 各Goサービスは既に
-
ステップ11: トレース相関(
trace_id,span_idを含む)を持つ構造化ログ(slog)を実装し、GrafanaでのTrace-to-Logs連携をテストします。- 現在の
log.PrintfをGoの標準ライブラリであるlog/slogを使用した構造化ロギングに置き換えます。その際、ログエントリに現在のスパンのtrace_idとspan_idを含めるようにします。 - PromtailがこれらのログをLokiに送信し、GrafanaのTempoデータソース設定にある「Trace to logs」機能(1つ前のやり取りでスクリーンショットをいただいた箇所)を適切に設定することで、トレーススパンから関連するログエントリへジャンプできるようにします。
- 現在の
-
ステップ12: ドキュメント(
README.md,.cursor/rules/knowledge.mdc)を完成させ、起動スクリプト(Makefileは既にありますね)を調整し、クリーンアップを行います。
素晴らしい進捗です!次のステップ(メトリクス収集の確認とダッシュボード作成)に進みますか?
もし進める場合は、まずGrafanaでPrometheusデータソースを使って、各サービスからどのようなメトリクスが収集できているかを確認するところから始めましょう。例えば、gateway_service_http_requests_total (これは otelhttp が自動的に提供するメトリクス名に近いはずです) のようなメトリクスをGrafanaのExploreでクエリしてみるのが良いでしょう。
promethus で metrics もとれてた

できた!

学び
- OTEL のなんとなくの使い方
- Grafana Stack(Prometheus / Loki / Tempo / Grafana ) の役割
- Grafana Stack のなんとなくの設定 (複雑だなあ)