オブザーバビリティで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版のお試しはここまで。