OrchestrionとError Trackingの検証
こんにちは、sugar-catです。
この記事では、Datadogが提供するOrchestrionを使用したGoアプリケーションの自動計装について、特にエラートラッキングの観点から検証した結果を共有します。
本記事では以下のバージョンを使用しています。
Orchrstrion v1.04
Go 1.23.0
Orchestrionとは
Orchestrionは、2024年11月末にGAしたDatadogが提供するコンパイル時計装ツールです。コードを静的解析し、トレース用のコードを自動的に挿入することで、アプリケーションの可観測性を向上させます。
内部の仕組み
Orchestrionはコンパイル時に計装用のコードを自動的に挿入します。
内部の実装についてはGopherCon 2023でも発表されているため、詳細はそちらを参照してください。
Orchestrionはdd-traceのライブラリをテンプレートで埋め込む形で動作します。
例えば、chi.v5のorchestrion.yml
を見ると、Datadogの計装用ミドルウェアを自動的に追加するようなTemplateを定義しています。
追加後のコード例(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の設定と使い方
インストール等は公式に従います。
前提知識
- 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対象から外れるため、トレースメトリクスにも記録されなくなります。
エラートラッキングの基本
エラートラッキングが機能するためには、以下の条件を満たす必要があります。
- エラーは各サービスレベルで処理される(エントリースパンである必要はない)
- スパンに以下の属性が含まれていること:
-
error.type
: エラーの種類 -
error.message
: エラーメッセージ -
error.stack
: スタックトレース情報
-
これらの属性を持つエラースパンは、Datadogによって自動的に収集され、同じフィンガープリントを持つエラーはグループ化されて同じ問題として扱われます。
標準的なエラー挙動
エラートラッキングを効果的に行うためには、エラーが発生した場所から適切にルートスパンまで伝播させることが重要です。これにより、エラーの発生源と影響範囲を正確に把握できます。
-
ルートスパンでエラーが発生した場合:
- エラーメッセージがルートスパンに記録され、エラートラッキングダッシュボードに表示される
-
子スパン(ネストされた関数)でエラーが発生した場合:
-
//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のミドルウェアに組み込む形で利用するのが手軽です。
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
のスパンがルートスパンとなります。
エラートラッキングへの影響
一般的にAPMを使用する際はサービス名を指定しますが、DD_SERVICE
を指定するとnet/http
、router componentのどちらのスパンもDD_SERVICE
で指定したサービス名となります。
そのためErrorTrackingには連携されますが、エラーの内容は伝搬されていないスパンが誕生してしまいます。
詳しくは下記Issueを参照してください。
対応策
対処療法的な対応ですが、DD_SERVICEを指定せず、Service Overrideを利用することでnet/http
のスパンを無視することができます。
DD_SERVICE
を指定しない場合、実行時のバイナリ名がそのままサービス名として使用されます。これを利用し、各サービス名をバイナリ名で表現(BaseService)し、Service OverrideされたRouterのスパン名でErrorTrackingを行うことで、エラーの内容を捕捉することができます。
またOrchestrionの開発チームは、エラートラッキングに改善案を提案してくれており、これらの機能が実装されるのを待つのも一つの手です。
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アプリケーションの自動計装を簡単に実現できる強力なツールですが、エラートラッキングを効果的に行うには、いくつかの課題と対策を理解する必要があります。
Discussion