オブザーバビリティでOpenTelemetryの計装をいざ始めよう!というときに、「そう言われても、今あるコードベースに何か追加するのは嫌なんじゃが……」ということはいかにもありそうな話。
そこでOpenTelemetryが提供している手法としてzero-code instrumentation、いわゆる自動計装というものがある。
この手法では、エージェントあるいはエージェントライクなものとして、バイトコード操作、モンキーパッチ、eBPFなどの手段でアプリケーションに計装が挿入される。現時点で公式ページに書かれているのは.NET、Go、Java、JavaScript、PHP、Pythonとなっている。
どの程度これが実用的、あるいはユーザーにとって嬉しさを感じられそうかを知っておこうと、1日1言語…とやるつもりだったけどあまり時間的余裕がなく、ここ数日ではGoと.NETを試していた。今日はGoの結果をまとめておく。
Goのはymtdzzzさんの記事にだいたい全部書かれているので、ちゃんとしたことはそっちを見たほうが早い。本記事もその追試にすぎないと言える。
Go言語のzero-code計装(opentelemetry-go-instrumentation)
Go言語の場合は実行バイナリがそれだけで完結しているので、ランタイムに割り込むようなことができない。
公式で案内されているopentelemetry-go-instrumentationは、LinuxカーネルのeBPFを使ってイベントを取得する手段をとっている。そのため、Linuxネイティブでない場合は、Dockerイメージあるいは適当なLinux VMを立てて代用することになる。
ネイティブDebian GNU/Linux環境なので、普通にGitHubから展開してotel-go-instrumentationをビルドした。
git clone https://github.com/open-telemetry/opentelemetry-go-instrumentation.git
cd opentelemetry-go-instrumentation
make build
次に、TCP/5000ポートで動き、指定パスによってエラーを起こす適当なアプリケーションhello-server
を作ってビルドしておく。
package main import "net/http" func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, World!\n")) }) http.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal Server Error", http.StatusInternalServerError) }) http.ListenAndServe(":5000", nil) }
OpenTelemetry Collectorを起動しておく。トレースがきてるのがわかればいいので設定は適当。
receivers: otlp: protocols: http: exporters: debug: verbosity: detailed service: pipelines: traces: receivers: [otlp] exporters: [debug]
では試してみよう。
otel-go-instrumentation
の実行バイナリはカレントフォルダにあるとする- アプリケーションの実行バイナリは
/home/kmuto/hello-server/hello-server
パスにあるとする
対象バイナリが実行されるのを待ち構える。
sudo OTEL_GO_AUTO_TARGET_EXE=/home/kmuto/hello-server/hello-server OTEL_SERVICE_NAME=go-zerocode OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 ./otel-go-instrumentation
/home/kmuto/hello-server/hello-server
を実行し、curlなどでhttp://localhost:5000
とhttp://localhost:5000/error
にアクセスすると、OpenTelemetry Collectorのほうにトレースとスパンが出力される。
2025-01-18T23:04:27.708+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1} 2025-01-18T23:04:27.708+0900 info ResourceSpans #0 Resource SchemaURL: https://opentelemetry.io/schemas/1.26.0 Resource attributes: -> process.runtime.description: Str(go version 1.23.1 linux/amd64) -> process.runtime.name: Str(go) -> process.runtime.version: Str(1.23.1) -> service.name: Str(go-zerocode) -> telemetry.distro.name: Str(opentelemetry-go-instrumentation) -> telemetry.distro.version: Str(v0.19.0-alpha) -> telemetry.sdk.language: Str(go) ScopeSpans #0 ScopeSpans SchemaURL: https://opentelemetry.io/schemas/1.26.0 InstrumentationScope go.opentelemetry.io/auto/net/http v0.19.0-alpha Span #0 Trace ID : f52b9e7c56d58516909ab0dbf4f6d866 Parent ID : ID : 4f212cd3e2c29532 Name : GET / Kind : Server Start time : 2025-01-18 14:04:23.501669372 +0000 UTC End time : 2025-01-18 14:04:23.50167801 +0000 UTC Status code : Unset Status message : Attributes: -> http.request.method: Str(GET) -> url.path: Str(/) -> http.response.status_code: Int(200) -> network.peer.address: Str(127.0.0.1) -> network.peer.port: Int(40270) -> server.address: Str(localhost) -> server.port: Int(5000) -> network.protocol.version: Str(1.1) -> http.route: Str(/) {"kind": "exporter", "data_type": "traces", "name": "debug"} 2025-01-18T23:04:32.712+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1} 2025-01-18T23:04:32.712+0900 info ResourceSpans #0 Resource SchemaURL: https://opentelemetry.io/schemas/1.26.0 Resource attributes: -> process.runtime.description: Str(go version 1.23.1 linux/amd64) -> process.runtime.name: Str(go) -> process.runtime.version: Str(1.23.1) -> service.name: Str(go-zerocode) -> telemetry.distro.name: Str(opentelemetry-go-instrumentation) -> telemetry.distro.version: Str(v0.19.0-alpha) -> telemetry.sdk.language: Str(go) ScopeSpans #0 ScopeSpans SchemaURL: https://opentelemetry.io/schemas/1.26.0 InstrumentationScope go.opentelemetry.io/auto/net/http v0.19.0-alpha Span #0 Trace ID : 6d7626cdf485322abd7d2785d00b31b9 Parent ID : ID : 2c801415b9953b48 Name : GET /error Kind : Server Start time : 2025-01-18 14:04:30.454387577 +0000 UTC End time : 2025-01-18 14:04:30.454400865 +0000 UTC Status code : Error Status message : Attributes: -> http.request.method: Str(GET) -> url.path: Str(/error) -> http.response.status_code: Int(500) -> network.peer.address: Str(127.0.0.1) -> network.peer.port: Int(41402) -> server.address: Str(localhost) -> server.port: Int(5000) -> network.protocol.version: Str(1.1) -> http.route: Str(/error) {"kind": "exporter", "data_type": "traces", "name": "debug"}
1トレース1スパンでこれ自体はさして面白いものではないが、エラーのときはちゃんとエラーのstatus codeになっている。
Vaxilaではうまく受け取れなかった。Jaegerでもなんか変な気がする。
Go言語のzero-code計装(opentelemetry-go-auto-instrumentation)
もう1つのzero-codeとしては、alibaba/opentelemetry-go-auto-instrumentationがある。これはビルド時に計装を仕込むもの。
Alibabaということに少々ドキドキはするのだが、さすがに目に見えるもので仕込んではこないだろう…。とは言うものの、全部Dockerで閉じた環境にした。
Dockerfile
を用意。
FROM golang:1.23 WORKDIR /usr/src/app RUN apt update \ && apt install -y sudo curl \ && curl -fsSL https://cdn.jsdelivr.net/gh/alibaba/opentelemetry-go-auto-instrumentation@main/install.sh | bash COPY go.mod main.go ./ RUN go mod download & go mod verify RUN otel go build -o hello-server-alibaba main.go CMD ["./hello-server-alibaba"]
docker-compose.yml
。
services: alibaba: build: context: . dockerfile: ./Dockerfile ports: - "5000:5000" environment: OTEL_EXPORTER_OTLP_ENDPOINT: "http://otelcol:4318" OTEL_EXPORTER_OTLP_INSECURE: true OTEL_SERVICE_NAME: "go-zerocode-alibaba" otelcol: image: otel/opentelemetry-collector-contrib:latest volumes: - ./otel-col-alibaba.yaml:/etc/otelcol-contrib/config.yaml ports: - "4318:4318"
otel-col-alibaba.yaml
はバインドアドレスをグローバルにしただけ。
receivers: otlp: protocols: http: endpoint: "0.0.0.0:4318" exporters: debug: verbosity: detailed service: pipelines: traces: receivers: [otlp] exporters: [debug]
docker compose up
で起動し、http://localhost:5000
やhttp//localhost:5000/error
にcurlでアクセスしてトレースを送ってみる。
otelcol-1 | 2025-01-18T15:02:31.587Z info TracesExporter {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 2} otelcol-1 | 2025-01-18T15:02:31.587Z info ResourceSpans #0 otelcol-1 | Resource SchemaURL: https://opentelemetry.io/schemas/1.26.0 otelcol-1 | Resource attributes: otelcol-1 | -> service.name: Str(go-zerocode-alibaba) otelcol-1 | -> telemetry.sdk.language: Str(go) otelcol-1 | -> telemetry.sdk.name: Str(opentelemetry) otelcol-1 | -> telemetry.sdk.version: Str(1.33.0) otelcol-1 | ScopeSpans #0 otelcol-1 | ScopeSpans SchemaURL: otelcol-1 | InstrumentationScope pkg/rules/http/server_setup.go v0.7.0 otelcol-1 | Span #0 otelcol-1 | Trace ID : 3b2ffa25076e74ef6b111af48c2494df otelcol-1 | Parent ID : otelcol-1 | ID : c3c9a8fbc916debf otelcol-1 | Name : GET / otelcol-1 | Kind : Server otelcol-1 | Start time : 2025-01-18 15:02:27.286134194 +0000 UTC otelcol-1 | End time : 2025-01-18 15:02:27.286153625 +0000 UTC otelcol-1 | Status code : Unset otelcol-1 | Status message : otelcol-1 | Attributes: otelcol-1 | -> http.request.method: Str(GET) otelcol-1 | -> url.scheme: Str(http) otelcol-1 | -> url.path: Str(/) otelcol-1 | -> url.query: Str() otelcol-1 | -> user_agent.original: Str(curl/7.88.1) otelcol-1 | -> http.response.status_code: Int(200) otelcol-1 | -> network.protocol.name: Str(http) otelcol-1 | -> network.protocol.version: Str(1.1) otelcol-1 | -> network.transport: Str(tcp) otelcol-1 | -> network.type: Str(ipv4) otelcol-1 | -> network.local.address: Str() otelcol-1 | -> network.peer.address: Str(localhost:5000) otelcol-1 | -> http.route: Str(/) otelcol-1 | Span #1 otelcol-1 | Trace ID : 5c071af6c91bb08a9a34fcbe942b2b70 otelcol-1 | Parent ID : otelcol-1 | ID : dd5524e5dbf689c9 otelcol-1 | Name : GET /error otelcol-1 | Kind : Server otelcol-1 | Start time : 2025-01-18 15:02:28.919892825 +0000 UTC otelcol-1 | End time : 2025-01-18 15:02:28.919909903 +0000 UTC otelcol-1 | Status code : Error otelcol-1 | Status message : INVALID_HTTP_STATUS_CODE otelcol-1 | Attributes: otelcol-1 | -> http.request.method: Str(GET) otelcol-1 | -> url.scheme: Str(http) otelcol-1 | -> url.path: Str(/error) otelcol-1 | -> url.query: Str() otelcol-1 | -> user_agent.original: Str(curl/7.88.1) otelcol-1 | -> http.response.status_code: Int(500) otelcol-1 | -> network.protocol.name: Str(http) otelcol-1 | -> network.protocol.version: Str(1.1) otelcol-1 | -> network.transport: Str(tcp) otelcol-1 | -> network.type: Str(ipv4) otelcol-1 | -> network.local.address: Str() otelcol-1 | -> network.peer.address: Str(localhost:5000) otelcol-1 | -> http.route: Str(/error) otelcol-1 | {"kind": "exporter", "data_type": "traces", "name": "debug"} otelcol-1 | 2025-01-18T15:02:36.588Z info TracesExporter {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1} otelcol-1 | 2025-01-18T15:02:36.588Z info ResourceSpans #0 otelcol-1 | Resource SchemaURL: https://opentelemetry.io/schemas/1.26.0 otelcol-1 | Resource attributes: otelcol-1 | -> service.name: Str(go-zerocode-alibaba) otelcol-1 | -> telemetry.sdk.language: Str(go) otelcol-1 | -> telemetry.sdk.name: Str(opentelemetry) otelcol-1 | -> telemetry.sdk.version: Str(1.33.0) otelcol-1 | ScopeSpans #0 otelcol-1 | ScopeSpans SchemaURL: otelcol-1 | InstrumentationScope pkg/rules/http/client_setup.go v0.7.0 otelcol-1 | Span #0 otelcol-1 | Trace ID : f3c89464869497c7d71975e077f55b6f otelcol-1 | Parent ID : otelcol-1 | ID : 76adf95e3720a020 otelcol-1 | Name : POST otelcol-1 | Kind : Client otelcol-1 | Start time : 2025-01-18 15:02:31.587172333 +0000 UTC otelcol-1 | End time : 2025-01-18 15:02:31.5880139 +0000 UTC otelcol-1 | Status code : Unset otelcol-1 | Status message : otelcol-1 | Attributes: otelcol-1 | -> http.request.method: Str(POST) otelcol-1 | -> url.full: Str(http://otelcol:4318/v1/traces) otelcol-1 | -> server.address: Str(otelcol:4318) otelcol-1 | -> server.port: Int(4318) otelcol-1 | -> http.response.status_code: Int(200) otelcol-1 | -> network.protocol.name: Str(http) otelcol-1 | -> network.protocol.version: Str(1.1) otelcol-1 | -> network.transport: Str(tcp) otelcol-1 | -> network.type: Str(ipv4) otelcol-1 | -> network.local.address: Str() otelcol-1 | -> network.peer.address: Str(otelcol:4318) otelcol-1 | -> network.peer.port: Int(4318) otelcol-1 | {"kind": "exporter", "data_type": "traces", "name": "debug"}
1トレース、1スパンであることは同じだが属性は少し細かい感じ。Status messageが丁寧になっている。Collectorへの送信自体もトレースにのっけてくるのかな。
こちらはVaxilaでもうまく取ることができている。
バイナリはそれなりに大きくなった。普通のビルドだと7,521,614バイト、otelビルドだと22,813,463バイト。
とりあえずGo版のお試しはここまで。