Skip to content

Expose OpenTelemetry metrics shutdown through MeterProvider - #12317

Draft
bm1549 wants to merge 8 commits into
masterfrom
brian.marks/otel-metrics-lifecycle
Draft

Expose OpenTelemetry metrics shutdown through MeterProvider#12317
bm1549 wants to merge 8 commits into
masterfrom
brian.marks/otel-metrics-lifecycle

Conversation

@bm1549

@bm1549 bm1549 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

Adds a Datadog extension to the OpenTelemetry MeterProvider returned by GlobalOpenTelemetry:

MeterProvider meterProvider = GlobalOpenTelemetry.get().getMeterProvider();
DatadogMeterProvider datadogMeterProvider = (DatadogMeterProvider) meterProvider;
CompletableResultCode result = datadogMeterProvider.shutdown().join(10, TimeUnit.SECONDS);

shutdown() performs a final export and stops Datadog's OpenTelemetry metrics export pipeline. Java does not expose a public forceFlush() extension.

The return type follows OpenTelemetry's CompletableResultCode pattern without using CompletableFuture. A timed join() bounds the caller's wait but does not cancel shutdown. Repeated calls observe the first shutdown result through separate result views. Completion is visible to every view before callbacks run, so a blocked callback on one result cannot hold up another caller. Callers also cannot change the stored result.

Periodic exports, the existing internal flush, and final export remain serialized on the metrics executor. A disabled or unavailable pipeline treats shutdown as a successful no-op. Cancellation, submission, export, and cleanup failures complete the result as failed.

Motivation

Short-lived Java processes need a supported way to wait for pending custom metrics before exit. Exposing shutdown from the meter provider keeps the API next to the OpenTelemetry component it controls.

Additional Notes

  • The existing void flushMetrics() bridge retains its signature and 2.5-second timeout.
  • Lifecycle submissions do not inherit the caller's active tracing context.
  • The Node.js companion PR continues to expose both forceFlush() and shutdown().
  • The system-tests and documentation companion PRs need Java-specific updates for the shutdown-only provider API.
  • Companion PRs: system-tests #7606, Node.js #10023, and documentation #39515.

Tests run:

  • ./gradlew :dd-trace-api:test --tests 'datadog.trace.api.metrics.*Test' :dd-trace-api:javadoc :dd-trace-api:spotbugsMain
  • ./gradlew :dd-trace-api:jacocoTestCoverageVerification
  • ./gradlew :dd-trace-core:test --tests datadog.trace.core.otlp.metrics.OtlpMetricsServiceTest
  • ./gradlew :internal-api:test --tests datadog.trace.bootstrap.instrumentation.api.AgentTracerTest :dd-trace-ot:test --tests datadog.opentracing.DDTracerTest
  • ./gradlew :dd-java-agent:instrumentation:opentelemetry:opentelemetry-1.47:forkedTest --tests opentelemetry147.metrics.OpenTelemetryMetricsLifecycleForkedTest
  • ./gradlew spotlessApply

Contributor Checklist

  • Format the title according to the contribution guidelines.
  • Assign the required type: and comp:/inst: labels.
  • Add focused JUnit 5 tests.
  • Include a companion documentation change.
  • Use merge queue after approval.

Jira ticket: N/A

@datadog-official

datadog-official Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
Patch Coverage: 46.03%
Overall Coverage: 58.84% (-0.17%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 2fd3467 | Docs | View more details | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.01 s 14.06 s [-1.0%; +0.2%] (no difference)
startup:insecure-bank:tracing:Agent 12.95 s 12.99 s [-1.1%; +0.3%] (no difference)
startup:petclinic:appsec:Agent 17.11 s 16.84 s [+0.4%; +2.7%] (maybe worse)
startup:petclinic:iast:Agent 17.11 s 17.01 s [-0.4%; +1.5%] (no difference)
startup:petclinic:profiling:Agent 16.63 s 16.77 s [-1.8%; +0.1%] (no difference)
startup:petclinic:sca:Agent 17.07 s 16.83 s [+0.5%; +2.4%] (maybe worse)
startup:petclinic:tracing:Agent 16.14 s 15.64 s [-1.1%; +7.4%] (no difference)

Commit: 2fd3467c · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@dougqh

dougqh commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Note from Claude (reviewing on behalf of @dougqh): not a blocker, but worth thinking through before this lands.

Neither OtlpMetricsService.forceFlush() nor shutdown() puts a deadline on the returned CompletableFuture<Boolean>. The only bound in the chain is the transport's own retry policy:

  • OtlpHttpSender retries via HttpRetryPolicy.Factory(5, 100, 2.0, true) — up to 5 retries, and the trailing true means InterruptedIOException (covers SocketTimeoutException) is retried, not just hard connection failures.
  • Each attempt is bounded by otlp.metrics.timeout, which defaults to 10s.
  • Worst case under a degraded (not dead) network — connects but reads stall — that's roughly 6 attempts × ~10s + backoff ≈ 60+ seconds before export()/finishShutdown() returns and the future completes.

For the automatic path (CoreTracer#close()) this is harmless today, since it calls shutdown() fire-and-forget without awaiting the future.

But it cuts against the PR's own motivation: a short-lived job calling OpenTelemetryMetrics.shutdown().join()/.get() to make sure metrics leave before the process exits could block up to ~a minute under a merely-slow network, which is a rough trade for something meant to bound a short-lived job's exit.

Worth considering wrapping the flush/shutdown future with orTimeout(...)/completeOnTimeout(...) (or using a tighter/no-retry policy specifically for the shutdown-triggered final export) so callers of the new public API get a predictable upper bound independent of the transport's retry behavior.

@mabdinur
mabdinur requested a review from mhlidd September 1, 2026 20:45
@mabdinur
mabdinur marked this pull request as ready for review September 1, 2026 21:57
@mabdinur
mabdinur requested review from a team as code owners September 1, 2026 21:57
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T22:04:50.136580Z 1b4200c Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@datadog-official datadog-official Bot 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.

Datadog Autotest: FAIL

The tracer shutdown path starts the final export on a daemon thread but does not wait for it. The DDTracer wrapper also returns unavailable for both new lifecycle calls instead of forwarding them.

Open Bits AI session

🤖 Datadog Autotest · Commit 1b4200c · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Comment thread dd-trace-api/src/main/java/datadog/trace/api/internal/InternalTracer.java Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

OtlpMetricsService.INSTANCE.shutdown();

P1 Badge Await metrics shutdown from CoreTracer.close

When CoreTracer.close() runs, especially from dd-tracer-shutdown-hook, this call now only enqueues finishShutdown and discards its future. The exporter uses a daemon AgentThreadFactory, so the JVM may terminate after the hook returns while the final send is still running; manual close likewise returns before sender.shutdown() completes. The previous implementation closed the sender synchronously, so wait for this future with a bounded timeout before returning.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread dd-trace-api/src/main/java/datadog/trace/api/internal/InternalTracer.java Outdated
@mabdinur
mabdinur force-pushed the brian.marks/otel-metrics-lifecycle branch from 1b4200c to 7e11776 Compare September 1, 2026 23:25
@mabdinur

mabdinur commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@dougqh On the deadline question: the future intentionally has no built-in timeout. Callers can bound their wait with get(timeout, unit), which preserves Java 8 support and does not imply that the in-flight transport was cancelled. CoreTracer.close() now applies a 2.5-second bound.

@mabdinur mabdinur 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.

Reviewed the Datadog lifecycle implementation, addressed feedback, and focused tests. CI is green.

@mhlidd
mhlidd requested a review from mcculls September 2, 2026 19:05
forceFlush();
}

public CompletableFuture<Boolean> shutdown() {

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.

From Claude:

Terminal singleton kills pipeline for any subsequent tracer. This is a regression the PR introduces, not pre-existing. Old shutdown() was scheduledTask.cancel(); sender.shutdown(); — no latch, and the AgentTaskScheduler was never shut down, so start() could reschedule. The PR adds both the one-way latch and executor termination.

* {@code false} if export is unavailable, fails, or shutdown has begun. The future has no
* deadline; a timed wait bounds only the caller and does not cancel export.
*/
public static CompletableFuture<Boolean> forceFlush() {

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.

w/ Claude:

The use of Boolean here does not match what Node already does. dd-trace-js#10023 uses forceFlush(callback?: (error?: Error) => void), and its not-configured path calls done?.() with no error, aka success, where Java returns false. So the same "OTLP metrics not enabled" deployment reports success on Node and failure on Java, and Boolean also can't separate that from a real export failure.

Should we align w/ the Node implementation here for cross language parity and to differentiate between real export failures and a shutdown?

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.

Looking at the other places where we check instanceof InternalTracer we usually fall back to a "no-op" behaviour. The equivalent here would be returning completedFuture(true) which would match what Node does.

This also makes sense from a caller's perspective - if the registered tracer doesn't support OTel metrics then this call would truly be a "no-op" and should succeed because it has nothing to do. Whereas returning false makes it look like there's a problem to be investigated...


void flushMetrics();

default CompletableFuture<Boolean> forceFlushOtelMetrics() {

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.

I would make these methods non-default, as otherwise you risk forgetting to implement them (as happened here)

There are only a few implementations of this API, and the javadoc clearly states that methods may be added/removed at any time. This would also help resolve some ambiguity of what the default implementation should be - the only remaining place to fix would be NoopTracerAPI and in that case it should be returning true (success) since there is nothing to flush/shutdown, so those methods always succeed.

return unavailable();
}

private static CompletableFuture<Boolean> unavailable() {

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.

As mentioned above, I'd change this to be a "no-op" style method returning completedFuture(true)

* first result; {@code false} means the pipeline was unavailable or export or cleanup failed. The
* future has no deadline; a timed wait bounds only the caller and does not cancel shutdown.
*/
public static CompletableFuture<Boolean> shutdown() {

@mcculls mcculls Sep 3, 2026

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.

I have concerns about using CompletableFuture in a public tracer API because this is something we trace. We also try to avoid touching anything fork-join related internally - at least before we've had the chance to do some initial transformations, because it ends up loading certain JDK classes before we get a chance to field-inject them.

( basically CompletableFuture refers to the common ForkJoinPool during static initialization, so ideally we'd avoid touching that class too early. )

I see we've already had to resort to setAsyncPropagationEnabled(false) in the updated implementation of the metrics service which shows this is a real concern.

I wonder if we could use another mechanism here that is simpler and more tracer friendly?

@mcculls mcculls Sep 3, 2026

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.

Side note: no OpenTelemetry API currently uses CompletableFuture (or even anything Future related)

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.

Potential options, in no particular order:

  • Return Future (an interface, not an implementation like CompletableFuture)
  • Let callers provide a callback - again, callers will need to decide how to use that
  • Return our own "completable" type, like OTel's CompletableResultCode
  • Make this a blocking method - callers then decide whether to make it async using their own pool

Note if we did return a Future then we should still try to avoid pulling in CompletableFuture behind the scenes if possible - instead we should try to wrap AgentTaskScheduler.Scheduled as a Future which would simplify the rest of the changes here.

@mcculls mcculls 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.

We should avoid using CompletableFuture in a public trace/metrics API

I've suggested some alternatives in other comments

if (initialConfig.isMetricsOtlpExporterEnabled()) {
return OtlpMetricsService.INSTANCE.forceFlush();
}
return CompletableFuture.completedFuture(false);

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.

If OTel metrics are not enabled this is effectively a no-op and should return true, not false

if (initialConfig.isMetricsOtlpExporterEnabled()) {
return OtlpMetricsService.INSTANCE.shutdown();
}
return CompletableFuture.completedFuture(false);

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.

If OTel metrics are not enabled this is effectively a no-op and should return true, not false

@bm1549
bm1549 requested a review from a team as a code owner September 4, 2026 16:10
@bm1549
bm1549 requested review from vandonr and removed request for a team September 4, 2026 16:10
@bm1549 bm1549 changed the title Add OpenTelemetry metrics lifecycle controls Expose OpenTelemetry metrics shutdown through MeterProvider Sep 4, 2026
@bm1549
bm1549 marked this pull request as draft September 4, 2026 16:18

@datadog-official datadog-official Bot 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.

Datadog Autotest: FAIL

A callback on one shutdown result can block completion of later shutdown results. This behavior breaks the repeated-call result contract.

Open Bits AI session

🤖 Datadog Autotest · Commit cd8ab82 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest


private CompletableResultCode shutdownResultView() {
CompletableResultCode result = new CompletableResultCode();
shutdownResult.whenComplete(

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 One callback can block other shutdown results

A second caller can time out after shutdown finishes, so repeated shutdown calls do not observe the first result.

Assertion details
  • Input: Two callers call shutdown before export finishes. The first caller adds a callback that does not return.
  • Expected: The service must complete all shutdown results after export and cleanup finish. One caller callback must not block another result.
  • Actual: The first result callback runs on the completion thread. If it does not return, the service does not complete the second result.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest · Open Bits AI session

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: metrics Metrics inst: opentelemetry OpenTelemetry instrumentation tag: ai generated Largely based on code generated by an AI or LLM type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants