Closed26

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

柾樹柾樹

概要

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

https://opentelemetry.io/ja/docs/languages/go/

柾樹柾樹

今回のために作成した、サンプルコード。
動作確認の結果は、otel-tui で行う。

https://github.com/Msksgm/otel-go-instrumentation-doc-with-nginx

なお、以下の観点に注意

  • go だけだと面白くないので、nginx も作成して propagation でトレースを繋いでいる
  • 実行時間が短すぎて、otel-tui で表示されるスパンが nginx -> go-app になっているが、本当は go ∈ nginx の状態
柾樹柾樹

2025/08/28 時点で対象ページを一通り読み終わり、手を動かして実装したのでクローズ。
別ページを動かしたくなったら、再度オープンするかも。
とりあえずは、別言語を実践予定。

柾樹柾樹
柾樹柾樹

トレーサーの取得

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#getting-a-tracer

  • 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 だとネームスペース、ポッド名などが含まれることがある。

https://opentelemetry.io/ja/docs/concepts/resources/

利用するときには、下のように書く。

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

柾樹柾樹

スパンの作成

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#creating-spans

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引数を使用していないため、デフォルト設定でスパンが作成
  されています。

柾樹柾樹

現在のスパンを取得

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#get-current-span

span を取得するやつ。
span を引数に渡すのではなく、ctx を引き回せば良いので、Go っぽい書き方 & 後付けで実装しやすそう。

// このコンテキストは、抽出予定のアクティブなスパンを含む必要があります
ctx := context.TODO()
span := trace.SpanFromContext(ctx)

// 現在のスパンで何かを行い、オプションで終了したい場合は`span.End()`を呼び出します

柾樹柾樹

ネストしたスパンの作成

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#create-nested-spans

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")
}

柾樹柾樹

スパン属性

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#span-attributes

スパン属性の追加について、いかのどちらのパターンでも作成可能。

  • スパン生成時
  • スパン生成後

属性のキーの構造体を作成して、属性を扱うことも可能。

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)
}

柾樹柾樹

イベント

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#events

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)
}

柾樹柾樹

スパンステータスの設定

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#set-span-status

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 と合わせて使う。

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#record-errors

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 を利用するときは、両方使う

柾樹柾樹

プロパゲーターとコンテキスト

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#record-errors

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))
}

柾樹柾樹

まとめて実装したので省略。

メトリクスの初期化

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#initialize-metrics

メーターの取得

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#acquiring-a-meter

同期および非同期計装

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#synchronous-and-asynchronous-instruments

カウンターの使用

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#using-counters

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カウンターの使用

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#using-updown-counters

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)


}

柾樹柾樹

ゲージの使用

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#using-gauges


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)

}

柾樹柾樹

ヒストグラムの使用

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#using-histograms

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)


}

柾樹柾樹

観測可能(非同期)カウンターの使用

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#using-observable-async-counters

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カウンターの使用

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#using-observable-async-updown-counters

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)
}

非同期なので、「観測可能(非同期)カウンター」もメトリクスに表示されている。

柾樹柾樹

観測可能(非同期)ゲージの使用

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#using-observable-async-gauges

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)

}
柾樹柾樹

ビューの登録

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#registering-views

task.durationrequest.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

柾樹柾樹

ログ

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#logs

Direct-to-Collector

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#direct-to-collector

ログ SDK

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#logs-sdk

ログブリッジ

https://opentelemetry.io/ja/docs/languages/go/instrumentation/#log-bridge


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)

}

ログが出力されることを確認。

このスクラップは1ヶ月前にクローズされました