【Go 編】OTel のドキュメントを読む

概要
OTel のドキュメントを読むことで、実装と用語の対応づけの理解を深めていきます。
今回は、Go 編。

計装

トレーサーの取得
- otel-tui に送信する Exporter の設定
- TraceProvider の設定。Trace しか利用しないのであれば、Exporter の設定と分離することは不要だけど、Metric と Log の Provider が存在するから、わけているんだと思う
var tracer trace.Tracer
func newOTelTUIExporter(ctx context.Context) (*otlptrace.Exporter, error) {
// Get New OTel TUI endpoint from environment variable or use default
endpoint := os.Getenv("OTLP_ENDPOINT")
if endpoint == "" {
return nil, fmt.Errorf("OTLP_ENDPOINT environment variable is required")
}
log.Printf("Initializing OpenTelemetry with OTLP endpoint: %s", endpoint)
// Create OTLP trace exporter with New Relic configuration
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("failed to create trace exporter: %w", err)
}
return exporter, nil
}
func newTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
// Create resource with service information
res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("go-app"),
),
)
if err != nil {
panic(err)
}
// Create TracerProvider
return sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(res),
)
}
resource とは、エンティティに付与する情報のこと。k8s だとネームスペース、ポッド名などが含まれることがある。
利用するときには、下のように書く。
func main() {
// Initialize OpenTelemetry
ctx := context.Background()
exp, err := newOTelTUIExporter(ctx)
if err != nil {
log.Fatalf("failed to create exporter: %v", err)
}
tp := newTracerProvider(exp)
defer func() { _ = tp.Shutdown(ctx) }()
otel.SetTracerProvider(tp)
tracer = tp.Tracer("go-app")
略
}
リクエストを送ると下のような感じになる。
curl http://localhost:8080/users/2

スパンの作成
tracer.Start
で span をさらに区切ることができる。
func getHello(w http.ResponseWriter, r *http.Request) {
_, span := tracer.Start(r.Context(), "getHello")
defer span.End()
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]string{"message": fmt.Sprintf("Hello, %s!", name)})
w.Write(data)
}
実効結果は以下。
curl http://localhost:8080/hello
hello リクエスト内にあたらしい span(getHello
)が区切られていることがわかる。
kind はどうやら Interal になるらしい。hello リクエストは Server だった。
AI によると第3引数で、span の制御ができるらしい。
_, span := tracer.Start(r.Context(), "getHello")
この行には第3引数がありません。tracer.Startは2つの引数のみを受け取っていま
す:
1. r.Context() - コンテキスト
2. "getHello" - スパン名
ただし、tracer.Startメソッドは実際には可変長引数で第3引数以降にオプション
を指定できます。例えば:
_, span := tracer.Start(r.Context(), "getHello",
trace.WithAttributes(attribute.String("key", "value")),
trace.WithSpanKind(trace.SpanKindServer),
)
これらのオプションで以下のような設定が可能です:
- trace.WithAttributes() - スパンに属性を追加
- trace.WithSpanKind() - スパンの種類を指定(Server, Client, Producer,
Consumer, Internal)
- trace.WithLinks() - 他のスパンへのリンクを追加
- trace.WithNewRoot() - 新しいトレースのルートとして開始
現在のコードでは第3引数を使用していないため、デフォルト設定でスパンが作成
されています。

現在のスパンを取得
span を取得するやつ。
span を引数に渡すのではなく、ctx を引き回せば良いので、Go っぽい書き方 & 後付けで実装しやすそう。
// このコンテキストは、抽出予定のアクティブなスパンを含む必要があります
ctx := context.TODO()
span := trace.SpanFromContext(ctx)
// 現在のスパンで何かを行い、オプションで終了したい場合は`span.End()`を呼び出します

ネストしたスパンの作成
tracer.Start
で生成した ctx を別の tracer.Start
の引数に渡すと、親子関係が作れる。
func getHello(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "getHello")
defer span.End()
childHello(ctx)
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]string{"message": fmt.Sprintf("Hello, %s!", name)})
w.Write(data)
}
func childHello(ctx context.Context) {
_, childSpan := tracer.Start(ctx, "childHello")
defer childSpan.End()
fmt.Println("This is a child function")
}
span を生成するときの context を利用しないと、親子関係が関連づけられない。
func getHello(w http.ResponseWriter, r *http.Request) {
_, span := tracer.Start(r.Context(), "getHello")
defer span.End()
childHello(r.Context())
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]string{"message": fmt.Sprintf("Hello, %s!", name)})
w.Write(data)
}
func childHello(ctx context.Context) {
_, childSpan := tracer.Start(ctx, "childHello")
defer childSpan.End()
fmt.Println("This is a child function")
}

スパン属性
スパン属性の追加について、いかのどちらのパターンでも作成可能。
- スパン生成時
- スパン生成後
属性のキーの構造体を作成して、属性を扱うことも可能。
func getHello(w http.ResponseWriter, r *http.Request) {
// span の作成。作成次に属性を設定可能
ctx, span := tracer.Start(r.Context(), "getHello", trace.WithAttributes(attribute.String("hello", "world")))
// さらに属性を追加することも可能
span.SetAttributes(attribute.Bool("isTrue", true), attribute.String("stringAttr", "hi!"))
// 属性のキーは、事前に定義されたものも利用できる
myKey := attribute.Key("myCoolAttribute")
span.SetAttributes(myKey.String("a value"))
defer span.End()
childHello(ctx)
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]string{"message": fmt.Sprintf("Hello, %s!", name)})
w.Write(data)
}

セマンティック属性
いわゆる semconv
のこと。
独自の属性のキー名を作ることになったときに、同期が大変そう。

イベント
Event という属性を追加できる。
mutex で排他処理をしているときや、特定の分岐に入る時などに利用できそう。
func getHello(w http.ResponseWriter, r *http.Request) {
... 略
// AddEvent により特定のタイミングで、Event を追加可能。
span.AddEvent("Hello with AddEvent")
childHello(ctx)
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]string{"message": fmt.Sprintf("Hello, %s!", name)})
w.Write(data)
}

スパンステータスの設定
span.SetStatus
を利用すると、Status
キーが追加される。
主に、Error
を強調するときにつかう。
func getError(w http.ResponseWriter, r *http.Request) {
// span の作成。作成次に属性を設定可能
_, span := tracer.Start(r.Context(), "getError")
defer span.End()
// ステータスにエラーを設定。設定すると、このスパンだけでなく、トレース全体がエラーとして扱われる
span.SetStatus(codes.Error, "Internal Server Error")
// span.RecordError(fmt.Errorf("err: Internal Server Error"))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
data, _ := json.Marshal(map[string]string{
"error": "Internal Server Error",
"message": "This is a sample 500 error endpoint for testing OpenTelemetry",
})
w.Write(data)
}

エラーの記録
span.RecordError
を利用すると、Event にエラーの情報を付加できる。
ただし、ステータスがエラーになるのは直前のスパンまでのため、span.SetStatus
と合わせて使う。
func getError(w http.ResponseWriter, r *http.Request) {
// span の作成。作成次に属性を設定可能
_, span := tracer.Start(r.Context(), "getError")
defer span.End()
// ステータスにエラーを設定。設定すると、このスパンだけでなく、トレース全体がエラーとして扱われる
span.SetStatus(codes.Error, "Internal Server Error")
// Event にエラー情報を追加する。このメソッドだけでは、トレース全体のステータスは変わらない(厳密には、直前のスパンまでエラーステータスになる)ため、span.SetStatus と合わせて使う
span.RecordError(fmt.Errorf("err: Internal Server Error"))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
data, _ := json.Marshal(map[string]string{
"error": "Internal Server Error",
"message": "This is a sample 500 error endpoint for testing OpenTelemetry",
})
w.Write(data)
}
Event が付加された画像。span.SetStatus
だけでは付与されない。
一方で、span.SetStatus
をしないとトレース全体がエラーにならない。そんのため、span.RecordError
を利用するときは、両方使う

プロパゲーターとコンテキスト
otel.SetTextMapPropagator
で他のエンティティとトレースIDを共有する。
func main() {
// Initialize OpenTelemetry
ctx := context.Background()
exp, err := newOTelTUIExporter(ctx)
if err != nil {
log.Fatalf("failed to create exporter: %v", err)
}
tp := newTracerProvider(exp)
defer func() { _ = tp.Shutdown(ctx) }()
otel.SetTracerProvider(tp)
// 伝搬を設定。nginx や他サービスとのトレースIDの受け渡しに利用できる
otel.SetTextMapPropagator(propagation.TraceContext{})
tracer = tp.Tracer("go-app")
// Create chi router
r := chi.NewRouter()
// 中略
log.Fatal(http.ListenAndServe(":8080", r))
}

メトリクス

まとめて実装したので省略。
メトリクスの初期化
メーターの取得
同期および非同期計装
カウンターの使用
Exporter を設定
func newOTelMetricExporter(ctx context.Context) (sdkmetric.Exporter, error) {
// Get OTLP endpoint from environment variable
endpoint := os.Getenv("OTLP_ENDPOINT")
if endpoint == "" {
return nil, fmt.Errorf("OTLP_ENDPOINT environment variable is required")
}
log.Printf("Initializing OpenTelemetry Metrics with OTLP endpoint: %s", endpoint)
// Create OTLP metric exporter
exporter, err := otlpmetricgrpc.New(ctx,
otlpmetricgrpc.WithEndpoint(endpoint),
otlpmetricgrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("failed to create metric exporter: %w", err)
}
return exporter, nil
}
Resource の作成。
// Create resource with service information
func newResource() (*resource.Resource, error) {
return resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("go-app"),
),
)
}
MeterProvider を作成
func newMeterProvider(metricExporter sdkmetric.Exporter, res *resource.Resource) *sdkmetric.MeterProvider {
return sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(
sdkmetric.NewPeriodicReader(metricExporter,
// デモ目的で3sに設定(デフォルトは1m)
sdkmetric.WithInterval(3*time.Second)),
),
)
}
Provider の初期化
mp := newMeterProvider(metricExp, res)
defer func() {
log.Printf("Shutting down meter provider...")
if err := mp.Shutdown(ctx); err != nil {
log.Fatalf("failed to shutdown meter provider: %v", err)
}
log.Printf("Meter provider shutdown complete")
}()
otel.SetMeterProvider(mp)
k
メトリクスカウンターを作成
// メトリクスカウンターを作成
meter = otel.Meter("go-app")
requestCounter, err = meter.Int64Counter(
"api.counter",
metric.WithDescription("Number of API calls"),
metric.WithUnit("{call}"),
)
if err != nil {
log.Fatalf("failed to create request counter: %v", err)
}
log.Printf("Request counter created successfully")

UpDownカウンターの使用
func addItem(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "addItem")
defer span.End()
// itemsCounterをインクリメント
if itemsCounter != nil {
itemsCounter.Add(ctx, 1)
log.Printf("Incremented items counter")
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]string{
"message": "Item added successfully",
"action": "increment",
})
w.Write(data)
}
func removeItem(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "removeItem")
defer span.End()
// itemsCounterをデクリメント
if itemsCounter != nil {
itemsCounter.Add(ctx, -1)
log.Printf("Decremented items counter")
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]string{
"message": "Item removed successfully",
"action": "decrement",
})
w.Write(data)
}
func main() {
// 中略
itemsCounter, err = meter.Int64UpDownCounter(
"items.counter",
metric.WithDescription("Number of items."),
metric.WithUnit("{item}"),
)
if err != nil {
log.Fatalf("failed to create items counter: %v", err)
}
// 中略
r.Post("/items/add", addItem)
r.Post("/items/remove", removeItem)
}

ゲージの使用
var (
fanSpeedSubsciption chan int64
speedGauge metric.Int64Gauge
)
// 中略
func getCPUFanSpeedHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "getCPUFanSpeed")
defer span.End()
// fanSpeedSubsciptionから最新の値を非ブロッキングで取得
var fanSpeed int64
select {
case speed, ok := <-fanSpeedSubsciption:
if ok {
fanSpeed = speed
// Gaugeメトリクスを記録
if speedGauge != nil {
speedGauge.Record(ctx, fanSpeed)
log.Printf("Recorded fan speed: %d rpm", fanSpeed)
}
} else {
// チャンネルがクローズされている場合はランダムな値を生成
fanSpeed = int64(1500 + rand.Intn(1000))
if speedGauge != nil {
speedGauge.Record(ctx, fanSpeed)
}
}
default:
// チャンネルに値がない場合はランダムな値を生成
fanSpeed = int64(1500 + rand.Intn(1000))
if speedGauge != nil {
speedGauge.Record(ctx, fanSpeed)
}
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]interface{}{
"fanSpeed": fanSpeed,
"unit": "rpm",
"message": "Current CPU fan speed",
})
w.Write(data)
}
// 中略
func main() {
// 中略
speedGauge, err = meter.Int64Gauge(
"cpu.fan.speed",
metric.WithDescription("CPU Fan Speed"),
metric.WithUnit("{rpm}"),
)
if err != nil {
log.Fatalf("failed to create speed gauge: %v", err)
}
getCPUFanSpeed := func() int64 {
// デモンストレーション目的でランダムなファン速度を生成します
// 実際のアプリケーションでは、これを実際のファン速度を取得するように置き換えてください
return int64(1500 + rand.Intn(1000))
}
fanSpeedSubsciption = make(chan int64, 1)
go func() {
defer close(fanSpeedSubsciption)
for idx := 0; idx < 5; idx++ {
time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
fanSpeed := getCPUFanSpeed()
fanSpeedSubsciption <- fanSpeed
}
}()
// 中略
r.Get("/cpu/fanspeed", getCPUFanSpeedHandler)
}

ヒストグラムの使用
var (
// 略
histogram metric.Float64Histogram
)
// 略
func callExternalAPI(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "callExternalAPI")
defer span.End()
// 処理開始時刻を記録
startTime := time.Now()
// 外部APIコールをエミュレート(50ms~10秒のランダムな遅延、より分散させる)
// 50% : 50ms - 1秒 (高速レスポンス)
// 30% : 1秒 - 5秒 (中程度のレスポンス)
// 20% : 5秒 - 10秒 (遅いレスポンス)
var apiLatency time.Duration
randValue := rand.Float32()
if randValue < 0.5 {
// 50ms - 1000ms
apiLatency = time.Duration(50+rand.Intn(950)) * time.Millisecond
} else if randValue < 0.8 {
// 1秒 - 5秒
apiLatency = time.Duration(1000+rand.Intn(4000)) * time.Millisecond
} else {
// 5秒 - 10秒
apiLatency = time.Duration(5000+rand.Intn(5000)) * time.Millisecond
}
// スパンに属性を追加
span.SetAttributes(
attribute.String("api.endpoint", "https://api.example.com/data"),
attribute.String("api.method", "GET"),
attribute.Int64("api.latency_ms", int64(apiLatency.Milliseconds())),
)
// 外部APIコールの開始をイベントとして記録
span.AddEvent("External API call started", trace.WithAttributes(
attribute.String("api.url", "https://api.example.com/data"),
))
// 外部APIコールをエミュレート
time.Sleep(apiLatency)
// 外部APIコールの完了をイベントとして記録
span.AddEvent("External API call completed", trace.WithAttributes(
attribute.Int("api.status_code", 200),
))
// 処理時間を計測
duration := time.Since(startTime).Seconds()
// ヒストグラムメトリクスに記録
if histogram != nil {
histogram.Record(ctx, duration, metric.WithAttributes(
attribute.String("api.endpoint", "external_api"),
attribute.Int("api.status_code", 200),
))
log.Printf("Recorded API call duration: %.3fs", duration)
}
// レスポンスを返す
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]interface{}{
"message": "External API call completed successfully",
"duration_ms": apiLatency.Milliseconds(),
"status": "success",
})
w.Write(data)
}
// 略
func main() {
// 略
histogram, err = meter.Float64Histogram(
"task.duration",
metric.WithDescription("The duration of task execution."),
metric.WithUnit("s"),
)
if err != nil {
panic(err)
}
// 略
r.Get("/external-api", callExternalAPI)
}

観測可能(非同期)カウンターの使用
var (
memoryObservable metric.Float64ObservableCounter
currentMemoryUsage float64
)
// 中略
// エンドポイントの設定は不要だが、確認のために作成してみた
func getMemoryMetrics(w http.ResponseWriter, r *http.Request) {
_, span := tracer.Start(r.Context(), "getMemoryMetrics")
defer span.End()
// 現在のメモリ使用量を返す(ObservableCounterによって自動的に収集されている値)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]interface{}{
"current_memory_bytes": currentMemoryUsage,
"unit": "bytes",
"message": "Current memory usage tracked by Observable Counter",
})
w.Write(data)
}
// 中略
func main() {
// Float64ObservableCounterを作成
memoryObservable, err = meter.Float64ObservableCounter(
"memory.usage",
metric.WithDescription("Current memory usage in bytes"),
metric.WithUnit("By"),
metric.WithFloat64Callback(func(ctx context.Context, o metric.Float64Observer) error {
// デモンストレーション目的でランダムな値を生成
// 実際のアプリケーションでは、実際のメモリ使用量を取得するように置き換えてください
// 100MB〜500MBの間でランダムに増加する値をシミュレート
currentMemoryUsage = float64(100*1024*1024) + float64(rand.Intn(400*1024*1024))
o.Observe(currentMemoryUsage, metric.WithAttributes(
attribute.String("memory.type", "heap"),
))
log.Printf("Observable Counter reported memory usage: %.2f MB", currentMemoryUsage/(1024*1024))
return nil
}),
)
if err != nil {
log.Fatalf("failed to create memory observable counter: %v", err)
}
log.Printf("Memory observable counter created successfully")
r.Get("/metrics/memory", getMemoryMetrics)
}
}

観測可能(非同期)UpDownカウンターの使用
var (
// 中略
currentMemoryUsage float64
connectionObservable metric.Int64ObservableUpDownCounter
activeConnections int64
activeConnectionsMutex sync.Mutex
)
// 中略
func getConnectionMetrics(w http.ResponseWriter, r *http.Request) {
_, span := tracer.Start(r.Context(), "getConnectionMetrics")
defer span.End()
// 現在のアクティブコネクション数を返す
activeConnectionsMutex.Lock()
connections := activeConnections
activeConnectionsMutex.Unlock()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]interface{}{
"active_connections": connections,
"unit": "connections",
"message": "Active connections tracked by Observable UpDownCounter",
})
w.Write(data)
}
func simulateConnect(w http.ResponseWriter, r *http.Request) {
_, span := tracer.Start(r.Context(), "simulateConnect")
defer span.End()
// コネクションを増やす
activeConnectionsMutex.Lock()
activeConnections++
connections := activeConnections
activeConnectionsMutex.Unlock()
log.Printf("Connection opened, total: %d", connections)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]interface{}{
"action": "connect",
"active_connections": connections,
"message": "Connection opened",
})
w.Write(data)
}
func simulateDisconnect(w http.ResponseWriter, r *http.Request) {
_, span := tracer.Start(r.Context(), "simulateDisconnect")
defer span.End()
// コネクションを減らす
activeConnectionsMutex.Lock()
if activeConnections > 0 {
activeConnections--
}
connections := activeConnections
activeConnectionsMutex.Unlock()
log.Printf("Connection closed, total: %d", connections)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]interface{}{
"action": "disconnect",
"active_connections": connections,
"message": "Connection closed",
})
w.Write(data)
}
func main() {
// 中略
// Int64ObservableUpDownCounterを作成
connectionObservable, err = meter.Int64ObservableUpDownCounter(
"active.connections",
metric.WithDescription("Number of active connections"),
metric.WithUnit("{connection}"),
metric.WithInt64Callback(func(ctx context.Context, o metric.Int64Observer) error {
activeConnectionsMutex.Lock()
connections := activeConnections
activeConnectionsMutex.Unlock()
o.Observe(connections, metric.WithAttributes(
attribute.String("connection.type", "http"),
))
log.Printf("Observable UpDownCounter reported active connections: %d", connections)
return nil
}),
)
if err != nil {
log.Fatalf("failed to create connection observable updown counter: %v", err)
}
log.Printf("Connection observable updown counter created successfully")
// 中略
// バックグラウンドでコネクション数をシミュレート
go func() {
for {
time.Sleep(time.Duration(2+rand.Intn(3)) * time.Second)
activeConnectionsMutex.Lock()
// -5〜+10の間でランダムに変動
change := int64(rand.Intn(16) - 5)
activeConnections += change
if activeConnections < 0 {
activeConnections = 0
}
if activeConnections > 100 {
activeConnections = 100
}
activeConnectionsMutex.Unlock()
log.Printf("Simulated connection change: %+d, total: %d", change, activeConnections)
}
}()
// 中略
r.Get("/metrics/connections", getConnectionMetrics)
r.Post("/connection/open", simulateConnect)
r.Post("/connection/close", simulateDisconnect)
}
非同期なので、「観測可能(非同期)カウンター」もメトリクスに表示されている。

観測可能(非同期)ゲージの使用
var (
// 中略
heapObservable metric.Int64ObservableGauge
currentHeapUsage int64
heapUsageMutex sync.Mutex
)
func getHeapMetrics(w http.ResponseWriter, r *http.Request) {
_, span := tracer.Start(r.Context(), "getHeapMetrics")
defer span.End()
// 現在のヒープメモリ使用量を返す
heapUsageMutex.Lock()
heapUsage := currentHeapUsage
heapUsageMutex.Unlock()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]interface{}{
"heap_bytes": heapUsage,
"heap_mb": float64(heapUsage) / (1024 * 1024),
"unit": "bytes",
"message": "Heap memory usage tracked by Observable Gauge",
})
w.Write(data)
}
func allocateMemory(w http.ResponseWriter, r *http.Request) {
_, span := tracer.Start(r.Context(), "allocateMemory")
defer span.End()
// メモリを割り当てる(10MB〜50MBのランダムな量)
allocation := int64(10*1024*1024) + int64(rand.Intn(40*1024*1024))
heapUsageMutex.Lock()
currentHeapUsage += allocation
if currentHeapUsage > 500*1024*1024 {
currentHeapUsage = 500 * 1024 * 1024
}
heapUsage := currentHeapUsage
heapUsageMutex.Unlock()
log.Printf("Memory allocated: %.2f MB, total heap: %.2f MB",
float64(allocation)/(1024*1024), float64(heapUsage)/(1024*1024))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]interface{}{
"action": "allocate",
"allocated_mb": float64(allocation) / (1024 * 1024),
"total_heap_mb": float64(heapUsage) / (1024 * 1024),
"message": "Memory allocated",
})
w.Write(data)
}
func freeMemory(w http.ResponseWriter, r *http.Request) {
_, span := tracer.Start(r.Context(), "freeMemory")
defer span.End()
// メモリを解放する(10MB〜50MBのランダムな量)
deallocation := int64(10*1024*1024) + int64(rand.Intn(40*1024*1024))
heapUsageMutex.Lock()
currentHeapUsage -= deallocation
if currentHeapUsage < 10*1024*1024 {
currentHeapUsage = 10 * 1024 * 1024
}
heapUsage := currentHeapUsage
heapUsageMutex.Unlock()
log.Printf("Memory freed: %.2f MB, total heap: %.2f MB",
float64(deallocation)/(1024*1024), float64(heapUsage)/(1024*1024))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(map[string]interface{}{
"action": "free",
"freed_mb": float64(deallocation) / (1024 * 1024),
"total_heap_mb": float64(heapUsage) / (1024 * 1024),
"message": "Memory freed",
})
w.Write(data)
}
func main() {
// Int64ObservableGaugeを作成
heapObservable, err = meter.Int64ObservableGauge(
"memory.heap",
metric.WithDescription("Heap memory usage in bytes"),
metric.WithUnit("By"),
metric.WithInt64Callback(func(ctx context.Context, o metric.Int64Observer) error {
heapUsageMutex.Lock()
heapUsage := currentHeapUsage
heapUsageMutex.Unlock()
o.Observe(heapUsage, metric.WithAttributes(
attribute.String("memory.state", "used"),
))
log.Printf("Observable Gauge reported heap usage: %.2f MB", float64(heapUsage)/(1024*1024))
return nil
}),
)
if err != nil {
log.Fatalf("failed to create heap observable gauge: %v", err)
}
log.Printf("Heap observable gauge created successfully")
// ヒープメモリ使用量の初期値を設定(50MB〜200MBの範囲)
currentHeapUsage = int64(50*1024*1024) + int64(rand.Intn(150*1024*1024))
// バックグラウンドでヒープメモリ使用量をシミュレート
go func() {
for {
time.Sleep(time.Duration(1+rand.Intn(3)) * time.Second)
heapUsageMutex.Lock()
// メモリ使用量を変動させる(-10MB〜+20MBの範囲)
change := int64(rand.Intn(30*1024*1024) - 10*1024*1024)
currentHeapUsage += change
// 最小値と最大値の制限(10MB〜500MB)
if currentHeapUsage < 10*1024*1024 {
currentHeapUsage = 10 * 1024 * 1024
}
if currentHeapUsage > 500*1024*1024 {
currentHeapUsage = 500 * 1024 * 1024
}
heapUsageMutex.Unlock()
log.Printf("Simulated heap memory change: %+.2f MB, total: %.2f MB",
float64(change)/(1024*1024), float64(currentHeapUsage)/(1024*1024))
}
}()
r.Get("/metrics/heap", getHeapMetrics)
r.Post("/memory/allocate", allocateMemory)
r.Post("/memory/free", freeMemory)
}

属性の追加
WithAttributeSet
または WIthAttributes
を使用して属性を追加できる。
ほかの箇所で確認済みのためスキップ。

ビューの登録
task.duration
を request.latency
に置き換える実装。
func newMeterProvider(metricExporter sdkmetric.Exporter, res *resource.Resource) *sdkmetric.MeterProvider {
// task.duration ヒストグラムの名前を request.latency に変更するビュー
view := sdkmetric.NewView(sdkmetric.Instrument{
Name: "task.duration",
Scope: instrumentation.Scope{
Name: "go-app",
},
}, sdkmetric.Stream{Name: "request.latency"})
return sdkmetric.NewMeterProvider(
sdkmetric.WithView(view),
sdkmetric.WithResource(res),
sdkmetric.WithReader(
sdkmetric.NewPeriodicReader(metricExporter,
// デモ目的で3sに設定(デフォルトは1m)
sdkmetric.WithInterval(3*time.Second)),
),
)
}
置き換え前(task.duration
)
置き換え後(request.latency
)

ログ
Direct-to-Collector
ログ SDK
ログブリッジ
func newOTelLogExporter(ctx context.Context) (sdklog.Exporter, error) {
// Get OTLP endpoint from environment variable
endpoint := os.Getenv("OTLP_ENDPOINT")
if endpoint == "" {
return nil, fmt.Errorf("OTLP_ENDPOINT environment variable is required")
}
log.Printf("Initializing OpenTelemetry Log with OTLP endpoint: %s", endpoint)
// Create OTLP log exporter
exporter, err := otlploggrpc.New(ctx,
otlploggrpc.WithEndpoint(endpoint),
otlploggrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("failed to create metric exporter: %w", err)
}
return exporter, nil
}
// 中略
func newLoggerProvider(exp sdklog.Exporter, res *resource.Resource) *sdklog.LoggerProvider {
processor := sdklog.NewBatchProcessor(exp)
return sdklog.NewLoggerProvider(
sdklog.WithResource(res),
sdklog.WithProcessor(processor),
)
}
func main() {
// 中略
// ログプロバイダーを初期化
lp := newLoggerProvider(logExp, res)
defer func() {
log.Printf("Shutting down logger provider...")
if err := lp.Shutdown(ctx); err != nil {
log.Fatalf("failed to shutdown logger provider: %v", err)
}
log.Printf("Logger provider shutdown complete")
}()
// slogとOpenTelemetryのブリッジを設定
logger := otelslog.NewLogger("go-app", otelslog.WithLoggerProvider(lp))
slog.SetDefault(logger)
}
ログが出力されることを確認。

サンプリング
スパンのサンプリングを調整する方法。
ドキュメントに記載されていたのは以下。
- AlwaysSample
- NeverSample
- TraceIDRatioBased
- ParentBased