以下の内容はhttps://masarasi.hatenablog.com/entry/2025/04/25/161624より取得しました。


MackerelにGoのトレースを送信する〜自動計装〜

アプリケーションから直接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のヘルプを参考に計装していく。

mackerel.io

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 builddocker compose upを実行し、ブラウザでhttp://localhost:8000/にアクセスしたところ、Hello, World! が表示されることを確認。

Mackerel側のトレース画面を見たところ、無事トレースも送信されていることを確認した。

OpenTelemetry Collectorを使ってトレースを送信

OpenTelemetry CollectorもDockerで稼働させる。Collectorの設定方法は下記のヘルプを参考にする。

mackerel.io

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.WithEndpointdocker-compose.yamlに追加したCollectorのサービス名とポートを指定する。なお、otlptracehttp.WithEndpointはデフォルトだとhttpsの通信をするのだが、それだとCollectorへの送信に失敗したので、httpで送信するためにotlptracehttp.WithInsecure()を追記している。

ヘッダーやAPIキーはotel-collector-config.yamlで設定してあるので削除する。otlptracehttp.WithCompressionもCollectorの方に任せられそうだが、とりあえずCollectorを使って送信することが目的なのでここでは気にしない。

トレースの送信

コンテナが起動中であれば一旦停止して、docker compose builddocker compose upを実行し、ブラウザでhttp://localhost:8000/にアクセスしてMackerelにトレースが送信されていることを確認する。

Goの自動計装の難しいところ

これ以前に、RubyPython(flask)への自動計装をやったが、それらは計装用のコードを追加するだけでよかったのに対し、Goの場合は既存のコードの書き換えが必要になるので、計装の難易度がアプリケーションの作りやGoへの理解度に大きく依存するなと感じた。

この検証を始めた頃は、kmutoさんのブログにあったhello-serverというアプリケーションを使って計装を進めていたのだが、http.HandleFuncの処理をOpenTelemetryの関数でラップするのが難しく、別の関数として定義し直して、それをラップするという手順を踏む必要があった。これはアプリケーションの構造自体を変える行為なので影響が大きいし、それを行うにはGoへの理解がないと達成できない。

幸い、先述のkmutoさんのブログの通り、Goはzero-code計装という手段があるので、とりあえずやってみたいという場合には、まずはzero-code計装をやってみるのがよさそうだ。




以上の内容はhttps://masarasi.hatenablog.com/entry/2025/04/25/161624より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14