Skip to content

fix(id): reset OpenTelemetry and runtime state on refresh - #10324

Draft
litianningdatadog wants to merge 1 commit into
tianning.li/1-dogstatsd-exporter-refresh-resetsfrom
tianning.li/2-otel-runtime-refresh-resets
Draft

litianningdatadog wants to merge 1 commit into
tianning.li/1-dogstatsd-exporter-refresh-resetsfrom
tianning.li/2-otel-runtime-refresh-resets

Conversation

@litianningdatadog

@litianningdatadog litianningdatadog commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Stacked PRs

  • #10323 tianning.li/1-dogstatsd-exporter-refresh-resets
  • #10324 tianning.li/2-otel-runtime-refresh-resets — current PR
  • #10325 tianning.li/3-exporter-otel-identity-gaps
  • #10326 tianning.li/4-runtime-datastream-identity-gaps
  • #10327 tianning.li/5-exporter-buffer-state
  • #10328 tianning.li/6-otel-datastream-counter-state
  • #10329 tianning.li/7-spanstats-otel-retry-state
  • #10330 tianning.li/8-agent-otel-counter-baselines
  • #10331 tianning.li/9-datastream-dogstatsd-exporter-requests

Origin

This change set was split from the monolithic PR #9814 to keep the identity-refresh fixes smaller and easier to review.

What does this PR do?

Reset OpenTelemetry log/metric queues and runtime metric baselines on identity refresh.

Motivation

OpenTelemetry and runtime metrics can retain measurements, observers, and baselines captured before a MicroVM snapshot. Resetting them ensures post-resume telemetry starts from the clone-local identity.

Logical change

This is the 2-otel-runtime-refresh-resets layer of the stacked MicroVM identity-refresh fix. It is based on tianning.li/1-dogstatsd-exporter-refresh-resets and keeps the subsystem-specific reset behavior isolated for review.

Jira

SVLS-9768

Tests

./node_modules/.bin/mocha packages/dd-trace/test/opentelemetry/logs.spec.js packages/dd-trace/test/opentelemetry/metrics.spec.js
node --expose-gc ./node_modules/mocha/bin/mocha.js packages/dd-trace/test/runtime_metrics.spec.js

Additional Notes

This PR is intentionally opened as a draft and is part of a linear stack.

@dd-octo-sts

dd-octo-sts Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 8.96 MB
Deduped: 9.7 MB
No deduping: 9.7 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.5.1 | 127.66 kB | 513.61 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog retried 1 test - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
• Patch Coverage: 100.00%
• Overall Coverage: 98.21% (-0.01%)

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

@codecov

codecov Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.71%. Comparing base (6533cee) to head (741937c).

Additional details and impacted files
@@                               Coverage Diff                                @@
##           tianning.li/1-dogstatsd-exporter-refresh-resets   #10324   +/-   ##
================================================================================
  Coverage                                            98.71%   98.71%           
================================================================================
  Files                                                 1025     1025           
  Lines                                               160673   160770   +97     
  Branches                                                74       74           
================================================================================
+ Hits                                                158610   158707   +97     
  Misses                                                2063     2063           
Flag Coverage Δ
ai-guard 65.26% <ø> (ø)
apm-capabilities 63.72% <100.00%> (+0.03%) ⬆️
apm-integrations 81.57% <ø> (ø)
appsec 58.94% <ø> (ø)
debugger 70.19% <ø> (-0.02%) ⬇️
instrumentation 54.46% <ø> (-0.01%) ⬇️
llmobs 79.08% <ø> (ø)
master-coverage ?
openfeature 66.51% <ø> (ø)
platform 69.40% <60.37%> (+0.01%) ⬆️
profiling 66.86% <ø> (ø)
serverless 65.99% <ø> (ø)
test-optimization 82.54% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@litianningdatadog
litianningdatadog marked this pull request as ready for review September 14, 2026 20:20
@litianningdatadog
litianningdatadog requested review from a team as code owners September 14, 2026 20:20
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 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-14T20:38:08.909173Z 741937c Draft marked ready
🔒 Security Review ✅ Completed 2026-09-14T20:33:31.091573Z 741937c 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.

@litianningdatadog litianningdatadog added the aws-microvm AWS MicroVM related features label Sep 14, 2026

Copilot AI 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.

🟡 Changes recommended

Unresolved critical issues remain around in-flight telemetry and runtime-client state, with additional shutdown cleanup gaps.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds identity-refresh resets for OpenTelemetry queues and runtime metric baselines after MicroVM refreshes.

Changes:

  • Resets queued logs, metrics, and runtime sampler baselines.
  • Subscribes telemetry components to identity refresh events.
  • Adds regression tests for refresh behavior.
File summaries
File Reviewed changes and final notes
packages/dd-trace/test/runtime_metrics.spec.js Adds runtime refresh coverage.
packages/dd-trace/test/opentelemetry/metrics.spec.js Adds metric queue reset coverage.
packages/dd-trace/test/opentelemetry/logs.spec.js Adds log queue reset coverage.
packages/dd-trace/src/runtime_metrics/runtime_metrics.js Resets runtime samplers. Moderate (1 vote): native sampler state is not cleared on refresh.
packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js Resets OTLP runtime sampler state.
packages/dd-trace/src/runtime_metrics/client.js Adds refresh handling. Critical (1 vote): pending aggregation and the wrapped DogStatsD buffer may survive refresh when runtime tags are unchanged.
packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js Clears pending metric state. Critical (1 vote each): in-flight batches can export pre-refresh data, noted at lines 241 and 244. Nits (1 vote each): clear stale dropped-count state, and strengthen the baseline-reset test.
packages/dd-trace/src/opentelemetry/metrics/index.js Subscribes metrics to refreshes. Moderate (1 vote): shutdown does not unsubscribe the refresh listener.
packages/dd-trace/src/opentelemetry/logs/index.js Subscribes logs to refreshes. Moderate (1 vote): shutdown does not unsubscribe the refresh listener.
packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js Clears pending log records. Critical (1 vote): in-flight exports can complete with pre-refresh data.
Review details

Suppressed comments (6)

packages/dd-trace/src/opentelemetry/logs/index.js:79

  • This listener is only removed when initializeOpenTelemetryLogs runs again. After the public LoggerProvider.shutdown() or global logs disable path, the channel still strongly retains the processor/exporter through this closure and invokes it on later refreshes, so a stopped pipeline cannot be collected. Unsubscribe it as part of provider shutdown rather than only on re-init.
  // A clone resume shouldn't export log records queued before the snapshot under its own identity.
  unsubscribeLogsPendingStateReset?.()
  const onIdentityRefresh = () => processor.resetPendingState()
  identityRefreshChannel.subscribe(onIdentityRefresh)
  unsubscribeLogsPendingStateReset = () => identityRefreshChannel.unsubscribe(onIdentityRefresh)

packages/dd-trace/src/opentelemetry/metrics/index.js:84

  • This listener is only removed when initializeOpenTelemetryMetrics runs again. If the public MeterProvider.shutdown() or global metrics disable path is used without reinitialization, the channel retains this closure—and therefore the reader, exporter, and instrument state—for the rest of the process, and future refreshes still call the shut-down reader. Tie the unsubscribe to provider/reader shutdown instead of only replacing it on re-init.
  // A clone resume shouldn't export measurements queued before the snapshot under its own identity.
  unsubscribeMetricsPendingStateReset?.()
  const onIdentityRefresh = () => reader.resetPendingState()
  identityRefreshChannel.subscribe(onIdentityRefresh)
  unsubscribeMetricsPendingStateReset = () => identityRefreshChannel.unsubscribe(onIdentityRefresh)

packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js:247

  • Clearing #cumulativeState restarts the accumulated value, but MetricAggregator.#startTime is initialized once and is not rebased here. With CUMULATIVE temporality (and always-cumulative UpDownCounters), the first post-refresh point therefore carries a startTimeUnixNano from before the snapshot while its value starts from the clone-local zero; reset the aggregator's cumulative start time together with this state.
    for (const key of this.#cumulativeState.keys()) {
      this.#lastExportedState.delete(key)
    }
    this.#cumulativeState.clear()

packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js:247

  • A queue overflow before the refresh leaves #droppedCount intact. If the refresh occurs before the next collection, the next export logs that it dropped pre-refresh measurements, which is stale and misleading; clear this counter when discarding the pending state.
  resetPendingState () {
    this.#measurements = []

    for (const key of this.#cumulativeState.keys()) {
      this.#lastExportedState.delete(key)
    }
    this.#cumulativeState.clear()

packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js:247

  • The new test refreshes before the first collection, so #cumulativeState is empty and this loop never executes. It would still pass if #lastExportedState were not deleted; export a synchronous counter before the refresh, then record 7 afterward and assert the post-refresh delta is 7 (not 2) to cover the baseline-reset behavior introduced here.
    for (const key of this.#cumulativeState.keys()) {
      this.#lastExportedState.delete(key)
    }
    this.#cumulativeState.clear()

packages/dd-trace/src/runtime_metrics/runtime_metrics.js:196

  • When the native-metrics branch is active, eventLoopDelayObserver is never created (the branch above uses nativeMetrics.stats() instead), so this refresh callback does not clear any state accumulated by the native sampler. Samples from the pre-snapshot interval can therefore be returned by the first post-refresh stats() call and emitted with the clone's tags. Reset or consume the native sampler state at the refresh boundary as well.
  if (eventLoopDelayObserver) {
    eventLoopDelayObserver.disable()
    eventLoopDelayObserver.reset()
    eventLoopDelayObserver.enable()
  }
  • Files reviewed: 10/10 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +109 to +111
resetPendingState () {
this.#logRecords = []
this.#clearTimer()
Comment on lines +241 to +242
resetPendingState () {
this.#measurements = []
Comment on lines +56 to +58
const onIdentityRefresh = () => {
client.updateTags(buildClientConfig(config).tags)
onRefresh?.()
@litianningdatadog
litianningdatadog marked this pull request as draft September 14, 2026 20:25

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 741937c1cd

ℹ️ 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 on lines +184 to +185
function resetSamplerBaselines () {
lastTime = performance.now()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drain native sampler state during refresh

When runtime metrics use the default native path on Node versions without samplePerIteration, this callback resets only the JavaScript baselines. @datadog/native-metrics retains the interval consumed by nativeMetrics.stats(), so after cloning its CPU, event-loop, and GC samples still include pre-snapshot work; resetting lastTime alone can additionally divide that old CPU delta by only the short post-resume interval. The first flush from every clone can therefore duplicate or spike these metrics, so drain or restart the native sampler here and cover the currently skipped native test variant.

AGENTS.md reference: AGENTS.md:L79-L79

Useful? React with 👍 / 👎.

Comment on lines +244 to +247
for (const key of this.#cumulativeState.keys()) {
this.#lastExportedState.delete(key)
}
this.#cumulativeState.clear()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebase cumulative metric start times when clearing state

When cumulative temporality is selected, or for an UpDownCounter that is always cumulative, clearing #cumulativeState restarts the exported value at zero but leaves MetricAggregator.#startTime at reader construction. Every subsequent point then advertises a pre-refresh start time while containing only post-refresh samples, producing a semantically inconsistent cumulative series and potentially undercounting backend rate conversions. Reset the aggregator start time with this state and add cumulative-temporality coverage rather than testing only the default delta counter path.

AGENTS.md reference: AGENTS.md:L79-L79

Useful? React with 👍 / 👎.

@litianningdatadog
litianningdatadog marked this pull request as ready for review September 14, 2026 20:29
@pr-commenter

pr-commenter Bot commented Sep 14, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-09-14 20:31:08

Comparing candidate commit 741937c in PR branch tianning.li/2-otel-runtime-refresh-resets with baseline commit 6533cee in branch tianning.li/1-dogstatsd-exporter-refresh-resets.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 2304 metrics, 9 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:appsec-iast-no-vulnerability-iast-enabled-default-config-24

  • unstable max_rss_usage [-23.989MB; +39.455MB] or [-7.289%; +11.988%]

scenario:appsec-iast-with-vulnerability-iast-enabled-default-config-20

  • unstable max_rss_usage [-17.046MB; +20.370MB] or [-4.984%; +5.955%]

scenario:encoders-0.4-immediate-flush-20

  • unstable max_rss_usage [-10.756MB; +3.082MB] or [-14.301%; +4.097%]

scenario:llmobs-encode-unicode-mixed-20

  • unstable max_rss_usage [-8.849MB; +5.806MB] or [-10.857%; +7.124%]

scenario:plugin-graphql-long-with-depth-off-26

  • unstable cpu_user_time [-417.974ms; +366.162ms] or [-5.516%; +4.832%]
  • unstable execution_time [-445.203ms; +389.343ms] or [-5.767%; +5.044%]
  • unstable max_rss_usage [-30717.857KB; +31721.057KB] or [-13.698%; +14.146%]
  • unstable throughput [-17.121op/s; +19.804op/s] or [-5.137%; +5.942%]

scenario:plugin-mongodb-core-binary-hash-20

  • unstable execution_time [-144.354ms; +227.802ms] or [-4.718%; +7.446%]

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 741937c1cd

ℹ️ 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".

* since resume.
* @returns {void}
*/
function resetSamplerBaselines () {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drain pending GC observer entries on refresh

When a GC entry is pending in the PerformanceObserver at snapshot time, this refresh callback resets the CPU and event-loop baselines but leaves that entry queued, so its callback runs after resume and reports the pre-snapshot pause through the clone's refreshed metrics client. Drain gcObserver.takeRecords() during refresh; the sibling resetSamplerBaselines() in otlp_runtime_metrics.js has the same omission and can likewise enqueue old GC durations after the metric reader has already been cleared.

AGENTS.md reference: AGENTS.md:L79-L79

Useful? React with 👍 / 👎.

Comment on lines +268 to +269
if (lastElu !== null) {
lastElu = performance.eventLoopUtilization()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Seed the event-loop-time counter on refresh

When a clone is restored before the first periodic metric export, the reader has no nodejs.eventloop.time entry in #lastExportedState; assigning lastElu here only rebases the utilization gauge, while the counter callback still observes the absolute idle and active totals from process startup. Its first delta export therefore includes the entire pre-snapshot initialization interval in every clone. Establish a refresh-time baseline for this observable counter, including the corresponding reader baseline, so only post-resume event-loop time is exported.

AGENTS.md reference: AGENTS.md:L79-L79

Useful? React with 👍 / 👎.

Comment on lines +80 to +83
// A clone resume shouldn't export measurements queued before the snapshot under its own identity.
unsubscribeMetricsPendingStateReset?.()
const onIdentityRefresh = () => reader.resetPendingState()
identityRefreshChannel.subscribe(onIdentityRefresh)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep reset hooked to the active provider on reinitialization

When this initializer is called again without first disabling the OpenTelemetry global API—the re-init case the new comment claims to handle—setGlobalMeterProvider() does not replace the already registered provider, so applications continue recording through the first reader. These lines nevertheless unsubscribe that reader's reset callback and subscribe the newly constructed but unused reader; a later identity refresh therefore leaves the active reader's queued and cumulative state intact. Only swap the subscription when provider registration succeeds, or explicitly tear down the old provider first; the logs initializer has the same sibling failure.

AGENTS.md reference: AGENTS.md:L79-L79

Useful? React with 👍 / 👎.

@litianningdatadog
litianningdatadog marked this pull request as draft September 14, 2026 21:41

This branch has not been deployed

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

Labels

aws-microvm AWS MicroVM related features semver-patch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants