1use libdd_trace_protobuf::opentelemetry::proto as otel_proto;
4use std::default::Default;
5
6#[derive(serde::Serialize, Debug, PartialEq, Eq, Hash)]
8pub struct TracerMetadata {
9 pub schema_version: u8,
11 #[serde(skip_serializing_if = "Option::is_none")]
13 pub runtime_id: Option<String>,
14 pub tracer_language: String,
17 pub tracer_version: String,
19 pub hostname: String,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub service_name: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub service_env: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub service_version: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub process_tags: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub container_id: Option<String>,
36 #[cfg(feature = "otel-thread-ctx")]
49 #[serde(skip)]
50 pub threadlocal_attribute_keys: Option<Vec<String>>,
51}
52
53impl Default for TracerMetadata {
54 fn default() -> Self {
55 TracerMetadata {
56 schema_version: 2,
57 runtime_id: None,
58 tracer_language: String::new(),
59 tracer_version: String::new(),
60 hostname: String::new(),
61 service_name: None,
62 service_env: None,
63 service_version: None,
64 process_tags: None,
65 container_id: None,
66 #[cfg(feature = "otel-thread-ctx")]
67 threadlocal_attribute_keys: None,
68 }
69 }
70}
71
72impl TracerMetadata {
73 const OTEL_SDK_NAME: &str = "libdatadog";
75
76 pub fn to_otel_process_ctx(&self) -> otel_proto::common::v1::ProcessContext {
77 #[cfg(feature = "otel-thread-ctx")]
78 use otel_proto::common::v1::ArrayValue;
79 use otel_proto::{
80 common::v1::{any_value, AnyValue, KeyValue, ProcessContext},
81 resource::v1::Resource,
82 };
83
84 fn key_value(key: &'static str, val: String) -> KeyValue {
85 KeyValue {
86 key: key.to_owned(),
87 value: Some(AnyValue {
88 value: Some(any_value::Value::StringValue(val)),
89 }),
90 key_ref: 0,
91 }
92 }
93
94 fn key_value_opt(key: &'static str, val: &Option<String>) -> KeyValue {
97 key_value(key, val.as_ref().cloned().unwrap_or_default())
98 }
99
100 let TracerMetadata {
103 schema_version: _,
105 runtime_id,
106 tracer_language,
107 tracer_version,
108 hostname,
109 service_name,
110 service_env,
111 service_version,
112 process_tags,
113 container_id,
114 #[cfg(feature = "otel-thread-ctx")]
115 threadlocal_attribute_keys,
116 } = self;
117
118 #[cfg_attr(not(feature = "otel-thread-ctx"), allow(unused_mut))]
119 let mut attributes = vec![
120 key_value_opt("service.name", service_name),
121 key_value_opt("service.instance.id", runtime_id),
122 key_value_opt("service.version", service_version),
123 key_value_opt("deployment.environment.name", service_env),
124 key_value("telemetry.sdk.language", tracer_language.clone()),
125 key_value("telemetry.sdk.version", tracer_version.clone()),
126 key_value("telemetry.sdk.name", Self::OTEL_SDK_NAME.to_owned()),
127 key_value("host.name", hostname.clone()),
128 key_value_opt("container.id", container_id),
129 ];
130
131 #[cfg(feature = "otel-thread-ctx")]
132 if let Some(threadlocal_attribute_keys) = threadlocal_attribute_keys.as_ref() {
133 attributes.push(key_value(
134 "threadlocal.schema_version",
135 "tlsdesc_v1_dev".to_owned(),
136 ));
137
138 attributes.push(KeyValue {
139 key: "threadlocal.attribute_key_map".to_owned(),
140 value: Some(AnyValue {
141 value: Some(any_value::Value::ArrayValue(ArrayValue {
142 values: std::iter::once(AnyValue {
143 value: Some(any_value::Value::StringValue(
144 "datadog.local_root_span_id".to_owned(),
145 )),
146 })
147 .chain(threadlocal_attribute_keys.iter().map(|k| AnyValue {
148 value: Some(any_value::Value::StringValue(k.clone())),
149 }))
150 .collect(),
151 })),
152 }),
153 key_ref: 0,
154 });
155 }
156
157 ProcessContext {
158 resource: Some(Resource {
159 attributes,
160 dropped_attributes_count: 0,
161 entity_refs: vec![],
162 }),
163 extra_attributes: vec![key_value_opt("datadog.process_tags", process_tags)],
164 }
165 }
166}
167
168pub enum AnonymousFileHandle {
169 #[cfg(target_os = "linux")]
170 Linux(memfd::Memfd),
171 #[cfg(not(target_os = "linux"))]
172 Other(()),
173}
174
175#[cfg(target_os = "linux")]
176mod linux {
177 use anyhow::Context;
178 use rand::distributions::Alphanumeric;
179 use rand::Rng;
180 use std::io::Write;
181
182 pub fn store_tracer_metadata(
185 data: &super::TracerMetadata,
186 ) -> anyhow::Result<super::AnonymousFileHandle> {
187 let _ = crate::otel_process_ctx::linux::publish(&data.to_otel_process_ctx());
188
189 let uid: String = rand::thread_rng()
190 .sample_iter(&Alphanumeric)
191 .take(8)
192 .map(char::from)
193 .collect();
194
195 let mfd_name: String = format!("datadog-tracer-info-{uid}");
196
197 let mfd = memfd::MemfdOptions::default()
198 .close_on_exec(true)
199 .allow_sealing(true)
200 .create::<&str>(mfd_name.as_ref())
201 .context("unable to create memfd")?;
202
203 let buf = rmp_serde::to_vec_named(data).context("failed serialization")?;
204 mfd.as_file()
205 .write_all(&buf)
206 .context("unable to write into memfd")?;
207
208 mfd.add_seals(&[
209 memfd::FileSeal::SealShrink,
210 memfd::FileSeal::SealGrow,
211 memfd::FileSeal::SealSeal,
212 ])
213 .context("unable to seal memfd")?;
214
215 Ok(super::AnonymousFileHandle::Linux(mfd))
216 }
217}
218
219#[cfg(not(target_os = "linux"))]
220mod other {
221 pub fn store_tracer_metadata(
222 _data: &super::TracerMetadata,
223 ) -> anyhow::Result<super::AnonymousFileHandle> {
224 Ok(super::AnonymousFileHandle::Other(()))
225 }
226}
227
228#[cfg(target_os = "linux")]
229pub use linux::*;
230#[cfg(not(target_os = "linux"))]
231pub use other::*;
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 #[cfg(feature = "otel-thread-ctx")]
237 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value;
238 use libdd_trace_protobuf::opentelemetry::proto::common::v1::{AnyValue, ProcessContext};
239
240 fn find_attr<'a>(ctx: &'a ProcessContext, key: &str) -> Option<&'a AnyValue> {
241 ctx.resource
242 .as_ref()?
243 .attributes
244 .iter()
245 .find(|kv| kv.key == key)?
246 .value
247 .as_ref()
248 }
249
250 #[test]
251 fn tracer_metadata_equality() {
252 let a = TracerMetadata {
253 tracer_language: "python".into(),
254 ..Default::default()
255 };
256 let b = TracerMetadata {
257 tracer_language: "python".into(),
258 ..Default::default()
259 };
260 let c = TracerMetadata {
261 tracer_language: "ruby".into(),
262 ..Default::default()
263 };
264
265 assert_eq!(a, b);
266 assert_ne!(a, c);
267 }
268
269 #[test]
270 fn threadlocal_attrs_absent_when_keys_empty() {
271 let ctx = TracerMetadata::default().to_otel_process_ctx();
272
273 assert!(find_attr(&ctx, "threadlocal.schema_version").is_none());
274 assert!(find_attr(&ctx, "threadlocal.attribute_key_map").is_none());
275 }
276
277 #[cfg(feature = "otel-thread-ctx")]
278 #[test]
279 fn threadlocal_attrs_present_with_correct_values() {
280 let ctx = TracerMetadata {
281 threadlocal_attribute_keys: Some(vec![
282 "span.id".to_owned(),
283 "trace.id".to_owned(),
284 "custom.key".to_owned(),
285 ]),
286 ..Default::default()
287 }
288 .to_otel_process_ctx();
289
290 let schema_version = find_attr(&ctx, "threadlocal.schema_version")
292 .expect("threadlocal.schema_version should be present");
293 assert_eq!(
294 schema_version.value,
295 Some(any_value::Value::StringValue("tlsdesc_v1_dev".to_owned()))
296 );
297
298 let key_map = find_attr(&ctx, "threadlocal.attribute_key_map")
300 .expect("threadlocal.attribute_key_map should be present");
301 let array = match &key_map.value {
302 Some(any_value::Value::ArrayValue(a)) => a,
303 other => panic!("expected ArrayValue, got {:?}", other),
304 };
305 let keys: Vec<&str> = array
306 .values
307 .iter()
308 .map(|v| match &v.value {
309 Some(any_value::Value::StringValue(s)) => s.as_str(),
310 other => panic!("expected StringValue, got {:?}", other),
311 })
312 .collect();
313 assert_eq!(
314 keys,
315 [
316 "datadog.local_root_span_id",
317 "span.id",
318 "trace.id",
319 "custom.key"
320 ]
321 );
322 }
323}