Expose OpenTelemetry metrics shutdown through MeterProvider - #12317
Expose OpenTelemetry metrics shutdown through MeterProvider#12317bm1549 wants to merge 8 commits into
Conversation
|
🎯 Code Coverage (details) 🔗 Commit SHA: 2fd3467 | Docs | View more details | Give us feedback! |
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
|
Note from Claude (reviewing on behalf of @dougqh): not a blocker, but worth thinking through before this lands. Neither
For the automatic path ( But it cuts against the PR's own motivation: a short-lived job calling Worth considering wrapping the flush/shutdown future with |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
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.
🤖 Datadog Autotest · Commit 1b4200c · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
💡 Codex Review
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".
1b4200c to
7e11776
Compare
|
@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
left a comment
There was a problem hiding this comment.
Reviewed the Datadog lifecycle implementation, addressed feedback, and focused tests. CI is green.
| forceFlush(); | ||
| } | ||
|
|
||
| public CompletableFuture<Boolean> shutdown() { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Side note: no OpenTelemetry API currently uses CompletableFuture (or even anything Future related)
There was a problem hiding this comment.
Potential options, in no particular order:
- Return
Future(an interface, not an implementation likeCompletableFuture) - 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
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
If OTel metrics are not enabled this is effectively a no-op and should return true, not false
There was a problem hiding this comment.
A callback on one shutdown result can block completion of later shutdown results. This behavior breaks the repeated-call result contract.
🤖 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( |
There was a problem hiding this comment.
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
What Does This Do
Adds a Datadog extension to the OpenTelemetry
MeterProviderreturned byGlobalOpenTelemetry:shutdown()performs a final export and stops Datadog's OpenTelemetry metrics export pipeline. Java does not expose a publicforceFlush()extension.The return type follows OpenTelemetry's
CompletableResultCodepattern without usingCompletableFuture. A timedjoin()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
void flushMetrics()bridge retains its signature and 2.5-second timeout.forceFlush()andshutdown().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 spotlessApplyContributor Checklist
type:andcomp:/inst:labels.Jira ticket: N/A