Skip to content

OpenTelemetryを使った計装の始め方

This content is a draft and will not be included in production builds.

コマンドラインツールは、サーバーアプリケーションと異なりテレメトリーデータの送信先バックエンドサービスを限定できません。社内のみで使われるツールであっても、異なるチームが別のオブザーバビリティバックエンドを使っていることはあるでしょうし、広く配布しているなら尚更です。そのためコマンドラインツールにバックエンドサービスや設定変更のオプションが必要となるのですが、OpenTelemetryのSDKはもともと環境変数を使ってテレメトリーデータの送信先を切り替えたり、属性を付与したりできるようになっていますので、独自のオプションを用意するよりも公式の環境変数を使って切り替えをする方が、開発も楽になるしユーザーにとっても分かりやすいでしょう。

以下ではOpenTelemetry Go API and SDKを使って実装例をみていきますが、どの言語でも同じ環境変数を参照できるようになっていると思います。利用可能な環境変数のリスト((SDKによってはotlpmetricgrpcのように以下より多くの環境変数をサポートしている場合もあります))は以下を参照してください。

実装としてはサーバーアプリケーションとそれほど違いはありません。ただしOTLPエクスポーターはデフォルトで localhost:4317 と通信してしまうので、必要なときだけテレメトリーデータを送出するようにしたほうが驚きが少ないでしょう。なのでコマンドラインオプションで -export を与えた場合だけ有効にするための実装を入れていきます。

import (
"context"
"flag"
"os/signal"
)
func main() {
flag.Parse()
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
if *exportFlag {
meterProviderShutdown := registerMeterProvider(ctx)
defer meterProviderShutdown(ctx)
tracerProviderShutdown := registerTracerProvider(ctx)
defer tracerProviderShutdown(ctx)
}
...
}

registerMeterProviderregisterTracerProvider はほぼ同等の関数で、OTLPエクスポーターを初期化しているだけです。エラーハンドリング周りはもっとシンプルな書き方ができるかもしれませんが、あまり時間がなくて調べきれませんでした。

import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
// otlpmetricgrpc部分はバックエンドに合わせて切り替えてください
func registerMeterProvider(ctx context.Context) func(context.Context) {
metricExporter, err := otlpmetricgrpc.New(ctx)
if err != nil {
otel.Handle(err)
return func(context.Context) {}
}
meterProvider := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter)),
)
otel.SetMeterProvider(meterProvider)
return func(ctx context.Context) {
if err := meterProvider.Shutdown(ctx); err != nil {
otel.Handle(err)
}
}
}
// otlptracehttp部分はバックエンドに合わせて切り替えてください
func registerTracerProvider(ctx context.Context) func(context.Context) {
traceExporter, err := otlptracehttp.New(ctx)
if err != nil {
otel.Handle(err)
return func(context.Context) {}
}
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(traceExporter),
)
otel.SetTracerProvider(tracerProvider)
return func(ctx context.Context) {
if err := tracerProvider.Shutdown(ctx); err != nil {
otel.Handle(err)
}
}
}

これでOpenTelemetry SDKの準備は終わりです。あとは必要なところでトレースやメトリックを計装しましょう。OpenTelemetry SDKでは各種プロバイダーを設定していない場合「何もしない」プロバイダーが使われるので、-export オプションの有無に関わらず otel.Tracerotel.Meter を使えます。

import (
"context"
"log"
"math/rand/v2"
"reflect"
"sync"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/noop"
"go.opentelemetry.io/otel/trace"
)
func main() {
...
tracer := otel.Tracer(reflect.TypeOf(t{}).PkgPath())
ctx, span := tracer.Start(ctx, "main")
defer span.End()
var wg sync.WaitGroup
for _, url := range []string{"https://www.google.com/", "https://example.com/"} {
wg.Go(func() {
fetchData(ctx, url)
})
}
wg.Wait()
}
func fetchData(ctx context.Context, url string) {
log.Println("fetching", url)
urlAttr := attribute.String("url", url)
// トレースの計装
tracer := otel.Tracer(reflect.TypeOf(t{}).PkgPath())
ctx, span := tracer.Start(ctx, "fetchData", trace.WithAttributes(urlAttr))
defer span.End()
// メトリックの計装
meter := otel.Meter(reflect.TypeOf(t{}).PkgPath())
histogram, err := meter.Int64Histogram("data_size")
if err != nil {
otel.Handle(err)
histogram = noop.Int64Histogram{}
}
// 以下はダミー処理です、処理内容にはとくに意味はありません
n := rand.N[time.Duration](100)
select {
case <-ctx.Done():
span.RecordError(ctx.Err(), trace.WithStackTrace(true))
case <-time.After(n * time.Millisecond):
histogram.Record(ctx, rand.Int64(), metric.WithAttributes(urlAttr))
}
}

これをそのまま実行すると、-export がないためテレメトリーデータの送信は行われません。-export を付けるとデフォルトのオブザーバビリティバックエンドへ送られるのですが、デフォルトは .http://localhost:4317 または .http://localhost:4318 となっているので、環境変数で切り替えてMackerelへ送ってみます。このときAPIキーも必要なので設定します。

Terminal window
MACKEREL_APIKEY=xxx
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://otlp.mackerelio.com:4317
export OTEL_EXPORTER_OTLP_METRICS_HEADERS="Mackerel-Api-Key=$MACKEREL_APIKEY"
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://otlp-vaxila.mackerelio.com/v1/traces
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Mackerel-Api-Key=$MACKEREL_APIKEY"
export OTEL_SERVICE_NAME=service1
export OTEL_RESOURCE_ATTRIBUTES="host.name=$(uname -n),user.name=$USER"

同じURLへ送ることができるバックエンドの場合は OTEL_EXPORTER_OTLP_ENDPOINT 環境変数を使ってまとめて設定できますが、Mackerelでは色々と事情があって分かれているため、トレースとメトリックで送信先を分ける必要があって少し面倒ですね。属性については、なるべく属性名をOpenTelemetry semantic conventionsに合わせておくことを推奨します。