Skip to content

chore(runtime): refresh runtime metric identity and collector state - #19822

Draft
litianningdatadog wants to merge 1 commit into
tianning.li/3-4-telemetry-identity-refreshfrom
tianning.li/3-5-runtime-metrics-identity-refresh
Draft

litianningdatadog wants to merge 1 commit into
tianning.li/3-4-telemetry-identity-refreshfrom
tianning.li/3-5-runtime-metrics-identity-refresh

Conversation

@litianningdatadog

@litianningdatadog litianningdatadog commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Stacked PRs:

Description

After a MicroVM identity refresh, runtime metrics must use the new runtime identity and
must not mix a pre-refresh measurement interval with post-refresh process activity. This
PR covers both halves of that:

Platform tags. When runtime-ID tagging is enabled, runtime metrics recollect platform
tags during the periodic runtime-metrics flush instead of keeping the tag list built before
/run, so a MicroVM identity refresh is reflected on the next flush. When runtime-ID
tagging is disabled, platform tags stay cached (no behavior change).

Collector state. A MicroVM /run transition rotates the runtime ID mid-process without
a fork. Left alone, the CPU time, context-switch, wall-clock, and GC pause/collections
collectors would report a delta spanning both the old and new identity on their next sample.
RuntimeWorker now resets those collectors' baselines through the existing
on_runtime_identity_refresh callback, registered only when the process is a MicroVM and
runtime-ID tagging is enabled; it's unregistered on stop(). Non-MicroVM processes and
MicroVMs without runtime-ID tagging register nothing and pay no extra cost.

The worker also records the runtime ID its collector baselines belong to, updated only after a
reset completes. If that ID differs from the current one, flush() resets and sends nothing for
that interval, which covers a refresh callback that was missed (the worker was created mid-refresh)
or failed and is still pending. A reset that raises there is logged and retried on the next flush
instead of stopping the worker thread, and a retried refresh callback for an ID a flush already
reset for is a no-op. A fork hook moves that ID to the child's new runtime ID, since the collectors
already reseed on fork, so a forked child's first flush doesn't reset them again.

GCRuntimeMetricCollector.reset() reseeds the collections baseline before clearing the pause
window, matching enable time: a collection between the two steps can show up as collections
without a pause, never as a pause without collections.

Refresh synchronization. The worker shares the fork-safe MicroVM refresh lock from #19939.
It holds that lock across collector reset and the full runtime-metrics flush, including tag
collection, sampling, and sending, so identity rotation cannot overlap an in-flight export.
The lock is not used by non-MicroVM or runtime-ID-disabled paths.

This does not add a second identity-refresh callback/guard, and does not touch trace,
writer, or telemetry-worker lifecycle code.

Reference

Testing

Added focused subprocess/unit coverage for:

  • replacing the old runtime-ID tag after identity refresh
  • retaining the current runtime-ID on subsequent flushes
  • keeping platform tags cached when runtime-ID tagging is disabled
  • serializing MicroVM identity rotation with an in-flight runtime-metrics flush
  • a MicroVM identity refresh resetting collector state (RuntimeMetrics.reset())
  • non-MicroVM and MicroVM-without-runtime-ID processes never registering the reset hook
  • RuntimeWorker.stop() unregistering the refresh callback and fork hook
  • flush() resetting (and sending nothing) after a failed refresh reset, and after a refresh that
    lands between collector seeding and callback registration
  • a flush whose reset fails neither raising nor sending, and a retried refresh callback not
    resetting again once a flush has
  • a forked MicroVM child's first flush not resetting collectors again
  • non-MicroVM flushes ignoring runtime-ID changes
  • a collection during GCRuntimeMetricCollector.reset() never surfacing as a pause without
    collections
  • GCRuntimeMetricCollector/NativeProcessMetricCollector.reset() discarding pre-refresh
    pause/collections and CPU-time/context-switch/wall-clock state

The previous test that forced refreshes to interleave inside a flush was replaced because the
shared lock makes that interleaving impossible; the new regression test verifies the intended
serialization boundary directly.

Validation:

  • scripts/run-tests --venv 817352e -- -p no:randomly tests/runtime/test_runtime_metrics_api.py (Python 3.14): all tests added or changed here pass (21 passed, 2 xfailed). The 9 failures predate this PR: RuntimeMetrics.enable() no longer accepts flush_interval (removed in 4.0), the DD_RUNTIME_METRICS_INTERVAL assertions, a dogstatsd Connection refused on stderr with no local agent, and test_runtime_metrics_experimental_runtime_tag (fails in the test container; the runtime-id tag is present when checked outside it). The runtime suite is skip: true in tests/suitespec.yml, so CI does not run it.
  • After the flush-skip and retry changes: scripts/run-tests -s --venv 817352e -- -p no:randomly -k "microvm or refresh_identity or non_microvm" tests/runtime/test_runtime_metrics_api.py (11 passed)
  • scripts/lint fmt -- ddtrace/internal/runtime/metric_collectors.py ddtrace/internal/runtime/runtime_metrics.py tests/runtime/test_runtime_metrics_api.py tests/tracer/runtime/test_metric_collectors.py
  • scripts/lint typing -- ddtrace/internal/runtime/metric_collectors.py ddtrace/internal/runtime/runtime_metrics.py

Risks

Limited to runtime-metrics platform-tag collection and the two metric collectors' interval
state. The shared lock adds synchronization only for MicroVMs with runtime-ID tagging; the
runtime-ID-disabled and non-MicroVM paths are unchanged. A flush now waits for an in-flight
MicroVM identity transition, and a transition waits for an in-flight metrics flush, preventing
partial identity attribution. When a refresh reset was missed or failed, the next flush resets
and sends nothing, so runtime metrics have a one-interval gap there (including a skipped final
flush if that happens at shutdown).

@litianningdatadog litianningdatadog added changelog/no-changelog A changelog entry is not required for this PR. aws-microvm Work related to AWS MicroVM onboarding labels Aug 23, 2026
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 1 circular imports that already exist on the base branch and have not been changed by this PR.

ddtrace.errortracking._handled_exceptions.bytecode_injector -> ddtrace.errortracking._handled_exceptions.callbacks -> ddtrace.errortracking._handled_exceptions.collector -> ddtrace.errortracking._handled_exceptions.bytecode_reporting -> ddtrace.errortracking._handled_exceptions.bytecode_injector

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

Dependency direction analysis

⚠️ Existing dependency direction violations

There are 163 dependency direction violations that already exist on the base branch and have not been changed by this PR.

Show existing violations (showing 5 of 163 highest severity)
ddtrace.internal.tracemethods -×-> ddtrace.trace  (internal-core -> product:tracing, score=121)
ddtrace.llmobs._integrations.llama_index -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=119)
ddtrace.llmobs._integrations.vertexai -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=119)
ddtrace.appsec._listeners -×-> ddtrace.trace  (product:appsec -> product:tracing, score=119)
ddtrace.llmobs._integrations.base -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=119)

To see all violations, download the layers-base.json and layers-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/layers.py compare layers-base.json layers-pr.json

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

Codeowners resolved as

Resolved from the full PR diff against tianning.li/3-4-telemetry-identity-refresh using the target branch CODEOWNERS file.
CODEOWNERS team requests not listed below are not required by the current file set.

ddtrace/internal/runtime/collector.py                                   @DataDog/apm-sdk-capabilities
ddtrace/internal/runtime/metric_collectors.py                           @DataDog/apm-sdk-capabilities
ddtrace/internal/runtime/runtime_metrics.py                             @DataDog/apm-sdk-capabilities
ddtrace/trace/__init__.py                                               @DataDog/apm-sdk-capabilities
releasenotes/notes/aws-lambda-microvm-identity-refresh-3a672cd6bcbad16d.yaml  @DataDog/apm-python
releasenotes/notes/fix-runtime-metrics-microvm-identity-refresh-57f069db02623f9d.yaml  @DataDog/apm-python
tests/runtime/test_runtime_metrics_api.py                               @DataDog/apm-sdk-capabilities
tests/tracer/runtime/test_metric_collectors.py                          @DataDog/apm-sdk-capabilities
tests/tracer/runtime/test_runtime_id.py                                 @DataDog/apm-sdk-capabilities

@litianningdatadog litianningdatadog changed the title fix(runtime): refresh runtime metric identity tags chore(runtime): refresh runtime metric identity tags Aug 23, 2026
@datadog-prod-us1-4

datadog-prod-us1-4 Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Tests

✅ All CI checks and tests passed.

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

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

@pr-commenter

pr-commenter Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-10-15 22:32:52

Comparing candidate commit e5e703f in PR branch tianning.li/3-5-runtime-metrics-identity-refresh with baseline commit 918bf21 in branch tianning.li/3-4-telemetry-identity-refresh.

📊 Benchmarking dashboard

Found 3 performance improvements and 4 performance regressions! Performance is the same for 354 metrics, 12 unstable metrics, 3 known flaky benchmarks, 5 flaky benchmarks without significant changes.

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 ----------------------------------'

scenario:httppropagationextract-empty_headers

  • 🟩 execution_time [-68.218ns; -56.365ns] or [-9.265%; -7.656%]

scenario:httppropagationextract-full_t_id_datadog_headers

  • 🟥 execution_time [+1.944µs; +2.049µs] or [+13.456%; +14.188%]

scenario:httppropagationextract-wsgi_empty_headers

  • 🟩 execution_time [-66.793ns; -55.074ns] or [-9.151%; -7.546%]

scenario:httppropagationextract-wsgi_invalid_span_id_header

  • 🟩 execution_time [-65.895ns; -53.191ns] or [-8.946%; -7.222%]

scenario:msgpackencoderscenario-simple_one_span

  • 🟥 execution_time [+449.156ns; +488.572ns] or [+11.566%; +12.580%]

scenario:otelspan-start

  • 🟥 execution_time [+2.234ms; +3.115ms] or [+8.568%; +11.949%]

scenario:recursivecomputation-shallow

  • 🟥 execution_time [+58.227µs; +62.384µs] or [+8.297%; +8.889%]

Unstable benchmarks

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

scenario:coreapiscenario-context_with_data_listeners

  • unstable execution_time [-678.931ns; +769.244ns] or [-6.578%; +7.453%]

scenario:coreapiscenario-core_dispatch_1_listener

  • unstable execution_time [-31.939ns; +46.432ns] or [-4.869%; +7.078%]

scenario:coreapiscenario-core_dispatch_50_listeners

  • unstable execution_time [-1676.377ns; +2126.864ns] or [-8.517%; +10.806%]

scenario:coreapiscenario-core_dispatch_exception_listeners

  • unstable execution_time [-2327.268ns; +1317.085ns] or [-11.961%; +6.769%]

scenario:coreapiscenario-core_dispatch_listeners

  • unstable execution_time [-332.593ns; +419.981ns] or [-7.880%; +9.951%]

scenario:coreapiscenario-core_dispatch_no_args_listeners

  • unstable execution_time [-166.359ns; +285.992ns] or [-6.324%; +10.871%]

scenario:coreapiscenario-core_dispatch_with_results_1_listener

  • unstable execution_time [-70.940ns; +112.734ns] or [-5.333%; +8.475%]

scenario:coreapiscenario-core_dispatch_with_results_50_listeners

  • unstable execution_time [-4396.606ns; +4953.213ns] or [-9.067%; +10.215%]

scenario:coreapiscenario-core_dispatch_with_results_listeners

  • unstable execution_time [-827.995ns; +1062.713ns] or [-8.142%; +10.450%]

scenario:flasksimple-appsec-post

  • unstable execution_time [-720.569µs; +435.645µs] or [-26.222%; +15.853%]

scenario:flasksimple-appsec-telemetry

  • unstable execution_time [-809.476µs; +1004.290µs] or [-23.641%; +29.331%]

scenario:flasksimple-resource-renaming

  • unstable execution_time [-537.426µs; +526.653µs] or [-16.492%; +16.161%]

Known flaky benchmarks

These benchmarks are marked as flaky and will not trigger a failure. Modify FLAKY_BENCHMARKS_REGEX to control which benchmarks are marked as flaky.

scenario:httppropagationinject-ids_only

  • 🟥 execution_time [+2.271µs; +2.373µs] or [+16.320%; +17.051%]

scenario:span-start

  • 🟥 execution_time [+1.389ms; +1.680ms] or [+12.434%; +15.039%]

scenario:tracer-small

  • 🟥 execution_time [+46.011µs; +47.457µs] or [+18.813%; +19.404%]

Known flaky benchmarks without significant changes:

  • scenario:errortrackingflasksqli-baseline
  • scenario:flasksimple-iast-get
  • scenario:sethttpmeta-all-enabled
  • scenario:telemetryaddmetric-1-count-metric-1-times
  • scenario:telemetryaddmetric-record-100-metrics

@litianningdatadog
litianningdatadog force-pushed the tianning.li/2-flask-web-request-starting-event branch from d59e112 to 16a5332 Compare August 24, 2026 02:23
@litianningdatadog
litianningdatadog force-pushed the tianning.li/3-5-runtime-metrics-identity-refresh branch from d6a1546 to 335f6ac Compare August 24, 2026 02:24
@litianningdatadog
litianningdatadog force-pushed the tianning.li/2-flask-web-request-starting-event branch from 16a5332 to 8dd7e8e Compare August 24, 2026 02:31
@litianningdatadog
litianningdatadog force-pushed the tianning.li/3-5-runtime-metrics-identity-refresh branch from 335f6ac to 2e625cf Compare August 24, 2026 02:31
@litianningdatadog
litianningdatadog force-pushed the tianning.li/2-flask-web-request-starting-event branch 5 times, most recently from cde3045 to a0e3c42 Compare August 24, 2026 23:56
@litianningdatadog
litianningdatadog force-pushed the tianning.li/3-5-runtime-metrics-identity-refresh branch from 2e625cf to 7d36a71 Compare August 25, 2026 13:23
@litianningdatadog
litianningdatadog requested a lite review from Copilot August 25, 2026 13:36

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.

Pull request overview

This PR ensures runtime-metrics platform tags that include runtime identity (notably runtime-id) are refreshed when the runtime identity changes (e.g., after a MicroVM /run-triggered identity refresh), so runtime metrics emitted afterward carry the new identity.

Changes:

  • Register a runtime-id change listener in RuntimeWorker to rebuild cached platform tags when identity changes.
  • Refactor platform-tag construction into a helper and add an identity-refresh callback method.
  • Add a subprocess test asserting runtime-id: tags update after runtime.refresh_identity().

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
ddtrace/internal/runtime/runtime_metrics.py Builds platform tags via a helper and hooks runtime-id changes to refresh cached _platform_tags.
tests/runtime/test_runtime_metrics_api.py Adds a subprocess test covering platform-tag refresh behavior on identity refresh.

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

Comment thread ddtrace/internal/runtime/runtime_metrics.py Outdated
Comment thread ddtrace/internal/runtime/runtime_metrics.py Outdated
Comment thread ddtrace/internal/runtime/runtime_metrics.py Outdated
@litianningdatadog
litianningdatadog force-pushed the tianning.li/3-5-runtime-metrics-identity-refresh branch from 75336e0 to 4fed6d7 Compare September 24, 2026 20:44
@litianningdatadog
litianningdatadog force-pushed the tianning.li/3-4-telemetry-identity-refresh branch from 30e6c88 to e484bf7 Compare September 25, 2026 15:10
@litianningdatadog
litianningdatadog force-pushed the tianning.li/3-5-runtime-metrics-identity-refresh branch from 4fed6d7 to c037394 Compare September 25, 2026 15:12
@litianningdatadog
litianningdatadog force-pushed the tianning.li/3-4-telemetry-identity-refresh branch from e484bf7 to fb28096 Compare September 25, 2026 16:01
@litianningdatadog
litianningdatadog force-pushed the tianning.li/3-5-runtime-metrics-identity-refresh branch from c037394 to e7d5175 Compare September 25, 2026 16:02
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 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-10-14T20:30:20.578674Z 34c3c28 New commits
🔒 Security Review ✅ Completed 2026-10-14T20:31:54.482359Z 34c3c28 New commits
ℹ️ 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.

@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: e7d51759d3

ℹ️ 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 ddtrace/internal/runtime/runtime_metrics.py Outdated
Comment thread ddtrace/internal/telemetry/writer.py
Comment thread ddtrace/internal/telemetry/writer.py
Comment thread ddtrace/internal/runtime/runtime_metrics.py
Comment thread ddtrace/_trace/tracer.py Outdated
Comment thread ddtrace/internal/writer/writer.py Outdated

@datadog-prod-us1-4 datadog-prod-us1-4 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.

Bits Code Review: FAIL

The two most critical issues are: (1) a retry-idempotency gap in the identity-refresh coordinator — when any subscriber fails, already-completed callbacks (tracer writer recreation and runtime metrics reset) re-execute for the same runtime ID, dropping buffered traces and discarding legitimate metric deltas; and (2) a race between reset() and flush() in the runtime metrics collector that can emit negative or cross-identity deltas when an identity refresh overlaps a periodic flush.

Open Bits AI session

🤖 Bits Code Review · Commit 3d4a374 · @DataDog review to ask questions

@emmettbutler emmettbutler left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Deferring review since the base branch is not main

@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: fd2f295d2c

ℹ️ 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 +185 to +188
runtime_id = get_runtime_id()
self._platform_tags = self._collect_platform_tags()
runtime_metrics = list(self._runtime_metrics)
if runtime_id == get_runtime_id():

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 Reset collectors when the refresh callback remains pending

When the MicroVM hook rotates the runtime ID but an identity-refresh callback raises before this worker's callback runs—or this worker's own collector reset fails partway—the coordinator leaves callbacks pending for a later request while the new ID is already visible. A periodic flush between those attempts reaches this block with the old collector baselines, recollects the new runtime-id, and sends a mixed pre/post-refresh interval under that new ID. The before/after comparison cannot detect this because both reads occur while holding the refresh lock; track the ID associated with the collector baselines and reset before collection when it differs.

Useful? React with 👍 / 👎.

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 051c02d. RuntimeWorker now records the runtime ID its collector baselines belong to (_collectors_runtime_id), updated only after reset() returns. flush() compares it with the current ID under the refresh lock and resets before collecting on a mismatch, so a failed or still-pending callback can't send pre-refresh baselines under the new runtime-id.

Covered by test_runtime_metrics_microvm_flush_resets_after_failed_refresh_reset.

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.

Follow-up in 7e091b1: on a mismatch, flush() now resets and sends nothing for that interval rather than collecting right after the reset (a just-reset interval is microseconds long). If that reset raises, it's logged and retried on the next flush instead of stopping the worker's periodic thread. A retried callback for an ID a flush already reset for is a no-op (see the thread on _on_identity_refresh).

Comment on lines +175 to +176
with self._identity_refresh_lock:
self._runtime_metrics.reset()

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 Reset collectors at a deterministic refresh boundary

On every successful MicroVM refresh, the runtime ID is rotated before the callbacks are invoked, and those callbacks come from an unordered set. If the tracer or telemetry rebuild runs before this callback, its CPU, context switches, and GC activity occur under the new identity but are discarded here; if this callback happens first, the same activity is retained. Runtime metrics therefore vary with callback iteration order and can omit initialization work from the new runtime, so the collector reset needs a defined position immediately at the identity boundary rather than an unordered subscriber callback.

Useful? React with 👍 / 👎.

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.

Deferring this one to a follow-up. What changes with callback order is whether the CPU and GC activity of the subscribers that run before this one (tracer/writer and telemetry worker rebuilds) lands in the first post-refresh interval or is discarded. That work happens during the transition itself and doesn't clearly belong to either identity.

A fixed position needs ordering support in the refresh coordinator in _runtime_id.py (#19939), e.g. an early callback tier that runs right after rotation. Calling runtime metrics directly from there isn't an option: runtime_metrics already imports from _runtime_id, so it would create an import cycle. I'd rather not widen this PR with a coordinator change.

@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: e16f6204f3

ℹ️ 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 +110 to +111
monitor.reset()
self._reset_state()

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 the GC baseline and pause reset atomic

When a collection starts after monitor.reset() but before gc.get_stats() is sampled by _reset_state()—which can itself occur while allocating the returned stats/list—the monitor records that pause while the new collections baseline already includes the collection. The next flush therefore reports a positive GC pause with zero corresponding collection delta. Reset the pause window and reseed the collection baseline under one synchronization boundary so activity during the identity transition is either retained or discarded consistently.

Useful? React with 👍 / 👎.

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.

Reordered rather than made atomic (051c02d): reset() now reseeds the collections baseline before clearing the pause window, the same order _on_modules_load() uses at enable time.

A collection that lands between the two steps (including one triggered by an allocation inside gc.get_stats()) now shows up as a collections delta with its pause discarded. The reverse, a pause with no collections delta, can no longer happen. Worst case, one collection's pause is missing from the first interval after a refresh.

True atomicity needs both values from one source, i.e. counting per-generation collections in GCPauseMonitor._on_gc instead of reading gc.get_stats(). That changes where gc.collections.* comes from and needs a fallback when gc callbacks aren't supported, so I'd rather keep it out of this PR. Holding the monitor lock across both steps doesn't help: _on_gc gives up when it can't take the lock, so a collection during the reset still ends up as a collection without a pause.

Covered by test_reset_collection_mid_reset_is_not_a_pause_without_collections.

Comment on lines +107 to +109
self._identity_refresh_lock = get_runtime_identity_refresh_lock() if self._identity_refresh_enabled else None
if self._identity_refresh_enabled:
on_runtime_identity_refresh(self._on_identity_refresh)

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 Register refresh handling before seeding collectors

When the public RuntimeMetrics.enable() is called concurrently with a MicroVM /run refresh, the collectors are constructed and seed their CPU/GC baselines before the callback is registered here, without holding the refresh lock. If identity rotation lands in that window, the new worker misses the transition entirely; its first flush recollects the new runtime-id tag but reports deltas whose baselines came from the previous identity. Construct and register the worker state under the shared refresh lock, or verify the identity after registration and reseed when it changed.

Useful? React with 👍 / 👎.

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 051c02d with the verify option: _collectors_runtime_id is read before RuntimeMetrics() seeds the baselines, so a refresh that lands before the callback is registered shows up as a mismatch, and the first flush() reseeds before anything is sent. Nothing is sent before that first flush, so checking there is equivalent to checking right after registration.

Covered by test_runtime_metrics_microvm_flush_resets_after_refresh_during_construction.

@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: 051c02d2a1

ℹ️ 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 +188 to +189
with self._identity_refresh_lock:
self._reset_collectors(runtime_id)

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 Skip resets already repaired by a flush

When this callback's collector reset fails transiently, the MicroVM coordinator keeps the callback pending. If a periodic flush subsequently repairs the state through the new runtime-ID mismatch path and advances _collectors_runtime_id, the next /run retry still invokes this unconditional reset, discarding valid CPU and GC activity accumulated since the successful repair. Check whether runtime_id already equals _collectors_runtime_id before resetting so pending callback retries are idempotent.

Useful? React with 👍 / 👎.

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 7e091b1: _on_identity_refresh now returns early when runtime_id == self._collectors_runtime_id, so a retried callback after a flush already reset for that ID is a no-op and the post-repair interval is kept.

Covered by test_runtime_metrics_microvm_flush_resets_after_failed_refresh_reset.

@quinna-h quinna-h 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.

I'm also deferring review until the PR's base branch is main

@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: f6bc6fd247

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

# A fork rotates the runtime ID without an identity refresh, and the collectors already
# reseed in their own fork hooks, so the child's baselines belong to its new ID. Runs
# after _runtime_id's fork hook (registered at import), so this reads the child's ID.
self._collectors_runtime_id = get_runtime_id()

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 Advance the collector ID only after successful fork resets

In a runtime-ID-enabled MicroVM child, if either collector’s earlier fork hook raises (for example, gc.get_stats() or native.process_metrics() fails), forksafe.run_hooks() logs the exception and continues to this hook. This assignment then marks the inherited baselines as belonging to the child ID, so flush() sees no mismatch and skips its recovery reset, potentially emitting parent-derived CPU/GC deltas under the child runtime ID. Preserve the mismatch unless the collector resets are known to have succeeded. ddtrace/internal/README.mdL263-L270

Useful? React with 👍 / 👎.

@litianningdatadog litianningdatadog Oct 10, 2026 •

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 1a6ac93. _on_fork no longer assumes the collectors' own fork hooks succeeded. It calls _reset_collectors() itself, so the ID only moves to the child's once a reset has returned. If that reset raises too, the mismatch stays and the child's first flush resets instead.

Covered by test_runtime_metrics_microvm_fork_resets_collectors_for_child_id and test_runtime_metrics_microvm_failed_fork_reset_keeps_mismatch.

@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: bfba2dbfa1

ℹ️ 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 +221 to +225
if get_runtime_id() != runtime_id:
# The ID rotated while the tags were collected. Don't pair new-ID tags with old
# baselines; the next flush resets.
return
self._flush_unlocked(self._runtime_metrics)

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 Synchronize identity rotation through metric collection

When /run refresh begins after this second ID check but before _flush_unlocked() finishes iterating the collectors, _refresh_runtime_id() immediately publishes the new ID and only then invokes this worker's callback, which blocks on _identity_refresh_lock. The flush can therefore collect post-rotation CPU/GC activity and send it with the cached old runtime-id before allowing the reset. The fresh evidence is that the newly added check still precedes both collector iteration and sending, while identity rotation itself does not acquire this lock; synchronize the rotation boundary or collect and revalidate the complete batch before sending.

Useful? React with 👍 / 👎.

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.

Not changing this one. The race is real: _refresh_runtime_id() doesn't take _identity_refresh_lock, so the ID can rotate while _flush_unlocked() iterates the collectors. But that batch covers the whole interval since the last flush, and only the last few microseconds of it can come after the rotation. The old runtime-id is the right tag for almost all of it.

Closing the gap fully means one of two things. The coordinator could take this lock, but runtime_metrics already imports from _runtime_id, so that would create an import cycle. Or the flush could collect, recheck the ID, and drop the batch on a mismatch, which throws away a full interval of correctly tagged data to avoid mislabeling a few microseconds. Neither seems worth it.

@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: 86bb4e3246

ℹ️ 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 ddtrace/trace/__init__.py
from ddtrace.internal._runtime_id import on_runtime_identity_refresh

on_runtime_identity_refresh(tracer._refresh_runtime_identity)
core.on(WEB_REQUEST_STARTING_EVENT, maybe_refresh_identity)

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 Dispatch the ASGI refresh before extracting headers

When the first MicroVM /run request is handled through ASGI and carries distributed-tracing headers, this listener fires from asgi/middleware.py:279, after activate_distributed_headers() has already activated the incoming context at lines 268–271. The resulting tracer refresh then clears that active Context in _detach_stale_context() (tracer.py:472–475), so the request span created immediately afterward loses its upstream parent. Move the ASGI request-start dispatch ahead of distributed-header activation, matching the existing WSGI ordering and the tracer refresh invariant.

Useful? React with 👍 / 👎.

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.

The request span didn't actually lose its parent. _start_span runs activate_distributed_headers again from distributed_headers, and because the refresh had just detached the context, it activated the extracted parent a second time.

The ordering was still wrong, though. The headers were activated, detached, and activated again, so http.activate_distributed_headers and distributed_context.activated fired twice. Fixed in #19939 (174c28b): the ASGI dispatch now runs before header extraction and activation, the same as WSGI. #19820 adds an end-to-end test that sends distributed headers on /run and checks the parent is kept and the context is activated once.

@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: 300990a307

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

# would otherwise mix pre- and post-refresh activity into one sample. Only register this
# for MicroVMs tagging runtime-id: everyone else keeps a single identity for their
# lifetime and pays nothing extra.
self._identity_refresh_enabled = in_aws_lambda_microvm() and config._runtime_metrics_runtime_id_enabled

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 Reset collectors when runtime-ID tags are disabled

In an AWS Lambda MicroVM with runtime metrics enabled but the runtime-ID tag left at its default disabled value, this condition prevents both callback registration and the ID-mismatch recovery path. Each restored instance therefore retains the snapshot's CPU, context-switch, and GC baselines, so its first interval includes and can duplicate pre-snapshot activity—the behavior this change is intended to prevent. Collector resets should follow the MicroVM identity transition regardless of whether that identity is emitted as a metric tag.

Useful? React with 👍 / 👎.

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.

Out of scope here. This stack exists to make sure the runtime ID is refreshed on the MicroVM /run transition. The collector reset is gated on runtime-ID tagging because that's the only mode where pre-refresh baselines could go out under a runtime-id they don't belong to. With tagging off there's no runtime-id on the metrics, so nothing gets attributed to the wrong identity. Baselines carried over from the snapshot in that mode are a general snapshot/restore issue that predates this change, and would need their own fix.

Comment on lines +49 to +51
def reset(self) -> None:
for collector in self._collectors:
collector.reset()

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 Avoid re-resetting collectors after a partial failure

If a later collector fails during this loop—for example, the native process-metrics read fails after the GC collector reset succeeds—the worker deliberately leaves its runtime ID mismatched and retries the entire loop on every flush. Each retry clears the already-repaired GC collector again, discarding valid post-refresh collections and pause data until the native collector recovers; track reset completion per collector or otherwise avoid rerunning successful resets.

Useful? React with 👍 / 👎.

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.

Leaving this as is. While the ID is mismatched, flush() sends nothing, so the GC data a retry discards was never going to be reported anyway. And when the retry finally succeeds, both collectors reseed at the same moment, so their first post-refresh intervals line up. Tracking resets per collector would keep the GC data from the retry window, but GC's first interval would then be longer than the native collector's.

A native collector that keeps failing isn't new either. ValueCollector.collect() doesn't catch exceptions, so a persistent native.process_metrics() failure already breaks flush() without this change.

@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: d81c45c3c7

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

# assume they succeeded: if this raises too, the mismatch stays and the child's first flush
# retries. Runs after _runtime_id's fork hook (registered at import), so this reads the
# child's ID.
self._reset_collectors(get_runtime_id())

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 Defer collector reset out of the child fork hook

When runtime-ID metrics are enabled in a MicroVM process that forks, this after-child hook calls NativeProcessMetricCollector.reset(), whose native.process_metrics() implementation reads /proc/self/stat, /proc/self/status, and /proc/self/statm. The internal fork-safety guide explicitly prohibits I/O in after-child hooks because it can delay child readiness; leave the collector ID mismatched and perform this reseed during the first post-fork flush instead.

AGENTS.md reference: AGENTS.md:L139-L140

Useful? React with 👍 / 👎.

@litianningdatadog litianningdatadog Oct 12, 2026 •

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.

Leaving this as-is. The I/O isn't new. On main, NativeProcessMetricCollector already registers _reset_state as a fork hook, and it reads the same /proc/self/* files through native.process_metrics(). _on_fork repeats that reset as a backstop, because forksafe logs and skips a hook that raises (earlier thread).

These are procfs reads, not network I/O, so they don't hold up the child the way a flush would. Moving the reset to the first flush would cost one interval of metrics in every forked child.

Replied with the help from Claude Code

Comment on lines +114 to +116
if self._identity_refresh_enabled:
on_runtime_identity_refresh(self._on_identity_refresh)
forksafe.register(self._on_fork)

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 Unregister refresh hooks when worker startup fails

If PeriodicThread.start() raises, such as from thread-resource exhaustion, RuntimeWorker.enable() exits before assigning _instance and never calls stop(), but these registrations retain the unstarted worker indefinitely. A later identity refresh or fork will invoke that dead worker, and each retry of enable() accumulates another callback and fork hook; register only after successful startup or unwind both registrations on the failure path.

Useful? React with 👍 / 👎.

@litianningdatadog litianningdatadog Oct 12, 2026 •

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.

Not changing this here. RuntimeMetrics() already registers the collectors' fork hooks in __init__, and the GC collector acquires the shared pause monitor there too. So a failed start() leaks all of that on main today, and these two registrations follow the same pattern. It only happens when the process can't start a thread.

If startup cleanup is worth doing, it should cover the collectors as well, in its own PR.

Replied with the help from Claude Code

When runtime-ID tagging is enabled, recollect platform tags during the
periodic runtime-metrics flush instead of keeping the pre-refresh list, so
an AWS Lambda MicroVM identity refresh is reflected on the next flush.
Platform tags stay cached when runtime-ID tagging is disabled.

A MicroVM /run transition also rotates the runtime ID mid-process without
a fork, which left the CPU time, context-switch, wall-clock, and GC pause
collectors mid-interval: their next sample would mix pre- and post-refresh
activity, or (for a fresh process) reports the whole process lifetime as
one delta. Reset those collectors' baselines on the existing runtime
identity refresh callback, registered only for MicroVM processes with
runtime-ID tagging enabled. Non-MicroVM processes and MicroVMs without
runtime-ID tagging register nothing and are unaffected.

This does not add another identity-refresh callback beyond the existing
one, and does not change trace, writer, or telemetry-worker lifecycle
behavior.

This is the top of the identity refresh stack, so it also registers the
/run listener next to the tracer refresh callback in
ddtrace/trace/__init__.py. RuntimeWorker uses its own lock to keep
collector resets atomic with flush(), and skips a flush whose ID rotated
during tag collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aws-microvm Work related to AWS MicroVM onboarding changelog/no-changelog A changelog entry is not required for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants