Skip to content

chore(telemetry): add fork-child identity, lifecycle and replay controls to the native worker - #21615

Draft
VianneyRuhlmann wants to merge 1 commit into
mainfrom
vianney/telemetry-retained-worker-fork-controls-efeca671
Draft

VianneyRuhlmann wants to merge 1 commit into
mainfrom
vianney/telemetry-retained-worker-fork-controls-efeca671

Conversation

@VianneyRuhlmann

Copy link
Copy Markdown
Contributor

Description

The native TelemetryWorker sets its runtime/session identity and lifecycle behavior only at construction. A forked process that wants to keep the existing worker therefore cannot switch it to the child's identity or prevent it from sending lifecycle events. This PR adds controls a forked child can apply before the worker restarts. None of them restart the runtime, so they also work while the child restart is pending or deferred:

  • set_identity(runtime_id, session_id, parent_session_id=None, root_session_id=None): identity now goes through libdatadog's mutable metadata. Every later request uses the new runtime id and the dd-session-id, dd-parent-session-id and dd-root-session-id headers.
  • set_app_lifecycle(*, app_started, app_closing): turns app-started / app-closing on or off for an already-built worker. emit_app_lifecycle remains the initial value. Per the telemetry API, forked children must not send app-started. They also skip app-closing, which closes the lifecycle app-started opened. They still heartbeat under their own identity.
  • stage_fork_replay(configurations, integrations=(), products=()): the fork reset clears inherited configurations, integrations, products and dependencies, along with their dedupe history. A small worker wrapper queues staged items right after that reset, before the worker restarts. The replay therefore cannot race Python threads on configuration seq_ids. Staged items are consumed once.

This PR only adds and tests the controls. The fork hooks still rebuild the worker as before. Switching them to keep the worker comes in a follow-up.

Testing

  • tests/telemetry/test_native_worker_fork_controls.py (new): subprocess tests against the test agent. Each test drives a standalone worker and simulates the child sequence with the runtime fork hooks (before_fork → controls → after_fork_child).
    • The child sends with its own runtime id, session headers and seq_id 1, and sends no app-started or app-closing.
    • It still heartbeats.
    • The replayed config arrives as app-client-configuration-change.
    • A dependency the parent already reported is reported again under the child.
    • When the parent never started its worker, the child's start() sends no app-started.
    • The controls work while defer_after_fork_child() is active.
  • Rust unit tests in src/native/telemetry.rs cover the wrapper. Staged items are queued after a child reset and consumed once. Nothing is injected when nothing is staged or the handle is gone. Parent resume leaves staged items untouched.
  • Local only: the build used libdatadog v48.0.0 with the libdatadog change below applied.
    • New tests: 3 Python tests pass via scripts/run-tests -r on Python 3.12, and 4 Rust tests pass via cargo nextest.
    • Existing tests: tests/telemetry/test_telemetry.py and test_writer.py (135) still pass.
    • Static checks: cargo clippy --all-features -- -D warnings, cargo +nightly fmt and scripts/lint checks are clean.

Risks

  • set_app_lifecycle depends on new libdatadog API (feat(telemetry): allow toggling app-started/app-closing on a built worker libdatadog#2837). Until a libdatadog release containing it is pinned here, this branch does not build against the current v48.0.0 pin, and CI is expected to fail. The PR stays in draft until the libdatadog version bump lands, and it must not merge on a non-tag pin.
  • No benchmarks were run: the metric hot paths are unchanged, and the libdatadog side only adds one atomic load when the worker starts and stops.

Additional Notes

  • No release note (changelog/no-changelog): this adds internal native APIs with no user-visible behavior change yet.
  • stop(send_app_closing) still ignores its argument. Stopping without app-closing is now done through set_app_lifecycle.

…ols to the native worker

Expose set_identity, set_app_lifecycle and stage_fork_replay on the native
TelemetryWorker so a forked child can switch to its own runtime/session
identity, suppress app-started/app-closing, and replay its configuration,
integrations and products right after the worker's fork reset, before the
worker restarts. None of these restart the runtime, so they can be applied
while the child restart is pending or deferred.

set_app_lifecycle requires the libdatadog TelemetryWorkerHandle lifecycle
switches (DataDog/libdatadog#2837).
@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Codeowners resolved as

Resolved from the full PR diff against main using the target branch CODEOWNERS file.
CODEOWNERS team requests not listed below are not required by the current file set.

ddtrace/internal/native/_native.pyi                                     @DataDog/apm-core-python
src/native/Cargo.lock                                                   @DataDog/apm-core-python
src/native/Cargo.toml                                                   @DataDog/apm-core-python
src/native/telemetry.rs                                                 @DataDog/apm-core-python
tests/telemetry/test_native_worker_fork_controls.py                     @DataDog/apm-core-python @DataDog/apm-python

@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Dependency direction analysis

⚠️ Existing dependency direction violations

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

Show existing violations (showing 5 of 83 highest severity)
ddtrace.internal.tracemethods -×-> ddtrace.trace  (internal-core -> product:tracing, score=120)
ddtrace.llmobs._utils -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=118)
ddtrace.llmobs._integrations.crewai -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=118)
ddtrace.llmobs._integrations.litellm -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=118)
ddtrace.llmobs._integrations.bedrock_agents -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=118)

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

@datadog-prod-us1-4

datadog-prod-us1-4 Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Pipelines

✨ Unblock PR with BitsAI

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 205 Pipeline jobs failed

Build | build_wheels / Build cp311-win_arm64 — 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

Build | build_wheels / Build cp312-win_arm64 — 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

Build | build_wheels / Build cp313-win_arm64 — 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

View all 205 failed jobs.

Useful? React with 👍 / 👎

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

@VianneyRuhlmann VianneyRuhlmann added the changelog/no-changelog A changelog entry is not required for this PR. label Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

1 participant