Skip to main content

libdd_library_config/
tracer_metadata.rs

1// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3use libdd_trace_protobuf::opentelemetry::proto as otel_proto;
4use std::default::Default;
5
6/// This struct MUST be backward compatible.
7#[derive(serde::Serialize, Debug, PartialEq, Eq, Hash)]
8pub struct TracerMetadata {
9    /// Version of the schema.
10    pub schema_version: u8,
11    /// Runtime UUID.
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub runtime_id: Option<String>,
14    /// Programming language of the tracer library (e.g., “python”). Refers to telemetry
15    /// for valid values.
16    pub tracer_language: String,
17    /// Version of the tracer (e.g., "1.0.0").
18    pub tracer_version: String,
19    /// Identifier of the machine running the process.
20    pub hostname: String,
21    /// Name of the service being instrumented.
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub service_name: Option<String>,
24    /// Environment of the service being instrumented.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub service_env: Option<String>,
27    /// Version of the service being instrumented.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub service_version: Option<String>,
30    /// Process tags of the application being instrumented.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub process_tags: Option<String>,
33    /// Container id seen by the application.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub container_id: Option<String>,
36    /// Ordered list of attribute key names for thread-level context records. Key indices from
37    /// thread context records index into this table. Set to `None` to disable thread-level related
38    /// attributes to the process-level context.
39    ///
40    /// If set to `Some`, the first key will be automatically set to `datadog.local_root_span_id`
41    /// in the OTel process context, because the thread context handling elsewhere in libdatadog
42    /// relies on this key's index to be zero. Only set additional keys in
43    /// `threadlocal_attribute_keys`; the root span id is considered to always be here implicitly.
44    ///
45    /// This field is specific to OTel process context. It is ignored for (de)serialization, and is
46    /// only used when converting to an OTel process context in
47    /// [TracerMetadata::to_otel_process_ctx].
48    #[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    // The value of the telemetry.sdk.name field to put in the otel context resource.
74    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        // Even if there's no value, we still set the key to let the reader know that we do support
95        // and emit this specific attribute, which happens to be empty in this case.
96        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        // Every field of `self` should get propagated to the otel context.
101        // If you add a new field, please also add it here and as a key/value in the otel context.
102        let TracerMetadata {
103            // This one isn't propagated on purpose
104            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    /// Create a memfd file storing the tracer metadata. This function also attempts to publish the
183    /// tracer metadata as an OTel process context separately, but will ignore resulting errors.
184    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        // Schema version attribute
291        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        // Key map attribute: ordered array of key name strings
299        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}