アプリケーションから直接Mackerelにトレースを送信するパターンと、OpenTelemetry Collectorを使ってトレースを送信するパターンを試す。
環境の準備
Dockerで簡単なGoのHTTPサーバーアプリケーションを動かす。
アプリケーションはGeminiにお願いしたところ、ルートパス(/)にアクセスすると "Hello, World!" と応答し、/greet/{name}の形式でアクセスすると、"{name} さん、こんにちは!" と応答するアプリケーションを作ってくれた。感謝。
ディレクトリ構造
app └Dockerfile └main.go .env docker-compose.yaml
Dockerfile
FROM golang:latest WORKDIR /app COPY main.go . RUN go build -o hello-server main.go ENTRYPOINT [ "/app/hello-server" ]
main.go
package main import ( "fmt" "log" "net/http" "regexp" ) func rootHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("Hello, World!\n")) } func greetHandler(w http.ResponseWriter, r *http.Request) { re := regexp.MustCompile(`/greet/([a-zA-Z0-9]+)`) match := re.FindStringSubmatch(r.URL.Path) if len(match) > 1 { name := match[1] response := fmt.Sprintf("%s さん、こんにちは!\n", name) w.WriteHeader(http.StatusOK) w.Write([]byte(response)) } else { http.NotFound(w, r) } } func main() { // ルートパスへのハンドラを登録 http.HandleFunc("/", rootHandler) // /greet/ 以降のパスへのハンドラを登録 http.HandleFunc("/greet/", greetHandler) // サーバーを起動してリクエストをリッスン port := ":8080" log.Printf("Server listening on port %s", port) err := http.ListenAndServe(port, nil) // デフォルトの ServeMux を使用 if err != nil { log.Fatalf("Server failed to start: %v", err) } }
.env
MACKEREL_API_KEY="<APIキー>"
docker-compose.yaml
services: hello-server: build: context: ./app dockerfile: Dockerfile ports: - 8000:8000 env_file: ./.env
計装
Mackerelのヘルプを参考に計装していく。
1. go get
これを行う前にgo mod init hello-serverを実行しておく。
私はGoに不慣れなので、いきなりヘルプ通りgo getを実行したらgo: go.mod file not found in current directory or any parent directory.というエラーが出てしまった。
2. 初期設定
ヘルプに書かれている内容をmain.goに追記する。semconv.ServiceNameなどの値は任意で変更可能。
なお、ヘルプには書かれていないが、実際は以下のパッケージもimportする必要がある(initTracerProvider関数で使用しているため)。また、semconvはこの検証を行った時点でv1.24.0が出ていた。
- go.opentelemetry.io/otel
- go.opentelemetry.io/otel/propagation
- go.opentelemetry.io/otel/sdk/resource
いくつかのパッケージでno required module provides packageが出ていたので、go mod tidyを実行した。
3. ミドルウェアの挿入
このパートは最初ヘルプを見ても何をやっているのかよくわからず、なかなか難易度が高かった。自力ではうまくいかなかったので、最終的にはGeminiに計装してもらった。
main()の差分は以下のとおり。
func main() { + ctx := context.Background() + shutdown, err := initTracerProvider(ctx) + if err != nil { + log.Fatalf("Failed to initialize tracer provider: %v", err) + return + } + defer shutdown(ctx) + // ルートパスへのハンドラを登録 - http.HandleFunc("/", rootHandler) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + otelhttp.NewHandler(http.HandlerFunc(rootHandler), "root").ServeHTTP(w, r) + }) // /greet/ 以降のパスへのハンドラを登録 - http.HandleFunc("/greet/", greetHandler) - - // サーバーを起動してリクエストをリッスン + http.HandleFunc("/greet/", func(w http.ResponseWriter, r *http.Request) { + otelhttp.NewHandler(http.HandlerFunc(greetHandler), "greet").ServeHTTP(w, r) + }) +
前半のdefer shutdown(ctx)まではヘルプの通り。
出来上がったコードを見るとなんとなく雰囲気がわかるが、やっていることとしては、本来実行される関数(rootHandlerなど)をOpenTelemetryの関数でラップすることでトレースを取得している。
Dockerfileの調整
もとのアプリケーションのコードから必要なパッケージが増えたのでgo mod関連のコマンドを追記。
FROM golang:latest WORKDIR /app COPY main.go . ### 追記ここから COPY go.mod . RUN go mod tidy ### 追記ここまで RUN go build -o hello-server main.go ENTRYPOINT [ "/app/hello-server" ]
アプリケーションからトレースを送信
docker compose buildとdocker compose upを実行し、ブラウザでhttp://localhost:8000/にアクセスしたところ、Hello, World! が表示されることを確認。
Mackerel側のトレース画面を見たところ、無事トレースも送信されていることを確認した。
OpenTelemetry Collectorを使ってトレースを送信
OpenTelemetry CollectorもDockerで稼働させる。Collectorの設定方法は下記のヘルプを参考にする。
OpenTelemetry Collectorの追加
docker-compose.yaml
services: hello-server: build: context: ./app dockerfile: Dockerfile ports: - 8000:8000 # Collectorで送信するのでAPIキーの読み込みは不要 # env_file: ./.env # 以下を追記 otel-collector: image: otel/opentelemetry-collector-contrib:latest volumes: - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml ports: - 4318:4318 env_file: ./.env
以下をdocker-compose.yamlなどと同じ階層に作成する。
otel-collector-config.yaml
receivers: otlp: protocols: http: endpoint: 0.0.0.0:4318 processors: memory_limiter: check_interval: 1s limit_mib: 500 spike_limit_mib: 100 batch: send_batch_size: 5000 send_batch_max_size: 5000 exporters: otlphttp/mackerel: endpoint: "https://otlp-vaxila.mackerelio.com" headers: Accept: "*/*" "Mackerel-Api-Key": ${env:MACKEREL_API_KEY} # デバッグ用。スパンの情報が標準出力されるので便利 debug: verbosity: detailed service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlphttp/mackerel, debug]
アプリケーションのトレースの送信先を変更
変更前
client := otlptracehttp.NewClient(
otlptracehttp.WithEndpoint("otlp-vaxila.mackerelio.com"),
otlptracehttp.WithHeaders(map[string]string{
"Accept": "*/*",
"Mackerel-Api-Key": os.Getenv("MACKEREL_API_KEY"),
}),
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
変更後
client := otlptracehttp.NewClient(
otlptracehttp.WithEndpoint("otel-collector:4318"),
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
otlptracehttp.WithInsecure(),
)
otlptracehttp.WithEndpointにdocker-compose.yamlに追加したCollectorのサービス名とポートを指定する。なお、otlptracehttp.WithEndpointはデフォルトだとhttpsの通信をするのだが、それだとCollectorへの送信に失敗したので、httpで送信するためにotlptracehttp.WithInsecure()を追記している。
ヘッダーやAPIキーはotel-collector-config.yamlで設定してあるので削除する。otlptracehttp.WithCompressionもCollectorの方に任せられそうだが、とりあえずCollectorを使って送信することが目的なのでここでは気にしない。
トレースの送信
コンテナが起動中であれば一旦停止して、docker compose buildとdocker compose upを実行し、ブラウザでhttp://localhost:8000/にアクセスしてMackerelにトレースが送信されていることを確認する。
Goの自動計装の難しいところ
これ以前に、RubyとPython(flask)への自動計装をやったが、それらは計装用のコードを追加するだけでよかったのに対し、Goの場合は既存のコードの書き換えが必要になるので、計装の難易度がアプリケーションの作りやGoへの理解度に大きく依存するなと感じた。
この検証を始めた頃は、kmutoさんのブログにあったhello-serverというアプリケーションを使って計装を進めていたのだが、http.HandleFuncの処理をOpenTelemetryの関数でラップするのが難しく、別の関数として定義し直して、それをラップするという手順を踏む必要があった。これはアプリケーションの構造自体を変える行為なので影響が大きいし、それを行うにはGoへの理解がないと達成できない。
幸い、先述のkmutoさんのブログの通り、Goはzero-code計装という手段があるので、とりあえずやってみたいという場合には、まずはzero-code計装をやってみるのがよさそうだ。