Zenn
📝

OrchestrionとError Trackingの検証

2025/02/26に公開
1

こんにちは、sugar-catです。

この記事では、Datadogが提供するOrchestrionを使用したGoアプリケーションの自動計装について、特にエラートラッキングの観点から検証した結果を共有します。

本記事では以下のバージョンを使用しています。
Orchrstrion v1.04
Go 1.23.0

Orchestrionとは

Orchestrionは、2024年11月末にGAしたDatadogが提供するコンパイル時計装ツールです。コードを静的解析し、トレース用のコードを自動的に挿入することで、アプリケーションの可観測性を向上させます。

https://www.datadoghq.com/ja/blog/go-instrumentation-orchestrion/

内部の仕組み

Orchestrionはコンパイル時に計装用のコードを自動的に挿入します。
内部の実装についてはGopherCon 2023でも発表されているため、詳細はそちらを参照してください。
https://www.youtube.com/watch?v=5l-W7vPSbuc
https://docs.google.com/presentation/d/16cpTIVfzSuFoA_jtsLcJ_m36_sQ0o6yBwhfBMFWZ47s
https://speakerdeck.com/iamshunta/recap-automatically-instrument-your-go-source-code-with-orchestrion?slide=13

Orchestrionはdd-traceのライブラリをテンプレートで埋め込む形で動作します。
例えば、chi.v5のorchestrion.ymlを見ると、Datadogの計装用ミドルウェアを自動的に追加するようなTemplateを定義しています。

https://github.com/DataDog/dd-trace-go/blob/386c125b024bfa7be093f4f32d6b3c84e46960ce/contrib/go-chi/chi.v5/orchestrion.yml#L7-L33

追加後のコード例(orchestrion go build -work)

//line /Users/xxx/main.go:1:1
package main

import (
	"net/http"

	"github.com/go-chi/chi/v5"
	__orchestrion_chitrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-chi/chi.v5"
	//line <generated>:1
)

//line /Users/xxx/main.go:9
func main() {
	r :=
//line <generated>:1
		func() *chi.Mux {
			mux :=
//line /Users/xxx/main.go:10
				chi.NewRouter()
//line <generated>:1
			mux.Use(__orchestrion_chitrace.Middleware())
			return mux
		}()

//line /Users/xxx/main.go:12
	r.Get("/hello", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, Chi!"))
	})

	http.ListenAndServe(":8080", r)
}

Orchestrionの設定と使い方

インストール等は公式に従います。
https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/go/?tab=compiletimeinstrumentation#usage

前提知識

  • DD Tagの設定: Agent等に設定したタグはUST(Universal Service Tags)として自動的に使用される
  • Custom Span/Custom Tag: //dd:spanのアノテーションで制御可能
  • エラー処理: アノテーションされた関数には自動的にエラーが記録される(返り値にerrorが含まれている場合)
  • 計装の無効化: //orchestrion:ignore ディレクティブで特定のコードの計装を無効可能
  • 互換性: Orchestrionがサポートしていないライブラリの場合、従来のトレーシングライブラリと併用可能(ただし、後に対応された場合は重複に注意)

フィルタリング(ヘルスチェック等の無視)

コンパイル時計装の場合、ルーターライブラリにignore対象を組み込むのは難しいため、datadog.yamlまたは環境変数で設定します。

DD_APM_IGNORE_RESOURCES="(GET|POST) /ping"

※ignoreするとingest対象から外れるため、トレースメトリクスにも記録されなくなります。
https://docs.datadoghq.com/ja/tracing/guide/ignoring_apm_resources/?tab=datadogyaml#リソースに基づいて無視する

エラートラッキングの基本

エラートラッキングが機能するためには、以下の条件を満たす必要があります。

  1. エラーは各サービスレベルで処理される(エントリースパンである必要はない)
  2. スパンに以下の属性が含まれていること:
    • error.type: エラーの種類
    • error.message: エラーメッセージ
    • error.stack: スタックトレース情報

これらの属性を持つエラースパンは、Datadogによって自動的に収集され、同じフィンガープリントを持つエラーはグループ化されて同じ問題として扱われます。

image

標準的なエラー挙動

エラートラッキングを効果的に行うためには、エラーが発生した場所から適切にルートスパンまで伝播させることが重要です。これにより、エラーの発生源と影響範囲を正確に把握できます。

  • ルートスパンでエラーが発生した場合:

    • エラーメッセージがルートスパンに記録され、エラートラッキングダッシュボードに表示される
  • 子スパン(ネストされた関数)でエラーが発生した場合:

    • //dd:spanがない場合:エラーは記録されますが、親スパンには伝播されない
    • //dd:spanがある場合:子スパンにエラーメッセージが記録され、適切に設定されていれば親スパンに伝播する

既知の問題としてdd-traceのライブラリを使う計装では自前でエラーを記録する必要があります。
ここではchiの例を挙げます。

  • 標準的なエラーの場合
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
    // something
    if err != nil {
        span, _ := tracer.SpanFromContext(r.Context())
        span.SetTag("error.msg", err.Error()) // エラーメッセージを記録
        http.Error(w, "Failed to create request", http.StatusInternalServerError)
        return
    }
    w.Write([]byte("pong"))
})
  • Panic

panicの場合はrecoverしてエラーを記録します。chiなどの場合は純粋に公式が提供しているRecovererのミドルウェアに組み込む形で利用するのが手軽です。
https://github.com/go-chi/chi/blob/master/middleware/recoverer.go

func Recoverer(next http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rvr := recover(); rvr != nil {
				if rvr == http.ErrAbortHandler {
					// we don't recover http.ErrAbortHandler so the response
					// to the client is aborted, this should not be logged
					panic(rvr)
				}
				span, _ := tracer.SpanFromContext(r.Context())
				span.SetTag("error.msg", rvr)
				if r.Header.Get("Connection") != "Upgrade" {
					w.WriteHeader(http.StatusInternalServerError)
				}
			}
		}()

		next.ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

Orchestrionでエラートラッキングを使った際の問題

Orchestrionを使用する際の最も重要な課題として、Root Spanの生成に関する問題があります。

Webアプリケーションをトレースする場合、理想的には「ルーターがリクエストを処理したこと」がトレースのルートスパンとなるべきです。しかしOrchestrionではコールグラフを辿り計装されるため一般的なルーターライブラリ(net/httpのラッパー)ではnet/httpパッケージも自動的に計装されるため、Root Spanがnet/httpのスパンとなります。

net/http.Server -> chi.Router -> handler(own span)

この結果実際のトレース構造では、ルーターのスパンではなくnet/httpのスパンがルートスパンとなります。

image

エラートラッキングへの影響

一般的にAPMを使用する際はサービス名を指定しますが、DD_SERVICEを指定するとnet/http、router componentのどちらのスパンもDD_SERVICEで指定したサービス名となります。
そのためErrorTrackingには連携されますが、エラーの内容は伝搬されていないスパンが誕生してしまいます。

image

詳しくは下記Issueを参照してください。

https://github.com/DataDog/orchestrion/issues/528

対応策

対処療法的な対応ですが、DD_SERVICEを指定せず、Service Overrideを利用することでnet/httpのスパンを無視することができます。

DD_SERVICEを指定しない場合、実行時のバイナリ名がそのままサービス名として使用されます。これを利用し、各サービス名をバイナリ名で表現(BaseService)し、Service OverrideされたRouterのスパン名でErrorTrackingを行うことで、エラーの内容を捕捉することができます。

image

またOrchestrionの開発チームは、エラートラッキングに改善案を提案してくれており、これらの機能が実装されるのを待つのも一つの手です。
https://github.com/DataDog/orchestrion/issues/528#issuecomment-2647496670

I have an idea for a strategy that could work here... Since we're basically getting two layers of instrumentation (one via net/http.Server and the other via chi.Router); we could have the tracer implement something along the lines of:
・Add an API to the net/http contrib that allows registration of "tracing" handler types
・Update WrapHanlder to be a no-op on types registered using the above API
・Have HTTP router contribs register their handler implementation using the above API (in the func init of the contrib, call httptrace.RegisterRoutingHandlerType*chi.Router)

まとめ

Orchestrionは、Goアプリケーションの自動計装を簡単に実現できる強力なツールですが、エラートラッキングを効果的に行うには、いくつかの課題と対策を理解する必要があります。

1

Discussion

ログインするとコメントできます