Skip to content

HDDS-17350. Build the OpenTelemetry SDK lazily in application-aware tracing mode - #11723

Open
yandrey321 wants to merge 3 commits into
apache:masterfrom
yandrey321:HDDS-17350
Open

yandrey321 wants to merge 3 commits into
apache:masterfrom
yandrey321:HDDS-17350

Conversation

@yandrey321

@yandrey321 yandrey321 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Problem. Every Ozone client calls TracingUtil.initTracing(). With tracing disabled but
ozone.tracing.client.application-aware=true (both defaults, HDDS-13803), and no GlobalOpenTelemetry registered by
the application, initialize() builds Ozone's OpenTelemetry SDK right away:

  • the OTLP exporter;
  • a BatchSpanProcessor with its export thread;
  • the SdkTracerProvider, registered as GlobalOpenTelemetry.

In this mode Ozone only creates a span under a valid parent span, which most client processes never have, so all
of this usually goes unused. It costs about 80 ms and several hundred classes in every cold client: YARN containers,
CLI commands, MapReduce tasks.

Implementation idea. In application-aware mode without an application GlobalOpenTelemetry, initialize() now
only records that an SDK may be needed. The first span created under a valid parent completes the initialization,
once, under the class lock:

  • it adopts an application GlobalOpenTelemetry registered since then, if there is one;
  • otherwise it builds Ozone's SDK, exactly as before.

Spans are created under a parent in three places: buildSpan (a valid current span), importAndCreateSpan (a
propagated parent) and createActivatedSpanFromW3cHttpHeaders (a valid remote parent). Each calls initPendingSdk()
before using the tracer. hasUsableTracer() counts a pending SDK as usable, so isTracingActive() and
createProxy() behave as before.

Key decisions.

  • Only application-aware mode changes. With ozone.tracing.enabled=true the SDK is still built at init, because
    Ozone can start root spans at once.
  • The pending state is cleared even if building the SDK fails. The failure is logged once, as at init before.
  • Adopting a global registered later is new. An application that registers its GlobalOpenTelemetry after
    creating the Ozone client now gets its spans into its own SDK. Before, Ozone had already installed its own SDK as
    the global, so the application's GlobalOpenTelemetry.set() failed.

Side effects.

  • In application-aware mode, Ozone registers its SDK as GlobalOpenTelemetry at the first traced call instead of at
    client creation. A co-resident library that reads GlobalOpenTelemetry before then sees the default no-op instance,
    as in a process with tracing turned off.
  • The export thread only exists after the first traced call.

Generated-by: Claude Code (Claude Opus 5.5)

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/HDDS-17350

How was this patch tested?

Unit + integration tests: https://github.com/yandrey321/ozone/actions/runs/40352078370

  • New tests in TestTracingInitModes, all with no application global unless stated:

    • application-aware mode without a parent span builds nothing and registers no global, while tracing still
      counts as active and no root span is created;
    • the first span under a valid parent builds Ozone's SDK and continues the parent's trace;
    • a GlobalOpenTelemetry that the application registers after init is adopted; a recording exporter shows that
      the child span went to the application's SDK;
    • with tracing enabled, the SDK is still built at init.

    A mutation check (always building the SDK at init) fails three of the four tests.

  • Existing tests: all TestTracing* and TestSpanSampling tests in hdds-common (43), and
    TestFailoverWithSCMHA, which wraps SCM clients with TracingUtil.createProxy.

  • Measurement, on JDK 17 (17.0.20.1), same build. The change has no switch back to the eager behaviour, so the
    eager arm enables tracing, which builds the same SDK at init:

    • TracingUtil.initTracing() in a fresh JVM, 9 runs: 98 ms and 772 extra classes when the SDK is built at init →
      16 ms and 149 classes in application-aware mode now (−82 ms). The rest is the tracing configuration and the
      OpenTelemetry API.

    • Cold ofs:// client on the secure ozonesecure compose cluster (create the client, getFileStatus, read 64 KB,
      close), 10 interleaved rounds: the default application-aware mode takes 1,244 ms, against 1,239 ms with
      ozone.tracing.client.application-aware=false (tracing off). The 5 ms difference is within noise, so the default
      no longer pays for tracing.

      JVM change
      localizer: localization time (main band of 63 localizations at about 1.6 s) −78 ms median, −81 ms mean (standard error about 0.015 s)
      AppMaster start-up −54 ms
      map start-up −38 ms median, −53 ms mean

…racing mode

Generated-by: Claude Code (Claude Opus 5.5)

@jojochuang jojochuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One compatibility finding in deferred tracing initialization, verified against the PR source and OpenTelemetry 1.67.0 implementation. Tests were not run.

Generated-by: Codex (GPT-6)

}
try {
if (GlobalOpenTelemetry.isSet() && isRealGlobal(GlobalOpenTelemetry.get())) {
tracer = GlobalOpenTelemetry.get().getTracer(GLOBAL_TRACER_NAME);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Handle the default global installed during deferred initialization

After deferred initialization, another library can call GlobalOpenTelemetry.get() before Ozone's first traced operation. OpenTelemetry 1.67.0 installs a wrapper around its no-op implementation, which passes isRealGlobal() because the wrapper differs from OpenTelemetry.noop(). This branch adopts the no-op tracer and permanently clears pending initialization, silently losing subsequent Ozone child spans even with valid parents. Previously, Ozone had already installed its functioning SDK before that read. Handle this wrapped default global and add coverage for initTracing(), GlobalOpenTelemetry.get(), then a valid parent span.

Reference: OpenTelemetry 1.67.0 GlobalOpenTelemetry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. In 1.67.0, GlobalOpenTelemetry.get() with nothing registered installs OpenTelemetry.noop() wrapped in ObfuscatedOpenTelemetry, which passed the identity check. isRealGlobal() now also rejects a global whose tracer provider is the no-op one. Since set() can't replace an existing global, initOzoneSdk() now registers Ozone's SDK only when none is set, and otherwise uses it unregistered. The same misdetection affected the eager path (tracing enabled after an early GlobalOpenTelemetry.get()), which this fixes too. New tests cover initTracing() → GlobalOpenTelemetry.get() → valid parent, asserting that Ozone creates its own child span (a no-op tracer would return the parent), and the enabled case.

@adoroszlai
adoroszlai marked this pull request as draft October 24, 2026 08:45
@adoroszlai

Copy link
Copy Markdown
Contributor

Thanks @yandrey321 for the patch. Created HDDS-17426 to mitigate in acceptance tests until this improvement lands.

@yandrey321
yandrey321 marked this pull request as ready for review October 26, 2026 15:43
@yandrey321
yandrey321 requested a review from jojochuang October 26, 2026 15:43
Co-authored-by: Codex (GPT-6)
Change-Id: I54bf5164fa3378f56160a48f3f66ddf3ee41ba73
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants