小さな環境変数の旅路

OpenTelemetry の Spring Boot スターターは、バージョン 2.26.0 から宣言的設定をサポートするようになりました。 これは Java エージェントが 2025 年後半に導入したのと同じ YAML スキーマを application.yaml の中に埋め込むものです。 この投稿では、1 つの環境変数 OTEL_SERVICE_NAME=petclinic がこの新しい仕組みの中で何をするのかを追跡し、どこに継ぎ目があるのかを明らかにします。

長年にわたり、環境変数(とその 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 をバンドルしています。 変わるのは、誰がワイヤリングを書くかです。

この投稿の残りの部分では、OTEL_SERVICE_NAME が SDK に到達するまでの 3 つのステージを追跡します。

ステージ 1:Spring のプロパティスタックへの到着

Spring のプロパティローダーは、アプリケーションが参照するすべてのソース(application.yaml、すべてのアクティブなプロファイルのオーバーレイ、JVM の -D フラグ、--key=value コマンドライン引数、環境変数)を 1 つのアドレス可能なプロパティユニバースに積み上げます。 OTEL_SERVICE_NAMESERVER_PORTSPRING_PROFILES_ACTIVE と並んでそのスタックに存在します。 Spring はこれらのうちどれが OpenTelemetry に属するかを知りません。 それはスターターの仕事であり、処理の最後の段階で行われます。

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 行の深さで名前を指定して探しにいくからです。 上の図のダイヤモンドが、それらが存在する場所です。

ステージ 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["&dollar;{VAR:default}<br/>&dollar;{VAR}"] --> S2[Spring リゾルバー]
    S2 --> S3["読み取り元:<br/>環境変数、システムプロパティ、引数、<br/>プロファイル、すべての yaml"]
  end
  subgraph AGENT["otel-config.yaml — エージェント"]
    direction TB
    A1["&dollar;{VAR:-default}<br/>&dollar;{VAR}<br/>&dollar;{env:VAR:-default}<br/>&dollar;{sys:property:-default}"] --> A2[SDK リゾルバー]
    A2 --> A3["読み取り元:<br/>環境変数 + システムプロパティ"]
  end
  STARTER --> MODEL[同じ SDK モデル]
  AGENT --> MODEL

これが、Spring ネイティブの設定トリック(プロファイル、コマンドラインの --key=value@Value スタイルの外部化、外部設定サーバーまで)がすべて OTel 設定に対して透過的に機能する理由でもあります。 スターターはそれらを一切実装していません。 Spring のリゾルバーがそれを行い、スターターはプロパティを読み取るだけです。

最大の実用的な帰結として、スターターでは同じ正規キーパスに名前が付けられた環境変数は、追加のワイヤリングなしに YAML のリーフを自動的にオーバーライドします。 エージェントのスタンドアロン YAML ではそれができません。 そこでは、環境変数のオーバーライドは事前に YAML に ${VAR} プレースホルダーとして組み込んでおく必要があり、そうしなければ何も起こりません。

到達点: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/>+ &dollar;{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 を使用してください。
  • プログラムによるカスタマイズの形が変わります。 AutoConfigurationCustomizerProviderDeclarativeConfigurationCustomizerProvider に置き換えられます。 SDK コンポーネントは ComponentProvider API 経由でプラグインします。 エージェントのエクステンション API ドキュメントがスターターにもそのまま適用されます。

実際のアプリケーションをこの設定に移行して問題が発生した場合は、イシューを起票してください。 コードに関しては opentelemetry-java-instrumentation、ドキュメントに関しては opentelemetry.io です。