小さな環境変数の旅路
OpenTelemetry の
Spring Boot スターターは、バージョン 2.26.0 から宣言的設定をサポートするようになりました。
これは Java エージェントが 2025 年後半に導入したのと同じ YAML スキーマを application.yaml の中に埋め込むものです。
この投稿では、1 つの環境変数 OTEL_SERVICE_NAME=petclinic がこの新しい仕組みの中で何をするのかを追跡し、どこに継ぎ目があるのかを明らかにします。
Spring Boot スターターの宣言的設定ドキュメントに直接ジャンプするか、
application.properties をインタラクティブコンバーターに貼り付けるか、
Ecosystem Explorer で Spring Boot スターターのターゲットを選択して SDK のセットアップを選んでください。
ストーリーはコーヒーを片手にどうぞ。
長年にわたり、環境変数(とその JVM -D 版)は OpenTelemetry SDK を設定する唯一の方法でした。
すべてのエクスポーター、すべてのサンプラー、すべてのキャプチャするヘッダーが、フラットな OTEL_* 変数のリストとして表現されていました。
OpenTelemetry Spring Boot スターターのバージョン 2.26.0 以降、このリストには新しい兄弟ができました。 SDK の宣言的設定スキーマは、テレメトリーパイプライン全体(すべてのプロセッサー、すべてのエクスポーター、すべてのネストされたオプション)を SDK が実際に動作するのと同じ形で記述できる YAML ツリーです。
環境変数だけでは表現できないことに対して、Spring スターターのユーザーは @Bean を書く必要がありました。
Java エージェントのユーザーは、完全なエクステンションを書き、エージェントと一緒に配布する別の jar にパッケージする必要がありましたが、これは負担が大きいことがありました。
これらの新しい変更により、スキーマは application.yaml の中の 1 つの otel: キーの下に移動します。
環境変数は引き続き機能しますが、その役割はより限定的になります。
OTEL_SERVICE_NAME はリソース属性として反映されますが、これはサービス名ディテクターが起動時にそれを読み取るためです。
また、YAML に書いた ${VAR:default} プレースホルダーは名前を指定して環境変数を取り込みます。
それ以外では、YAML が信頼できる情報源です。
YAML ファイルの中の YAML ファイル
otel:
file_format: '1.0'
resource:
attributes:
- name: service.name
value: petclinic
tracer_provider:
processors:
- batch:
exporter:
otlp_http:
endpoint: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:http://localhost:4318/v1/traces}
otel: の下のブロックは、OpenTelemetry SDK スキーマ(プロセッサーのリスト、それぞれがエクスポーターを保持し、それぞれが設定を保持する)を Spring の application.yaml の中に埋め込んだものです。
otel.file_format の存在がスイッチです。
その下にあるすべてが SDK スキーマに対してパースされます。
Spring はそれが何を意味するかを知る必要がありません。
環境変数だけでは越えられなかった壁
環境変数は組み込みの選択肢の固定リストをカバーします。
OTEL_TRACES_SAMPLER による固定のサンプラーセット、OTEL_EXPORTER_OTLP_* による標準の OTLP エクスポーター、そして通常のシグナル切り替えフラグです。
そのカタログ外のもの(カスタムのルールベースサンプラー、デバッグパイプライン上の 2 番目の OTLP エクスポーター、バゲージプロセッサー、SDK が公開するネストされたオプション)は環境変数モデルの対象外でした。
宣言的設定がツリーの残りの部分を解放します。
スターターのドキュメントページには、ほとんどのチームが初日に必要とする小さな例があります。
アクチュエーターエンドポイントをトレースから除外する方法です。
以前は、これは @Configuration クラスで処理されていました。
@Configuration
public class FilterPaths {
@Bean
public AutoConfigurationCustomizerProvider otelCustomizer() {
return p ->
p.addSamplerCustomizer(
(fallback, config) ->
RuleBasedRoutingSampler.builder(SpanKind.SERVER, fallback)
.drop(UrlAttributes.URL_PATH, "^/actuator")
.build());
}
}
現在は application.yaml の YAML ブロックです。
otel:
tracer_provider:
sampler:
parent_based:
root:
rule_based_routing:
fallback_sampler:
always_on:
span_kind: SERVER
rules:
- action: DROP
attribute: url.path
pattern: /actuator.*
どちらのバージョンも同じ Java コードを実行します。
エージェントとスターターは、どちらもすでに opentelemetry-samplers contrib jar をバンドルしています。
変わるのは、誰がワイヤリングを書くかです。
スキーマには組み合わせ可能なルールベースサンプラー(設定タイプリファレンスの composite/development.rule_based 配下)もあり、より豊富なルール文法で同じことを実現します。
属性パターンの include/exclude、複数条件のマッチ、スパン種別フィルター、親の状態、任意のネストされた組み合わせが利用可能です。
上記の例では rule_based_routing を使っています。
OTel Java がこの問題に対して長くそのサンプラーを推奨してきたことと、composite/development パスが組み合わせ可能なスキーマの安定化まで */development サフィックスを持ち続けるためです。
SDK 設定リポジトリの実際の使用例を参照してください。
この投稿の残りの部分では、OTEL_SERVICE_NAME が SDK に到達するまでの 3 つのステージを追跡します。
ステージ 1:Spring のプロパティスタックへの到着
Spring のプロパティローダーは、アプリケーションが参照するすべてのソース(application.yaml、すべてのアクティブなプロファイルのオーバーレイ、JVM の -D フラグ、--key=value コマンドライン引数、環境変数)を 1 つのアドレス可能なプロパティユニバースに積み上げます。
OTEL_SERVICE_NAME は SERVER_PORT や SPRING_PROFILES_ACTIVE と並んでそのスタックに存在します。
Spring はこれらのうちどれが OpenTelemetry に属するかを知りません。
それはスターターの仕事であり、処理の最後の段階で行われます。
Spring はすべてのプロパティを、1 つの正規化された小文字のドット区切りの名前で公開します。 同じプロパティがさまざまなソースから、それぞれ異なる書き方で提供される場合があります。
| ソース | 記述方法 |
|---|---|
| 環境変数 | OTEL_SERVICE_NAME=petclinic |
| システムプロパティ | -Dotel.service.name=petclinic |
| コマンドライン | --otel.service.name=petclinic |
application.yaml | otel.service.name: petclinic |
Spring は「小文字化し、_ を . にする」などのルールで変換します。
スターターは、ユーザーがどれを使ったかを知る必要がありません。
これは Spring 固有の強みです。
OpenTelemetry SDK 自体は環境変数を YAML パスに自動的にマッピングしません。
これはスキーマの設計中に議論されましたが、複雑すぎるとして却下されました。
そのため、エージェントのスタンドアロン YAML 内では、OTEL_SERVICE_NAME を設定してもツリー内の service.name に反映されることはありません。
スターターはこれを無料で得られます。
マッピングを行っているのは SDK ではなく Spring だからです。
flowchart LR
S["Spring がすべての<br/>プロパティを解決"] --> W["スターターが otel.* キーを走査"]
W --> SEAM{"キーがリスト<br/>インデックスの下<br/>にある?"}
SEAM -- いいえ --> OUT["マップ全体をアンフラットン化<br/>→ Jackson(1回)<br/>→ SDK モデル"]
SEAM -- はい --> RE["環境変数名を再計算<br/>環境を再読み込み"]
RE --> OUTスターターは Spring が公開するすべてのプロパティを走査し、otel.* キーを選び出し、組み立てたマップを Jackson に渡します。
これはツリー全体に対して 1 回だけ行われ、要素ごとではありません。
ダイヤモンドはこの投稿で取り上げる継ぎ目です。
リストインデックスの下にあるキーに対する追加のステップであり、次のステージで説明します。
ステージ 2:Spring が見落としかけた環境変数
ほとんどの otel.* 環境変数は軽量です。
しかし、この変数はそうではありません。
OTEL_TRACER_PROVIDER_PROCESSORS_0_BATCH_EXPORTER_OTLP_HTTP_ENDPOINT=http://collector:4318/v1/traces
この変数は通過しますが、それはスターターの内部 16 行の深さで名前を指定して探しにいくからです。 上の図のダイヤモンドが、それらが存在する場所です。
Spring にプロパティを名前で問い合わせれば、この値は問題なく返されるでしょう。
Spring の relaxed バインディングは、OTEL_..._ENDPOINT が otel....endpoint の別の綴りであること(ブラケットを含めて)をずっと理解してきました。
「設定のツリーがある」場合の通常の Spring の手法は、
@ConfigurationProperties
クラスでしょう。
設定ツリーの形をした Java POJO を宣言し、プロパティプレフィックスでアノテーションを付けると、Spring がすべてのソースをバインドしてくれます。
OTel SDK スキーマは、多くのポリモーフィックな層にまたがる数百のプロパティ(すべてのエクスポーター種別、すべてのサンプラー種別、すべてのプロセッサー種別)を持ち、まだ進化中の YAML スキーマから生成されます。
これをミラーリングする手書きの POJO ツリーは、第 2 の信頼できる情報源となり、常に第 1 のものに遅れを取ります。
理想的な解決策は、スキーマ(SDK の DC スキーマと、まだ存在しない distribution.* および instrumentation/development.* サブツリーのスキーマ)から POJO を生成することです。
同じ生成された記述は、Spring が IDE 補完に使用する JSON メタデータも駆動できます。
Java 計装チームは、これを将来の改善として opentelemetry-java-instrumentation#14083 で追跡しています。
そのため、スターターは Spring ではめったに見られないことを行います。
すべての PropertySource を直接走査し、otel. で始まるキーを収集します。
この走査では、YAML ソースの名前はブラケット付き(otel.tracer_provider.processors[0]...)で、環境変数ソースの名前はアンダースコア付き(OTEL_TRACER_..._ENDPOINT)で見えます。
Spring のリネームはプロパティを解決するときにのみ行われ、プロパティを一覧するときには行われません。
その後、Jackson が収集したキーを生成された設定モデルにバインドし、このモデルは常にライブスキーマと一致します。
EmbeddedConfigFile
の 16 行が、2 つの命名規約の間のギャップを埋めます。
[N] ブラケットを含むすべての otel.* キーに対して、スターターはプロパティ名から環境変数名を再構築し、Spring に直接問い合わせます。
ステージ 3:2 つの置換器、1 つの構文
application.yaml と SDK のスタンドアロン YAML の両方が ${...} プレースホルダーを使用します。
これらはほぼ同じことを意味しますが、完全には同じではありません。
Spring は ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318}}/v1/traces のようなチェーンされたフォールバックを喜んで解決し、外側のプレースホルダーを内側に追跡します。
これにより、シグナル固有のオーバーライドを優先し、汎用的なものにフォールバックし、最終的にリテラルにたどり着くことができます。
SDK の置換器は、再帰しない単一の正規表現パスです。
同じ式が otel-config.yaml ではパースされません。
flowchart LR
subgraph STARTER["application.yaml — スターター"]
direction TB
S1["${VAR:default}<br/>${VAR}"] --> S2[Spring リゾルバー]
S2 --> S3["読み取り元:<br/>環境変数、システムプロパティ、引数、<br/>プロファイル、すべての yaml"]
end
subgraph AGENT["otel-config.yaml — エージェント"]
direction TB
A1["${VAR:-default}<br/>${VAR}<br/>${env:VAR:-default}<br/>${sys:property:-default}"] --> A2[SDK リゾルバー]
A2 --> A3["読み取り元:<br/>環境変数 + システムプロパティ"]
end
STARTER --> MODEL[同じ SDK モデル]
AGENT --> MODELapplication.yaml の中では、Spring は ${VAR:default}(コロン 1 つ)を、自身が知るすべてのプロパティソース(環境変数、システムプロパティ、プロファイル、コマンドライン引数、外部設定サーバー)から解決します。
SDK のスタンドアロン YAML は ${VAR:-default}(コロン 2 つ、ダッシュ)を使用し、プロセスの環境変数と JVM のシステムプロパティのみから解決します。
スターターでは、SDK の置換器は実行されません。
スターターが値を読み取る時点で、Spring がすでに処理を完了しているためです。
これが、Spring ネイティブの設定トリック(プロファイル、コマンドラインの --key=value、@Value スタイルの外部化、外部設定サーバーまで)がすべて OTel 設定に対して透過的に機能する理由でもあります。
スターターはそれらを一切実装していません。
Spring のリゾルバーがそれを行い、スターターはプロパティを読み取るだけです。
最大の実用的な帰結として、スターターでは同じ正規キーパスに名前が付けられた環境変数は、追加のワイヤリングなしに YAML のリーフを自動的にオーバーライドします。
エージェントのスタンドアロン YAML ではそれができません。
そこでは、環境変数のオーバーライドは事前に YAML に ${VAR} プレースホルダーとして組み込んでおく必要があり、そうしなければ何も起こりません。
スターターでは、YAML に他の変更を加えなくても、起動時にこれが機能します。
OTEL_TRACER_PROVIDER_PROCESSORS_0_BATCH_EXPORTER_OTLP_HTTP_ENDPOINT=\
http://prod-collector:4318/v1/traces
application.yaml でそのエンドポイントに設定されていた値が置き換えられます。
エージェントでは、まず YAML に ${MY_ENDPOINT:-http://...} と書いてから、起動時に MY_ENDPOINT を設定する必要があります。
本番環境のコンテナは、スターターの方がスムーズに着地します。
到達点:SDK が実際に起動する解決済みツリー
SDK が起動する時点で、すべての otel.* 値は解決、relaxed 処理、正規化され、他のすべてと結合されて 1 つのフラットマップになっています。
スターターはそのフラットマップを SDK が期待するツリーにアンフラットン化し、Jackson に渡します。
Jackson は SDK が起動する OpenTelemetryConfigurationModel を生成します。
どの値を yaml、環境変数、プロファイルオーバーレイ、コマンドライン引数のどれが書いたかに関係なく、SDK は解決された結果だけを見ます。
flowchart TD
A[application.yaml] --> S
B[application-prod.yaml] --> S
C[環境変数] --> S
D[JVM システムプロパティ] --> S
E[コマンドライン引数] --> S
S["Spring プロパティローダー<br/>+ ${VAR} 解決"] --> F["フラットプロパティマップ<br/>otel.foo.bar[0].baz = ..."]
F --> X["スターター:EmbeddedConfigFile<br/>otel.* キーを走査、アンフラットン化"]
X --> J[Jackson → OpenTelemetryConfigurationModel]
J --> SDK[SDK ランタイム]Spring がフロントドアを所有します。
SDK は生の ${VAR}、プロファイル名、プロパティファイルを見ることはありません。
完全に解決されたツリーが、起動時に 1 回だけ渡されるだけです。
「実験的」が宣言的設定を今試すべき最大の理由
宣言的設定は、OpenTelemetry がすべての言語で収束しつつあるスキーマです。 まだ完成していません。 Spring Boot スターターのサポートが実験的とマークされているのは、まさにスキーマのどの角を締めるべきかを知るのに十分な数の実際のアプリケーションをまだ見ていないからです。
これは警告ではありません。
招待です。
スキーマがフリーズする前の今が、宣言的設定を実際の application.yaml に入れて何が壊れるかを確認する、最もレバレッジの高いタイミングです。
あなたの摩擦こそが、最終的に着地するスキーマを形作るのです。
60 秒で始める
2 つの出発点があり、どちらもすでに用意されています。
- すでに
application.propertiesをお持ちですか? ドキュメントページのインタラクティブコンバーターに貼り付けてください。 YAML が出力され、application.yamlにそのまま入れられます。 - 新規プロジェクトですか?
OpenTelemetry Ecosystem Explorer が宣言的設定の YAML をインタラクティブに生成します。
エクスポーター、サンプラー、計装を選択して結果をコピーしてください。
新しい Spring Boot スターターのターゲットモードが出力を
otel:の下にラップし、適切なdistribution.spring_starter.*キーを使用します。
注意事項
- Spring Boot 3.5 以降では依存関係管理が必要です。
Spring Boot 3.5 は、スターターが必要とするものと競合する独自の OpenTelemetry バージョンピンを同梱しています。
dependencyManagementで OTel 計装 BOM をインポートしてください(ドキュメントを参照)。 これを省略すると、起動時にNoClassDefFoundError: io/opentelemetry/common/ComponentLoaderが発生します。 - Duration はミリ秒の数値です。
5sではなく5000を使用してください。 - プログラムによるカスタマイズの形が変わります。
AutoConfigurationCustomizerProviderはDeclarativeConfigurationCustomizerProviderに置き換えられます。 SDK コンポーネントはComponentProviderAPI 経由でプラグインします。 エージェントのエクステンション API ドキュメントがスターターにもそのまま適用されます。
実際のアプリケーションをこの設定に移行して問題が発生した場合は、イシューを起票してください。 コードに関しては opentelemetry-java-instrumentation、ドキュメントに関しては opentelemetry.io です。