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 のなんとなくの設定 (複雑だなあ)