Skip to content

feat(library-config)!: caller-supplied threadlocal schema and extra process-context attributes#2162

Open
szegedi wants to merge 5 commits into
mainfrom
szegedi/threadlocal-schema-and-extras
Open

feat(library-config)!: caller-supplied threadlocal schema and extra process-context attributes#2162
szegedi wants to merge 5 commits into
mainfrom
szegedi/threadlocal-schema-and-extras

Conversation

@szegedi

@szegedi szegedi commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

TracerMetadata::to_otel_process_ctx used to hardcode threadlocal.schema_version = "tlsdesc_v1_dev" and offered no way to publish additional threadlocal.* attributes. A language-specific writer (e.g. a Node.js writer with its own V8 layout constants under a different schema) couldn't fully drive the process context through this path.

This PR introduces a ThreadLocalMetadata substruct that gathers all thread-local context configuration in one place, and replaces the existing threadlocal_attribute_keys field on TracerMetadata with a single Option<ThreadLocalMetadata> threadlocal_metadata:

#[cfg(feature = "otel-thread-ctx")]
pub struct ThreadLocalMetadata {
    pub attribute_keys: Vec<String>,
    pub schema_version: Option<String>,
    pub extra_attributes: Vec<(String, any_value::Value)>,
}
  • attribute_keys — same role as the previous threadlocal_attribute_keys. The first key is still implicitly datadog.local_root_span_id.
  • schema_version — value of threadlocal.schema_version published in the process context. Falls back to "tlsdesc_v1_dev" when None, preserving the behavior current callers rely on.
  • extra_attributes — additional KeyValues to publish in the process context, e.g. the V8 threadlocal.wrapped_object_offset / threadlocal.tagged_size layout constants the Node.js reader needs. Values are typed directly as any_value::Value, so callers can use any of the OTel-supported variants (string, int, bool, double, bytes, array, kvlist).

Option<ThreadLocalMetadata> is the single switch for whether the threadlocal.* section appears at all. Illegal states like "schema_version set but no key map" are unrepresentable by construction.

Backward compatibility

Adding fields to ThreadLocalMetadata (rather than directly to TracerMetadata) means future extensions don't break exhaustive struct literals at call sites. Existing threadlocal_attribute_keys: Some(vec![...]) callers migrate to threadlocal_metadata: Some(ThreadLocalMetadata { attribute_keys: vec![...], ..Default::default() }) — that's a breaking change, hence the ! in the PR title, but in practice the only in-tree consumer is libdatadog-nodejs.

Also dropped Eq, Hash from TracerMetadata's derive: any_value::Value contains f64 (no Eq/Hash) and arrays (no Hash). Nothing in-tree puts TracerMetadata in a hash-keyed collection; PartialEq is retained.

Test plan

  • cargo test -p libdd-library-config --features otel-thread-ctx
  • Wire up ThreadLocalMetadata in libdatadog-nodejs's process_discovery crate
  • Have dd-trace-js pass schema "nodejs_v1_dev" + V8 layout constants through, dropping its own ad-hoc handling

@datadog-prod-us1-4

datadog-prod-us1-4 Bot commented Jun 25, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 73.97% (+0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 6e826b1 | Docs | Datadog PR Page | Give us feedback!

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation Check Results

⚠️ 159 documentation warning(s) found

📦 libdd-library-config - 159 warning(s)


Updated: 2026-06-26 11:03:06 UTC | Commit: 8373909 | missing-docs job results

@github-actions

Copy link
Copy Markdown
Contributor

Clippy Allow Annotation Report

Comparing clippy allow annotations between branches:

  • Base Branch: origin/main
  • PR Branch: origin/szegedi/threadlocal-schema-and-extras

Summary by Rule

Rule Base Branch PR Branch Change

Annotation Counts by File

File Base Branch PR Branch Change

Annotation Stats by Crate

Crate Base Branch PR Branch Change
clippy-annotation-reporter 5 5 No change (0%)
datadog-ffe-ffi 1 1 No change (0%)
datadog-ipc 22 22 No change (0%)
datadog-live-debugger 4 4 No change (0%)
datadog-live-debugger-ffi 10 10 No change (0%)
datadog-profiling-replayer 4 4 No change (0%)
datadog-sidecar 45 45 No change (0%)
libdd-common 13 13 No change (0%)
libdd-common-ffi 12 12 No change (0%)
libdd-data-pipeline 6 6 No change (0%)
libdd-ddsketch 2 2 No change (0%)
libdd-dogstatsd-client 1 1 No change (0%)
libdd-profiling 13 13 No change (0%)
libdd-remote-config 3 3 No change (0%)
libdd-telemetry 20 20 No change (0%)
libdd-tinybytes 4 4 No change (0%)
libdd-trace-normalization 2 2 No change (0%)
libdd-trace-obfuscation 3 3 No change (0%)
libdd-trace-stats 1 1 No change (0%)
libdd-trace-utils 11 11 No change (0%)
Total 182 182 No change (0%)

About This Report

This report tracks Clippy allow annotations for specific rules, showing how they've changed in this PR. Decreasing the number of these annotations generally improves code quality.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🔒 Cargo Deny Results

⚠️ 1 issue(s) found, showing only errors (advisories, bans, sources)

📦 libdd-library-config - 1 error(s)

Show output
error[unsound]: Rand is unsound with a custom logger using `rand::rng()`
   ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:47:1
   │
47 │ rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index
   │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ unsound advisory detected
   │
   ├ ID: RUSTSEC-2026-0097
   ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0097
   ├ It has been reported (by @lopopolo) that the `rand` library is [unsound](https://rust-lang.github.io/unsafe-code-guidelines/glossary.html#soundness-of-code--of-a-library) (i.e. that safe code using the public API can cause Undefined Behaviour) when all the following conditions are met:
     
     - The `log` and `thread_rng` features are enabled
     - A [custom logger](https://docs.rs/log/latest/log/#implementing-a-logger) is defined
     - The custom logger accesses `rand::rng()` (previously `rand::thread_rng()`) and calls any `TryRng` (previously `RngCore`) methods on `ThreadRng`
     - The `ThreadRng` (attempts to) reseed while called from the custom logger (this happens every 64 kB of generated data)
     - Trace-level logging is enabled or warn-level logging is enabled and the random source (the `getrandom` crate) is unable to provide a new seed
     
     `TryRng` (previously `RngCore`) methods for `ThreadRng` use `unsafe` code to cast `*mut BlockRng<ReseedingCore>` to `&mut BlockRng<ReseedingCore>`. When all the above conditions are met this results in an aliased mutable reference, violating the Stacked Borrows rules. Miri is able to detect this violation in sample code. Since construction of [aliased mutable references is Undefined Behaviour](https://doc.rust-lang.org/stable/nomicon/references.html), the behaviour of optimized builds is hard to predict.
   ├ Announcement: https://github.com/rust-random/rand/pull/1763
   ├ Solution: Upgrade to >=0.10.1 OR <0.10.0, >=0.9.3 OR <0.9.0, >=0.8.6 (try `cargo update -p rand`)
   ├ rand v0.8.5
     └── libdd-library-config v2.0.0

advisories FAILED, bans ok, sources ok

Updated: 2026-06-26 11:05:14 UTC | Commit: 8373909 | dependency-check job results

@szegedi szegedi force-pushed the szegedi/threadlocal-schema-and-extras branch from c22f97e to 9543321 Compare June 25, 2026 14:37
@szegedi szegedi changed the title Allow caller to override threadlocal schema and add extra process-context attributes feat(library-config)!: support caller-supplied threadlocal schema and extra process-context attributes Jun 25, 2026
@szegedi szegedi force-pushed the szegedi/threadlocal-schema-and-extras branch from 9543321 to ce85ea7 Compare June 25, 2026 14:43
@szegedi szegedi changed the title feat(library-config)!: support caller-supplied threadlocal schema and extra process-context attributes feat(library-config)!: caller-supplied threadlocal schema and extra process-context attributes Jun 25, 2026
…ributes

The OTel process-context block emitted by TracerMetadata::to_otel_process_ctx
used to hardcode 'threadlocal.schema_version' to 'tlsdesc_v1_dev' and offered
no way to add further threadlocal.* attributes. Language-specific writers
(e.g. a Node.js writer that publishes its own V8 layout constants under a
different schema name) couldn't fully drive the process context through
this path.

Add two new fields on TracerMetadata, gated on the otel-thread-ctx feature:

- threadlocal_schema_version: Option<String> — explicit override of the
  schema attribute. Falls back to 'tlsdesc_v1_dev' when None and
  threadlocal_attribute_keys is Some, preserving backward-compatible
  behavior for existing callers.

- threadlocal_extra_attributes: Vec<(String, ProcessContextAttrValue)> —
  additional KeyValues emitted in the process context. The value type is
  a small enum (String or Int) covering the cases needed for writer
  layout / runtime metadata.

Emission of each piece is now independent: the schema attribute, the
key map, and each extra are emitted iff their source field is set.
@szegedi szegedi force-pushed the szegedi/threadlocal-schema-and-extras branch from ce85ea7 to 79d90e9 Compare June 25, 2026 14:52
@szegedi szegedi marked this pull request as ready for review June 25, 2026 15:31
@szegedi szegedi requested a review from a team as a code owner June 25, 2026 15:31
@szegedi szegedi requested review from vpellan and removed request for a team June 25, 2026 15:31
@dd-octo-sts

dd-octo-sts Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Artifact Size Benchmark Report

aarch64-alpine-linux-musl
Artifact Baseline Commit Change
/aarch64-alpine-linux-musl/lib/libdatadog_profiling.a 84.87 MB 84.87 MB +0% (+16 B) 👌
/aarch64-alpine-linux-musl/lib/libdatadog_profiling.so 7.82 MB 7.82 MB 0% (0 B) 👌
aarch64-unknown-linux-gnu
Artifact Baseline Commit Change
/aarch64-unknown-linux-gnu/lib/libdatadog_profiling.a 96.00 MB 96.00 MB +0% (+16 B) 👌
/aarch64-unknown-linux-gnu/lib/libdatadog_profiling.so 10.44 MB 10.44 MB 0% (0 B) 👌
libdatadog-x64-windows
Artifact Baseline Commit Change
/libdatadog-x64-windows/debug/dynamic/datadog_profiling_ffi.dll 25.05 MB 25.05 MB 0% (0 B) 👌
/libdatadog-x64-windows/debug/dynamic/datadog_profiling_ffi.lib 87.68 KB 87.68 KB 0% (0 B) 👌
/libdatadog-x64-windows/debug/dynamic/datadog_profiling_ffi.pdb 182.83 MB 182.81 MB -0% (-16.00 KB) 👌
/libdatadog-x64-windows/debug/static/datadog_profiling_ffi.lib 936.10 MB 936.10 MB 0% (0 B) 👌
/libdatadog-x64-windows/release/dynamic/datadog_profiling_ffi.dll 8.20 MB 8.20 MB 0% (0 B) 👌
/libdatadog-x64-windows/release/dynamic/datadog_profiling_ffi.lib 87.68 KB 87.68 KB 0% (0 B) 👌
/libdatadog-x64-windows/release/dynamic/datadog_profiling_ffi.pdb 24.23 MB 24.23 MB 0% (0 B) 👌
/libdatadog-x64-windows/release/static/datadog_profiling_ffi.lib 48.34 MB 48.34 MB 0% (0 B) 👌
libdatadog-x86-windows
Artifact Baseline Commit Change
/libdatadog-x86-windows/debug/dynamic/datadog_profiling_ffi.dll 21.71 MB 21.71 MB 0% (0 B) 👌
/libdatadog-x86-windows/debug/dynamic/datadog_profiling_ffi.lib 89.06 KB 89.06 KB 0% (0 B) 👌
/libdatadog-x86-windows/debug/dynamic/datadog_profiling_ffi.pdb 186.80 MB 186.80 MB -0% (-8.00 KB) 👌
/libdatadog-x86-windows/debug/static/datadog_profiling_ffi.lib 924.73 MB 924.73 MB 0% (0 B) 👌
/libdatadog-x86-windows/release/dynamic/datadog_profiling_ffi.dll 6.33 MB 6.33 MB 0% (0 B) 👌
/libdatadog-x86-windows/release/dynamic/datadog_profiling_ffi.lib 89.06 KB 89.06 KB 0% (0 B) 👌
/libdatadog-x86-windows/release/dynamic/datadog_profiling_ffi.pdb 25.99 MB 25.99 MB 0% (0 B) 👌
/libdatadog-x86-windows/release/static/datadog_profiling_ffi.lib 45.96 MB 45.96 MB 0% (0 B) 👌
x86_64-alpine-linux-musl
Artifact Baseline Commit Change
/x86_64-alpine-linux-musl/lib/libdatadog_profiling.a 75.64 MB 75.64 MB 0% (0 B) 👌
/x86_64-alpine-linux-musl/lib/libdatadog_profiling.so 8.68 MB 8.68 MB 0% (0 B) 👌
x86_64-unknown-linux-gnu
Artifact Baseline Commit Change
/x86_64-unknown-linux-gnu/lib/libdatadog_profiling.a 91.09 MB 91.09 MB 0% (0 B) 👌
/x86_64-unknown-linux-gnu/lib/libdatadog_profiling.so 10.57 MB 10.57 MB 0% (0 B) 👌

@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: 79d90e9755

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@@ -132,7 +171,9 @@ impl TracerMetadata {
if let Some(threadlocal_attribute_keys) = threadlocal_attribute_keys.as_ref() {

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 Emit supplied threadlocal extras without requiring a key map

When a caller sets threadlocal_schema_version or threadlocal_extra_attributes but has no additional attribute keys to publish, this guard skips the whole block and silently drops the supplied schema/layout attributes. That prevents the new caller-supplied process-context fields from working unless callers also know to set threadlocal_attribute_keys to Some(vec![]), even when they do not need a key map; gate the schema, key map, and extras independently so provided extras are actually published.

Useful? React with 👍 / 👎.

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

The motivation LGTM. I wonder though if we shouldn't gather all thread-locals related stuff in a substruct, something like:

#[cfg(feature = "otel-thread-ctx")]
ThreadLocalMetadata {
        threadlocal_attribute_keys: Vec<String>,
        threadlocal_schema_version: Option<String>,
        threadlocal_extra_attributes: Vec<(String, ProcessContextAttrValue)>,
}

And then have in TracerMetadata a single field

    #[cfg(feature = "otel-thread-ctx")]
    #[serde(skip)]
    pub threadlocal_metadata: Option<ThreadLocalMetadata>,

The idea being that this avoid illegal states that are currently possible, like threadlocal_extra_attributes or threadlocal_schema_version being defined/non-empty but with attribute_keys being empty, which results in silently not publishing a ProcessContext.

With the ThreadLocalMetadata, the single point of activation is whether threadlocal_metadata is Some or not. So it's not possible to have it halfway defined.

This would be breaking but is ok - in practice the FFI is the only one currently using this code, so it's all within libdatadog, to the best of my knowledge.

What do you think?

/// Value of an additional OTel process-context attribute. Mirrors the small subset of
/// `opentelemetry::proto::common::v1::AnyValue` variants we support for caller-supplied threadlocal
/// extras — string and 64-bit integer, since the only consumers so far are textual schema
/// identifiers and small numeric layout constants (e.g. struct offsets, pointer widths).

@yannham yannham Jun 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are we confident this will always be the case? I feel a simple approach would just to let caller set any value supported by the process context spec (and that would get rid of this additional type). Put differently, would that be a problem / redflag if libdatadog consumers passed something else than a string or an int?

It's ok to start like this and evolve in the future (although it's a breaking change), we don't have to envision all possible use cases right now. I'm asking mostly because it adds boilerplate, so wondering if it's really necessary.

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.

Fair enough, we can either go with Vec<(String, any_value::Value)> or even Vec<KeyValue> in which case the caller needs to construct KeyValue. For that reason, I'd lean towards Vec(String, any_value::Value) although admittedly I don't have enough idiomatic Rust experience to decide. Which one would you suggest?

Comment thread libdd-library-config/src/tracer_metadata.rs Outdated
Comment thread libdd-library-config/src/tracer_metadata.rs Outdated
@yannham

yannham commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

(Another solution would be to remove the Option around attribute_keys and just publish a context unconditionally)

@szegedi

szegedi commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@yannham I like the suggestion of adding a struct a lot, I'm all for eliminating illegal states. I might remove the threadlocal_ prefix from its fields though, although that's admittedly a bikeshedding:

#[cfg(feature = "otel-thread-ctx")]
ThreadLocalMetadata {
        attribute_keys: Vec<String>,
        schema_version: Option<String>,
        extra_attributes: Vec<(String, ProcessContextAttrValue)>,
}

@szegedi szegedi force-pushed the szegedi/threadlocal-schema-and-extras branch 2 times, most recently from dfe7a07 to 261edb3 Compare June 26, 2026 10:28
szegedi and others added 4 commits June 26, 2026 12:55
Co-authored-by: Yann Hamdaoui <yann.hamdaoui@datadoghq.com>
Co-authored-by: Yann Hamdaoui <yann.hamdaoui@datadoghq.com>
Per review feedback: dropping the ProcessContextAttrValue enum (String + Int) avoids the
"what about bool/double/array" extension problem and reuses the otel_proto type that's
already part of this file's surface. The struct also has to drop its Eq+Hash derive,
since any_value::Value contains f64 (no Eq) and arrays (no Hash); nothing in-tree uses
TracerMetadata as a hash-map key, and PartialEq is preserved.
Per review: a single Option<ThreadLocalMetadata> field on TracerMetadata replaces the
three loose threadlocal_* fields. The 'illegal state' of schema/extras set without a key
map is now unrepresentable by construction. Field names inside the substruct drop the
threadlocal_ prefix since the namespace lives in the type name.
@szegedi szegedi force-pushed the szegedi/threadlocal-schema-and-extras branch from 261edb3 to 6e826b1 Compare June 26, 2026 11:01
@szegedi

szegedi commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

I updated the PR with the suggestions.

For one, switched to any_value::Value for extra attributes. It does have one weird side effect, though: I had to drop Eq and Hash from TracerMetadata's derive (required since any_value::Value can also be f64 and arrays, and those don't have Eq and Hash either). No internal consumer puts TracerMetadata in a hash-keyed collection so I think this is fine? It retains PartialEq so existing equality test still works.

For another, I introduced the ThreadlocalMetadata struct; it's indeed much cleaner like this.

Apologies for force pushes after review; my clanker did that by amending into the original commit and force-pushing. I restored the original commit from reflog and then applied the changes on top again as separate commits, so even though it says a bunch of "force pushed" in the PR history, the first commit is again the exact same SHA that you originally reviewed.

@szegedi szegedi requested a review from yannham June 26, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants