kmuto’s blog

はてな社でMackerel CREをやっています。料理と旅行といろんなIT技術

OpenTelemetryのzero-code計装を試している〜まずはGo

オブザーバビリティでOpenTelemetryの計装をいざ始めよう!というときに、「そう言われても、今あるコードベースに何か追加するのは嫌なんじゃが……」ということはいかにもありそうな話。

そこでOpenTelemetryが提供している手法としてzero-code instrumentation、いわゆる自動計装というものがある。

この手法では、エージェントあるいはエージェントライクなものとして、バイトコード操作、モンキーパッチ、eBPFなどの手段でアプリケーションに計装が挿入される。現時点で公式ページに書かれているのは.NET、Go、JavaJavaScriptPHPPythonとなっている。

どの程度これが実用的、あるいはユーザーにとって嬉しさを感じられそうかを知っておこうと、1日1言語…とやるつもりだったけどあまり時間的余裕がなく、ここ数日ではGoと.NETを試していた。今日はGoの結果をまとめておく。

Goのはymtdzzzさんの記事にだいたい全部書かれているので、ちゃんとしたことはそっちを見たほうが早い。本記事もその追試にすぎないと言える。

ymtdzzz.dev

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:5000http://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:5000http//localhost:5000/errorcurlでアクセスしてトレースを送ってみる。

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版のお試しはここまで。