Skip to content

Commit e23ce1a

Browse files
authored
feat!: expose ThreadLocalMetadata via process_discovery (#153)
* Expose ThreadLocalMetadata via process_discovery Replaces the flat `threadlocal_attribute_keys` field on the napi TracerMetadata with a `threadlocal_metadata` substruct mirroring the libdatadog Rust API. Lets callers set the schema-version string and extra process-context attributes (strings and 64-bit ints for now) alongside the attribute key map. libdd-library-config and libdd-trace-protobuf are pinned to the merge commit that introduced the new API (7cdeb7896e92d1ba38bde495934e112dac2eda25); swap back to a tagged release once one that includes it is published. This is a breaking change to the process_discovery.TracerMetadata constructor. Downstream (dd-trace-js) will need to migrate from passing threadlocalAttributeKeys directly to passing a threadlocalMetadata object.
1 parent 18b6f3f commit e23ce1a

4 files changed

Lines changed: 154 additions & 19 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/process_discovery/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ crate-type = ["cdylib", "rlib"]
88

99
[dependencies]
1010
anyhow = "1"
11-
libdd-library-config = { git = "https://github.com/DataDog/libdatadog.git", tag = "v35.0.0", features = ["otel-thread-ctx"] }
11+
# Pointed at the merge commit that introduced ThreadLocalMetadata (caller-supplied
12+
# schema version + extra process-context attributes). Swap back to a tagged release
13+
# once one that includes 7cdeb7896e92d1ba38bde495934e112dac2eda25 is published.
14+
libdd-library-config = { git = "https://github.com/DataDog/libdatadog.git", rev = "7cdeb7896e92d1ba38bde495934e112dac2eda25", features = ["otel-thread-ctx"] }
15+
libdd-trace-protobuf = { git = "https://github.com/DataDog/libdatadog.git", rev = "7cdeb7896e92d1ba38bde495934e112dac2eda25" }
1216

1317
napi = { version = "2" }
1418
napi-derive = { version = "2", default-features = false }

crates/process_discovery/src/lib.rs

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use napi::{Error, Status};
22
use napi_derive::napi;
33

44
use libdd_library_config::tracer_metadata;
5+
use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value;
56

67
#[napi]
78
pub struct NapiAnonymousFileHandle {
@@ -11,6 +12,44 @@ pub struct NapiAnonymousFileHandle {
1112
#[napi]
1213
impl NapiAnonymousFileHandle {}
1314

15+
/// Additional OTel process-context attribute the threadlocal writer wants to
16+
/// publish alongside the key map (e.g. language-runtime layout constants). Set
17+
/// exactly one of `string_value` / `int_value` — the other variants of OTel's
18+
/// `AnyValue` (bool, double, bytes, array, kvlist) are not yet exposed.
19+
/// Passing both set or neither set is rejected as invalid input.
20+
#[derive(Clone)]
21+
#[napi(object)]
22+
pub struct ExtraAttribute {
23+
pub key: String,
24+
pub string_value: Option<String>,
25+
pub int_value: Option<i64>,
26+
}
27+
28+
/// Thread-level context metadata the tracer wants to publish as part of the
29+
/// OTel process context. When present on a [`TracerMetadata`], drives the
30+
/// `threadlocal.*` block in the emitted process context; when absent, no such
31+
/// block is emitted.
32+
#[derive(Clone)]
33+
#[napi(object)]
34+
pub struct ThreadLocalMetadata {
35+
/// Ordered list of attribute key names for thread-level OTEP-4947 context
36+
/// records. Wire key indices index into this list. libdatadog implicitly
37+
/// prepends `datadog.local_root_span_id` at wire index 0, so entry 0 here
38+
/// is wire key index 1.
39+
pub attribute_keys: Vec<String>,
40+
41+
/// Value of the `threadlocal.schema_version` attribute. Identifies the
42+
/// on-the-wire record schema (e.g. `"tlsdesc_v1_dev"` for libdatadog's own
43+
/// TLSDESC writer, `"nodejs_v1_dev"` for a Node.js writer). Defaults to
44+
/// `"tlsdesc_v1_dev"` when omitted.
45+
pub schema_version: Option<String>,
46+
47+
/// Extra `threadlocal.*` attributes to publish alongside the key map (e.g.
48+
/// V8 layout constants a Node.js reader needs to walk from the discovery
49+
/// TLS symbol into the record).
50+
pub extra_attributes: Vec<ExtraAttribute>,
51+
}
52+
1453
#[napi(constructor)]
1554
pub struct TracerMetadata {
1655
pub runtime_id: Option<String>,
@@ -21,16 +60,50 @@ pub struct TracerMetadata {
2160
pub service_version: Option<String>,
2261
pub process_tags: Option<String>,
2362
pub container_id: Option<String>,
24-
/// Ordered list of attribute key names for thread-level OTEP-4947
25-
/// context records. Key indices on the wire index into this list.
26-
/// libdatadog's OTel process-context conversion prepends the
27-
/// implicit `datadog.local_root_span_id` entry at wire index 0, so
28-
/// callers should only set their additional keys here — entry 0 in
29-
/// this list corresponds to wire key index 1.
30-
///
31-
/// `null`/omitted (the default) disables the thread-context-related
32-
/// attributes in the OTel process context entirely.
33-
pub threadlocal_attribute_keys: Option<Vec<String>>,
63+
/// Optional thread-level context metadata; see [`ThreadLocalMetadata`].
64+
/// `null`/omitted (the default) disables the `threadlocal.*` block in the
65+
/// emitted OTel process context entirely.
66+
pub threadlocal_metadata: Option<ThreadLocalMetadata>,
67+
}
68+
69+
fn convert_extra_attribute(ea: &ExtraAttribute) -> napi::Result<(String, any_value::Value)> {
70+
let value = match (&ea.string_value, ea.int_value) {
71+
(Some(s), None) => any_value::Value::StringValue(s.clone()),
72+
(None, Some(i)) => any_value::Value::IntValue(i),
73+
(Some(_), Some(_)) => {
74+
return Err(Error::new(
75+
Status::InvalidArg,
76+
format!(
77+
"ExtraAttribute {:?}: exactly one of stringValue / intValue must be set, both are",
78+
ea.key,
79+
),
80+
));
81+
}
82+
(None, None) => {
83+
return Err(Error::new(
84+
Status::InvalidArg,
85+
format!(
86+
"ExtraAttribute {:?}: exactly one of stringValue / intValue must be set, neither is",
87+
ea.key,
88+
),
89+
));
90+
}
91+
};
92+
Ok((ea.key.clone(), value))
93+
}
94+
95+
fn convert_threadlocal_metadata(
96+
tlm: &ThreadLocalMetadata,
97+
) -> napi::Result<tracer_metadata::ThreadLocalMetadata> {
98+
Ok(tracer_metadata::ThreadLocalMetadata {
99+
attribute_keys: tlm.attribute_keys.clone(),
100+
schema_version: tlm.schema_version.clone(),
101+
extra_attributes: tlm
102+
.extra_attributes
103+
.iter()
104+
.map(convert_extra_attribute)
105+
.collect::<napi::Result<_>>()?,
106+
})
34107
}
35108

36109
#[napi]
@@ -46,7 +119,11 @@ pub fn store_metadata(data: &TracerMetadata) -> napi::Result<NapiAnonymousFileHa
46119
service_version: data.service_version.clone(),
47120
process_tags: data.process_tags.clone(),
48121
container_id: data.container_id.clone(),
49-
threadlocal_attribute_keys: data.threadlocal_attribute_keys.clone(),
122+
threadlocal_metadata: data
123+
.threadlocal_metadata
124+
.as_ref()
125+
.map(convert_threadlocal_metadata)
126+
.transpose()?,
50127
});
51128

52129
match res {

test/process-discovery.js

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ const metadata = new process_discovery.TracerMetadata(
2222
const cfg_handle = process_discovery.storeMetadata(metadata)
2323
assert(cfg_handle !== undefined)
2424

25-
// Same shape, plus a thread-local attribute key map (OTEP-4947). libdatadog
26-
// implicitly prepends `datadog.local_root_span_id` at wire index 0; entries
27-
// here start at wire index 1.
25+
// Same shape, plus a thread-local metadata block (OTEP-4947). libdatadog
26+
// implicitly prepends `datadog.local_root_span_id` at wire index 0 in the
27+
// attribute key map; entries here start at wire index 1. `schemaVersion` and
28+
// `extraAttributes` describe the on-the-wire record schema for readers.
2829
const metadata_with_threadlocal = new process_discovery.TracerMetadata(
2930
'7938685c-19dd-490f-b9b3-8aae4c22f898',
3031
'1.0.0',
@@ -34,15 +35,67 @@ const metadata_with_threadlocal = new process_discovery.TracerMetadata(
3435
'my_version',
3536
undefined,
3637
undefined,
37-
['endpoint', 'http.status'],
38+
{
39+
attributeKeys: ['endpoint', 'http.status'],
40+
schemaVersion: 'nodejs_v1_dev',
41+
extraAttributes: [
42+
{ key: 'threadlocal.wrapped_object_offset', intValue: 24 },
43+
{ key: 'threadlocal.tagged_size', intValue: 8 },
44+
{ key: 'threadlocal.runtime.name', stringValue: 'nodejs' },
45+
],
46+
},
3847
)
3948
assert.deepStrictEqual(
40-
metadata_with_threadlocal.threadlocalAttributeKeys,
49+
metadata_with_threadlocal.threadlocalMetadata.attributeKeys,
4150
['endpoint', 'http.status'],
4251
)
52+
assert.strictEqual(
53+
metadata_with_threadlocal.threadlocalMetadata.schemaVersion,
54+
'nodejs_v1_dev',
55+
)
56+
assert.strictEqual(
57+
metadata_with_threadlocal.threadlocalMetadata.extraAttributes.length,
58+
3,
59+
)
4360
const cfg_handle_threadlocal = process_discovery.storeMetadata(metadata_with_threadlocal)
4461
assert(cfg_handle_threadlocal !== undefined)
4562

63+
// An ExtraAttribute with neither stringValue nor intValue set is a caller
64+
// error — one of them has to be picked.
65+
const bad_metadata_neither = new process_discovery.TracerMetadata(
66+
'7938685c-19dd-490f-b9b3-8aae4c22f899',
67+
'1.0.0',
68+
'my_hostname',
69+
undefined, undefined, undefined, undefined, undefined,
70+
{
71+
attributeKeys: [],
72+
schemaVersion: undefined,
73+
extraAttributes: [{ key: 'threadlocal.bogus' }],
74+
},
75+
)
76+
assert.throws(
77+
() => process_discovery.storeMetadata(bad_metadata_neither),
78+
/neither is/,
79+
)
80+
81+
// Setting both stringValue and intValue is also a caller error — the intent
82+
// is ambiguous, so reject.
83+
const bad_metadata_both = new process_discovery.TracerMetadata(
84+
'7938685c-19dd-490f-b9b3-8aae4c22f89a',
85+
'1.0.0',
86+
'my_hostname',
87+
undefined, undefined, undefined, undefined, undefined,
88+
{
89+
attributeKeys: [],
90+
schemaVersion: undefined,
91+
extraAttributes: [{ key: 'threadlocal.bogus', stringValue: 's', intValue: 1 }],
92+
},
93+
)
94+
assert.throws(
95+
() => process_discovery.storeMetadata(bad_metadata_both),
96+
/both are/,
97+
)
98+
4699
if (process.platform === 'linux') {
47100
const contains_datadog_memfd = (fds) => {
48101
for (const fd in fds) {

0 commit comments

Comments
 (0)