
## はじめに

dd-trace-go は Datadog の APM 向けに Trace を計装・送信するためのライブラリですが、Datadog のネイティブプロトコルだけでなく、[OTLP Trace Export](https://docs.datadoghq.com/opentelemetry/instrument/dd_sdks/otlp_trace_export) にも対応しています。

これまで dd-trace-go で計装したアプリケーションは、基本的に Datadog Agent 経由で Datadog に Trace を送信することが前提となっていました。しかし OTLP Trace Export に対応したことで、dd-trace-go で計装しているアプリケーションであっても、DDOT / OpenTelemetry Collector を経由した Tail Based Sampling を行えるようになったり、Datadog 以外の Observability Backend へテレメトリデータを送ることができる等様々な選択肢が出てきました。

本記事では、dd-trace-go で計装したアプリケーションの Trace を OTLP 形式で送信する方法について紹介します。

## 前提

本記事では以下のバージョンのパッケージの使用を前提としています。

- Go: 1.25.5
- dd-trace-go: v2.9.1

また、本記事で扱うサンプルコードは以下のリポジトリで公開しています。

[github.com/ucpr/workspace2026](https://github.com/ucpr/workspace2026/tree/main/dd-trace-go-with-otlp)

> [!WARNING]
> dd-trace-go の OTLP Trace Export は 2026/07 現在 preview の機能です。将来のバージョンで挙動が変更される可能性があることに注意してください。

## dd-trace-go の OTLP Trace Export について

dd-trace-go はデフォルトでは Datadog のネイティブプロトコルで Trace を送信しますが、環境変数 `OTEL_TRACES_EXPORTER=otlp` と `DD_TRACE_OTEL_ENABLED=true` を設定することで、Trace を OTLP 形式で送信するモードに切り替えることができます。

このモードでは、tracer が以下の環境変数を読み取って動作します。

| 環境変数 | 役割 |
| --- | --- |
| `OTEL_TRACES_EXPORTER=otlp` | OpenTelemetry 仕様標準の環境変数。Trace の出力先プロトコルとして OTLP を指定する |
| `DD_TRACE_OTEL_ENABLED=true` | Datadog SDK 内で OpenTelemetry API / 機能の互換レイヤーを有効化する |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | 送信先のエンドポイント |
| `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | 付与するヘッダー（認証情報など）。`key=value` をカンマ区切りで指定する |

これらは OpenTelemetry 由来の環境変数に沿った命名であり、既存の OpenTelemetry の知識を活かしやすくなっています。ただし、dd-trace-go が対応している変数や挙動が OpenTelemetry SDK と完全に同一とは限らない点には注意が必要です。

送信は OTLP/HTTP の protobuf 形式（`Content-Type: application/x-protobuf`）で行われます。

また、Trace Context の伝搬方式は `DD_TRACE_PROPAGATION_STYLE` で制御します。本記事では W3C Trace Context を利用するため、`DD_TRACE_PROPAGATION_STYLE=tracecontext` を設定します。これにより、`traceparent` / `tracestate` ヘッダーを通じてサービス間で Trace Context が伝搬されます。

## 実際に送信する

ここでは例として、以下のようなコールチェーンを持つアプリケーションを用意し、Trace を OTLP 形式で送信してみます。送信先には、OTLP を直接受信できる [Jaeger](https://www.jaegertracing.io/) を利用します。


![export_traces_from_dd-trace-go_00.png](https://ucprdev-image-proxy.ucpr.workers.dev/images/articles/export_traces_from_dd-trace-go/export_traces_from_dd-trace-go_00.png)

各サービスの計装には、以下の dd-trace-go パッケージを利用し、OpenTelemetry SDK を追加せずに OTLP 形式で送信できることを検証します。

- `contrib/net/http/v2`: HTTP サーバーの計装（inbound）
- `contrib/google.golang.org/grpc/v2`: gRPC サーバー・クライアントの計装（Interceptor）
- `contrib/database/sql/v2`: `database/sql` 経由の SQL クエリの計装

### tracer の初期化

tracer の初期化は、通常の dd-trace-go の利用時と変わりません。Export モードやエンドポイント、伝搬方式はすべて環境変数で制御されるため、コード側では Service 名を指定するだけです。

```go:internal/tracing/tracing.go
package tracing

import (
	"log/slog"

	"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
)

func Start(service string) func() {
	// Export モード (OTLP か Datadog agent か)、エンドポイント、env、version、
	// 伝搬方式はすべて環境変数で制御されるため、ここでは何も設定しない。
	tracer.Start(
		tracer.WithService(service),
	)
	slog.Info("tracer started", "service", service)
	return func() {
		tracer.Stop()
		slog.Info("tracer stopped", "service", service)
	}
}
```

計装済みのアプリケーションに OTLP Trace Export を導入する際に、コードの変更範囲を小さくできる点は大きな利点です。

### 環境変数の設定

前述の環境変数を各サービスに設定します。ここでは Docker Compose で設定する例を示します。

```yaml:compose.yaml
x-otel-env: &otel-env
  OTEL_TRACES_EXPORTER: otlp
  DD_TRACE_OTEL_ENABLED: "true"
  OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://jaeger:4318/v1/traces
  # gRPC 間で W3C traceparent/tracestate を伝搬する
  DD_TRACE_PROPAGATION_STYLE: tracecontext
  DD_ENV: dev
  DD_VERSION: "0.1.0"
```

### リクエストを送り Trace を確認する

`docker compose up` でアプリケーションと Jaeger を起動し、実際にリクエストを送って Trace を生成してみます。

```sh
$ docker compose up --build -d
$ curl -sS -XPOST localhost:8080/orders -d '{"item":"book","quantity":3}'
$ curl -sS localhost:8080/orders
$ curl -sS localhost:8080/orders/1
```

Jaeger を開き、Service に `http-gateway` を選択して Trace を確認すると、以下のように 3 つのサービスと SQL クエリまでを含む Span が、W3C Trace Context によって 1 本の Trace として繋がっていることが確認できました。

![export_traces_from_dd-trace-go_01.png](https://ucprdev-image-proxy.ucpr.workers.dev/images/articles/export_traces_from_dd-trace-go/export_traces_from_dd-trace-go_01.png)

## Head Based Sampling の挙動

OTLP Trace Export に切り替えた際に気になるのが、Sampling がこれまでと同じように動作するかという点です。ここでは dd-trace-go の Head Based Sampling が OTLP Trace Export でもそのまま動作することを確認してみます。

dd-trace-go は Head Sampler として動作します。すなわち、Sampling の判定は Trace の入り口となるサービス（head）で一度だけ行われ、その判定結果が W3C `traceparent` の sampled フラグを通じて下流のサービスへ伝搬されます。下流のサービスは伝搬されてきた親の判定を尊重する（parent-based）ため、Trace 全体で一貫した Sampling が行われます。

Trace ごとの判定は、以下のような順序で行われます。

1. parent-based の判定で、親がすでに伝搬されている場合はそれを尊重する
2. `DD_TRACE_SAMPLING_RULES`、次に `DD_TRACE_SAMPLE_RATE` を評価する
3. いずれにも該当しない場合は fallback sampler を利用する

OTLP Trace Export では、明示的な Sampling 設定に該当しない場合の [fallback sampler として `parentbased_always_on` が使われます](https://github.com/DataDog/dd-trace-go/pull/4567)。親の判定がすでに伝搬されていればそれを尊重し、親の判定がなければ Trace を rate 1.0 で保持します。

Head Based Sampling のレートは、入り口となるサービスの環境変数で制御します。

```yaml
DD_TRACE_SAMPLE_RATE: "0.1"                                   # 10% を保持する
DD_TRACE_SAMPLING_RULES: '[{"service":"http-gateway","sample_rate":0.5}]'
```

この例では、`http-gateway` にマッチする Trace は `DD_TRACE_SAMPLING_RULES` により 50% 保持され、ルールにマッチしないサービスは `DD_TRACE_SAMPLE_RATE` により 10% 保持されます。

### 親の判定が尊重されることを確認する

parent-based の挙動を確認するため、入り口の `http-gateway` を KEEP（1.0）に、下流の `grpc-service1` / `grpc-service2` を DROP（0.0）に設定してみます。

```yaml:compose.sampling.yaml
services:
  http-gateway:
    environment:
      DD_TRACE_SAMPLE_RATE: "1.0"
  grpc-service1:
    environment:
      DD_TRACE_SAMPLE_RATE: "0.0"
  grpc-service2:
    environment:
      DD_TRACE_SAMPLE_RATE: "0.0"
```

この overlay を適用して起動し、リクエストを送ってみます。

```sh
$ docker compose -f compose.yaml -f compose.sampling.yaml up -d
$ curl -sS -XPOST localhost:8080/orders -d '{"item":"book","quantity":3}'
```

下流のサービスは自身のレートが `0.0`（DROP）に設定されているにもかかわらず、Jaeger には 3 サービスすべてを含む Trace が保持されていることが確認できました。これは、下流のサービスが入り口の `http-gateway` の KEEP の判定を尊重しているためです。

逆に、値を反転させて入り口を DROP（0.0）、下流を KEEP（1.0）に設定すると、下流のレートが `1.0` であっても Trace は全体として Drop され、Jaeger には表示されなくなります。このとき、業務ロジック（DB への書き込み）自体は実行されています。これは、各サービスではなく入り口の判定が Trace 全体の Sampling を制御していることを示しています。

以上の検証結果をまとめると、以下のようになります。

|  | gateway | service1/2 | Jaeger の結果 | 意味 |
| --- | --- | --- | --- | --- |
| A | 1.0 | 0.0 | 3 サービスを含む Trace が保持される | 親の KEEP が尊重される |
| B | 0.0 | 1.0 | 新規 Trace は保持されない (DB 書き込みは実行済み) | 親の DROP が尊重される |

OTLP Trace Export においても、dd-trace-go の Head Based Sampling (parent-based) がこれまでと同様に動作することが確認できました。

## おわりに

本記事では、dd-trace-go v2 で計装したアプリケーションの Trace を OTLP 形式で送信する方法について紹介しました。あわせて、OTLP Trace Export でも Head Based Sampling がそのまま動作することを確認しました。

dd-trace-go が OTLP Trace Export に対応したことで、既存の dd-trace-go の計装をほとんど変更することなく、DDOT / OpenTelemetry Collector を経由した Tail Based Sampling や、Datadog 以外の Observability Backend への送信といった選択肢を取れるようになりました。既存の計装資産を活かしつつテレメトリパイプラインの柔軟性を高められる点は、非常に良さそうだと感じました。

本記事において、異なっている説明や表現がありましたらご連絡ください。

## 参考

- [dd-trace-go - github.com](https://github.com/DataDog/dd-trace-go)
- [Export Traces from Datadog SDKs in OTLP Format - docs.datadoghq.com](https://docs.datadoghq.com/opentelemetry/instrument/dd_sdks/otlp_trace_export/?tab=go)
- [OpenTelemetry Protocol Exporter - opentelemetry.io](https://opentelemetry.io/docs/specs/otel/protocol/exporter/)
