メインコンテンツへスキップ
友田 陽大
Go・Echo 本番運用
Go
Echo
可観測性
アーキテクチャ設計
型安全
SRE

Echo の可観測性:OpenTelemetry で分散トレース・メトリクス・slog 相関を自前ミドルウェアで実装する

Go Echo(v5)の可観測性を OpenTelemetry で本番品質に実装するガイド。otelecho が非推奨・v4 想定の現状を踏まえ、バージョン非依存の自前トレースミドルウェア、context によるトレース伝播(DB・外部HTTP)、リクエスト処理時間ヒストグラム等のメトリクス、slog ログへの trace_id 相関、OTLP エクスポートまでを実コードで解説します。

公開日
読了時間
9分
著者
友田 陽大
シェア

「本番で API が遅い」と言われたとき、どの処理が・どれだけ・なぜ遅いかを即座に答えられますか。ログを grep して当たりを付ける運用は、障害のたびに時間を溶かします。可観測性(Observability)とは、止まった処理・遅い処理を、推測ではなくデータで一目で追える状態を作ることです。

この記事は、Go Echo 本番運用ガイドの可観測性編です。OpenTelemetry(OTel)でトレース・メトリクスを取り、slog 構造化ログと相関させます。プラットフォーム全体の可観測性設計はOpenTelemetry 実践ガイドに譲り、ここでは Echo v5 で今すぐ動く実装に集中します。

この記事のルール:Echo の API は 公式ドキュメント(v5・2026年6月時点) に基づきます。重要:定番だった otelechogo.opentelemetry.io/contrib/.../labstack/echo/otelecho)は非推奨であり、かつ Echo v4 想定です。本記事は、それに依存しない OTel SDK を直接使う自前ミドルウェアを採用します(理由は第1章)。OTel SDK は更新されるため、API は公式で最新を確認してください。


0. 三本柱:トレース・メトリクス・ログを「相関」させる

可観測性は3つのシグナルで成り立ちます。バラバラに集めるのではなく、相関させることに価値があります。

  • トレース(Traces):1リクエストが「どの処理を・どの順で・どれだけ」かけたかの分解。遅延の犯人特定に効く。
  • メトリクス(Metrics):集計値(リクエスト数・エラー率・処理時間分布)。傾向とアラートに効く。
  • ログ(Logs):個別イベントの詳細。原因の文脈に効く。

これらを trace_id で繋ぐと、「メトリクスでエラー率の上昇に気づく → 該当時間帯のトレースで遅い span を特定 → その span の trace_id でログに飛んで原因を読む」という一本の調査線ができます。これが本記事のゴールです。


1. なぜ otelecho を使わず自前ミドルウェアにするのか

定番は otelecho ミドルウェアですが、2026年6月時点で2つの問題があります。

  1. 非推奨(deprecated):パッケージ自体が非推奨化されている。
  2. Echo v4 想定:v5 でハンドラ署名が func(c *echo.Context) error に変わったため、v4 前提の計装はそのままでは噛み合わない。

OpenTelemetry のコア SDK(go.opentelemetry.io/otel)はフレームワーク非依存です。だから、Echo のミドルウェアとして OTel SDK を直接呼ぶ薄い計装を自前で持つ方が、バージョンに左右されず・非推奨に巻き込まれず・中身を理解できる——ETC(変更容易性)の観点で堅い選択です。コードは数十行で済みます。


2. トレース計装ミドルウェア:伝播が命

分散トレースの肝は context 伝播です。受信リクエストのヘッダから親トレース文脈を Extract し、span を開始し、その ctx後続(DB・外部 API)に必ず引き継ぐ——ここが切れると、トレースがブツ切りになります。

import (
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/propagation"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
	"go.opentelemetry.io/otel/trace"
)

func OTelTracing(service string) echo.MiddlewareFunc {
	tracer := otel.Tracer(service)
	propagator := otel.GetTextMapPropagator()

	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c *echo.Context) error {
			req := c.Request()
			// ① 受信ヘッダから親トレース文脈を取り出す(W3C traceparent 等)
			ctx := propagator.Extract(req.Context(), propagation.HeaderCarrier(req.Header))

			// ② span を開始。ルートパターンを名前にする(カーディナリティを抑える)
			route := c.Path() // "/users/:id"(実値ではなくパターン=低カーディナリティ)
			ctx, span := tracer.Start(ctx, req.Method+" "+route,
				trace.WithSpanKind(trace.SpanKindServer),
				trace.WithAttributes(
					semconv.HTTPRequestMethodKey.String(req.Method),
					semconv.HTTPRouteKey.String(route),
				),
			)
			defer span.End()

			// ③ 後続(ハンドラ→DB→外部API)へ ctx を貫通させる(最重要)
			c.SetRequest(req.WithContext(ctx))

			err := next(c)

			// ④ 結果を span に記録
			status := c.Response().Status
			span.SetAttributes(semconv.HTTPResponseStatusCodeKey.Int(status))
			if err != nil || status >= 500 {
				span.SetStatus(codes.Error, http.StatusText(status))
				if err != nil {
					span.RecordError(err)
				}
			}
			return err
		}
	}
}

設計上の要点

  • span 名は c.Path()(ルートパターン)/users/42 のような実値を名前にすると、ID ごとに別 span 扱いになりカーディナリティが爆発します。/users/:id に正規化します。
  • c.SetRequest(req.WithContext(ctx)) が伝播の心臓部。これを忘れると、ハンドラ内の c.Request().Context() に span が乗らず、DB クエリや外部 API の子 span が親に繋がりません
  • Recover の内側に置き、panic も集中エラーハンドラ経由で span に記録されるようにします。

3. 子 span:DB と外部 HTTP まで一本に繋ぐ

ミドルウェアで作った ctx を渡せば、下位処理が子 spanとして親にぶら下がります。これで「API は速いが DB が遅い」が可視化されます。

// DB:pgx なら otelpgx で自動計装、または手動で子 span
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
	ctx, span := otel.Tracer("repo").Start(ctx, "UserRepo.FindByID") // 親 ctx から子 span
	defer span.End()
	// ... r.pool.Query(ctx, ...) ← ctx 経由で DB span が親に繋がる
}

// 外部 HTTP:otelhttp はフレームワーク非依存なのでそのまま使える
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
// client.Do(req.WithContext(ctx)) ← 送信先へ traceparent を自動伝播

otelhttp(outbound HTTP 計装)は net/http のトランスポートをラップするだけで、Echo のバージョンに依存しません。pgx には otelpgx のような計装ライブラリもあります。「フレームワーク非依存の計装は使い、フレームワーク密結合の計装(otelecho)は自前に置き換える」のが、移行期の堅い方針です。


4. メトリクス:RED を最小構成で

メトリクスは RED(Rate・Errors・Duration)を基本に持ちます。最小構成はリクエスト処理時間の Histogram進行中リクエストの UpDownCounterです。これを同じミドルウェアに足します。

import "go.opentelemetry.io/otel/metric"

func OTelMetrics(service string) echo.MiddlewareFunc {
	meter := otel.Meter(service)
	duration, _ := meter.Float64Histogram("http.server.request.duration",
		metric.WithUnit("s"), metric.WithDescription("HTTP request duration"))
	inflight, _ := meter.Int64UpDownCounter("http.server.active_requests")

	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c *echo.Context) error {
			ctx := c.Request().Context()
			start := time.Now()
			inflight.Add(ctx, 1)
			defer inflight.Add(ctx, -1)

			err := next(c)

			// 属性はルートパターン+ステータスクラスに絞る(カーディナリティ管理)
			attrs := metric.WithAttributes(
				attribute.String("http.route", c.Path()),
				attribute.String("http.method", c.Request().Method),
				attribute.Int("http.status_code", c.Response().Status),
			)
			duration.Record(ctx, time.Since(start).Seconds(), attrs)
			return err
		}
	}
}

カーディナリティの罠:メトリクスの属性にユーザー ID や生 URL を入れると、時系列の組み合わせが爆発し、コストとストレージを破壊します(コスト効率に直結)。属性はルートパターン・メソッド・ステータスのような低カーディナリティに厳しく絞ります。SLO/エラーバジェットの設計は可観測性・SRE の実践へ。


5. ログ相関:slog に trace_id を載せる

最後のピースがログとトレースの相関です。v5 標準の slogtrace_id/span_id を載せれば、ログ1行からトレースへジャンプできます。ctx から span 文脈を取り出すヘルパを噛ませます。

// ctx の span 文脈を slog 属性に変換する
func traceAttrs(ctx context.Context) []slog.Attr {
	sc := trace.SpanContextFromContext(ctx)
	if !sc.IsValid() {
		return nil
	}
	return []slog.Attr{
		slog.String("trace_id", sc.TraceID().String()),
		slog.String("span_id", sc.SpanID().String()),
	}
}

// RequestLogger の LogValuesFunc で相関ログを出す
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
	LogStatus: true, LogURI: true, LogError: true, LogLatency: true, HandleError: true,
	LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
		ctx := c.Request().Context()
		attrs := append(traceAttrs(ctx),
			slog.String("uri", v.URI),
			slog.Int("status", v.Status),
			slog.Duration("latency", v.Latency),
		)
		level := slog.LevelInfo
		if v.Error != nil {
			level = slog.LevelError
			attrs = append(attrs, slog.String("err", v.Error.Error()))
		}
		logger.LogAttrs(ctx, level, "REQUEST", attrs...)
		return nil
	},
}))

これで、メトリクスでエラー率の急増に気づき → 該当時間帯のトレースで遅い span を見つけ → その trace_id でログを引く、という三本柱を貫通した調査が成立します。RequestLoggerミドルウェア並び順で OTel ミドルウェアの内側に置き、span 文脈が乗った状態でログを出します。


6. エクスポート:OTLP で収集基盤へ送る

計装したシグナルは、**OTLP(OpenTelemetry Protocol)**で収集基盤(OpenTelemetry Collector → Grafana Tempo / Jaeger / Datadog / Cloud 各社)へ送ります。アプリ起動時に TracerProvider/MeterProvider を構成し、グレースフルシャットダウンflush します。

import (
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func initTracing(ctx context.Context, service string) (func(context.Context) error, error) {
	exp, err := otlptracegrpc.New(ctx) // 送信先は OTEL_EXPORTER_OTLP_ENDPOINT 環境変数
	if err != nil {
		return nil, err
	}
	res, _ := resource.New(ctx, resource.WithAttributes(semconv.ServiceName(service)))
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exp),                       // バッチ送信(性能・コスト)
		sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))), // 10%サンプリング
		sdktrace.WithResource(res),
	)
	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.TraceContext{}) // W3C 伝播
	return tp.Shutdown, nil // ← main の defer で呼び、未送信 span を flush
}
// main 側:起動時に初期化、終了時に flush
shutdown, err := initTracing(ctx, "user-api")
if err != nil { /* ... */ }
defer shutdown(context.Background()) // グレースフル停止時に未送信分を送る

コスト最適化:本番ではサンプリング(例:TraceIDRatioBased(0.1) で 10%)でトレース量とコストを抑えます。エラーや遅い trace を優先して残す「テールサンプリング」は Collector 側で行います。送信先は OTEL_EXPORTER_OTLP_ENDPOINT環境変数化し、エンドポイントをコードに焼きません。


まとめ:Echo の可観測性を本番品質にする7原則

  1. 三本柱を trace_id で相関させ、1リクエストを貫通して辿れるようにする。
  2. otelecho は非推奨・v4 想定。OTel SDK を直接使う自前ミドルウェアがバージョン非依存で堅い(ETC)。
  3. トレースは伝播が命Extract → span → c.SetRequest(req.WithContext(ctx)) で後続まで貫通。
  4. span 名・属性はルートパターンに正規化し、カーディナリティ爆発を防ぐ。
  5. メトリクスは RED(処理時間 Histogram + 進行中 UpDownCounter)を最小構成で。
  6. slog に trace_id を載せ、ログ ↔ トレースを相互ジャンプ可能にする。
  7. OTLP でエクスポートし、サンプリングでコスト最適化、停止時に flush する。

可観測性は「ログを出すこと」ではなく「障害時に推測をゼロにすること」です。Echo v5 では、非推奨ツールに頼らず OTel SDK を薄く自前計装する方が、結果的に堅く・安く・理解可能になります。プラットフォーム横断の可観測性はOpenTelemetry 実践ガイド、Echo の全体像は本番運用ガイドへ。

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事の実装を、案件として承ります

Go / Echo のバックエンドを、設計から本番運用まで承ります

Echo v5 へのAPI設計・移行、クリーンアーキテクチャ(Controller/UseCase/Repository + DI)、ミドルウェアとセキュリティ、集中エラー処理、グレースフルシャットダウン、テストとCIまで。Go/Echo + google/wire で実際にクリーンアーキのバックエンドを構築した知見で、落ちない・追える・変更しやすいAPIを実装します。

プロジェクト単位(請負)・技術顧問のどちらにも対応可能です。まずは30分の無料技術相談から。

あわせて読みたい