Skip to main content

dcontext_tracing/
field_mapping.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use tracing_core::field::{Field, Visit};
5
6/// A mapping from a tracing span field to a dcontext key.
7///
8/// When a span with the configured field is entered, the field value
9/// is extracted and set as a dcontext context value.
10pub(crate) struct FieldMapping {
11    pub field_name: &'static str,
12    pub context_key: &'static str,
13    pub setter: Box<dyn FieldSetter>,
14}
15
16/// Trait for setting a dcontext value from a tracing field value.
17pub(crate) trait FieldSetter: Send + Sync {
18    /// Try to set the context value from a string representation.
19    fn set_from_str(&self, key: &'static str, value: &str);
20    /// Try to set the context value from a u64.
21    fn set_from_u64(&self, key: &'static str, value: u64);
22    /// Try to set the context value from an i64.
23    fn set_from_i64(&self, key: &'static str, value: i64);
24    /// Try to set the context value from a bool.
25    fn set_from_bool(&self, key: &'static str, value: bool);
26}
27
28/// Trait for types that can be constructed from tracing field values.
29///
30/// Implement this for your context types to enable automatic
31/// field-to-context mapping.
32///
33/// # Example
34///
35/// ```rust
36/// use dcontext_tracing::FromFieldValue;
37///
38/// #[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
39/// struct RequestId(String);
40///
41/// impl FromFieldValue for RequestId {
42///     fn from_str_value(s: &str) -> Option<Self> {
43///         Some(RequestId(s.to_string()))
44///     }
45/// }
46/// ```
47pub trait FromFieldValue: Clone + Default + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static {
48    /// Construct from a string value. Returns `None` if conversion fails.
49    fn from_str_value(s: &str) -> Option<Self> {
50        let _ = s;
51        None
52    }
53
54    /// Construct from a u64 value. Returns `None` if conversion fails.
55    fn from_u64_value(v: u64) -> Option<Self> {
56        let _ = v;
57        None
58    }
59
60    /// Construct from an i64 value. Returns `None` if conversion fails.
61    fn from_i64_value(v: i64) -> Option<Self> {
62        let _ = v;
63        None
64    }
65
66    /// Construct from a bool value. Returns `None` if conversion fails.
67    fn from_bool_value(v: bool) -> Option<Self> {
68        let _ = v;
69        None
70    }
71}
72
73/// Built-in implementation for `String`, allowing direct field-to-context
74/// mapping without a newtype wrapper:
75///
76/// ```rust,no_run
77/// # use dcontext_tracing::DcontextLayer;
78/// # use tracing_subscriber::Registry;
79/// let layer: DcontextLayer<Registry> = DcontextLayer::builder()
80///     .map_field::<String>("job_id")
81///     .build();
82/// ```
83impl FromFieldValue for String {
84    fn from_str_value(s: &str) -> Option<Self> {
85        Some(s.to_string())
86    }
87}
88
89/// A concrete FieldSetter for a specific type T.
90pub(crate) struct TypedFieldSetter<T> {
91    _marker: std::marker::PhantomData<T>,
92}
93
94impl<T> TypedFieldSetter<T> {
95    pub fn new() -> Self {
96        Self {
97            _marker: std::marker::PhantomData,
98        }
99    }
100}
101
102impl<T> FieldSetter for TypedFieldSetter<T>
103where
104    T: FromFieldValue,
105{
106    fn set_from_str(&self, key: &'static str, value: &str) {
107        if let Some(v) = T::from_str_value(value) {
108            dcontext::set_context(key, v);
109        }
110    }
111
112    fn set_from_u64(&self, key: &'static str, value: u64) {
113        if let Some(v) = T::from_u64_value(value) {
114            dcontext::set_context(key, v);
115        }
116    }
117
118    fn set_from_i64(&self, key: &'static str, value: i64) {
119        if let Some(v) = T::from_i64_value(value) {
120            dcontext::set_context(key, v);
121        }
122    }
123
124    fn set_from_bool(&self, key: &'static str, value: bool) {
125        if let Some(v) = T::from_bool_value(value) {
126            dcontext::set_context(key, v);
127        }
128    }
129}
130
131/// Extracted field values from a span, stored in span extensions.
132///
133/// Must be `Send + Sync` to satisfy span extension requirements.
134pub(crate) struct ExtractedFields {
135    pub string_values: HashMap<&'static str, String>,
136    pub u64_values: HashMap<&'static str, u64>,
137    pub i64_values: HashMap<&'static str, i64>,
138    pub bool_values: HashMap<&'static str, bool>,
139}
140
141impl ExtractedFields {
142    pub fn new() -> Self {
143        Self {
144            string_values: HashMap::new(),
145            u64_values: HashMap::new(),
146            i64_values: HashMap::new(),
147            bool_values: HashMap::new(),
148        }
149    }
150
151    pub fn is_empty(&self) -> bool {
152        self.string_values.is_empty()
153            && self.u64_values.is_empty()
154            && self.i64_values.is_empty()
155            && self.bool_values.is_empty()
156    }
157}
158
159/// Visitor that extracts field values from span attributes.
160pub(crate) struct FieldExtractor<'a> {
161    /// Set of field names we're interested in.
162    pub target_fields: &'a [&'static str],
163    pub extracted: ExtractedFields,
164}
165
166impl<'a> FieldExtractor<'a> {
167    pub fn new(target_fields: &'a [&'static str]) -> Self {
168        Self {
169            target_fields,
170            extracted: ExtractedFields::new(),
171        }
172    }
173
174    fn is_target(&self, field: &Field) -> Option<&'static str> {
175        self.target_fields.iter().find(|&&f| f == field.name()).copied()
176    }
177}
178
179impl<'a> Visit for FieldExtractor<'a> {
180    fn record_str(&mut self, field: &Field, value: &str) {
181        if let Some(key) = self.is_target(field) {
182            self.extracted.string_values.insert(key, value.to_string());
183        }
184    }
185
186    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
187        if let Some(key) = self.is_target(field) {
188            // Fall back to Debug formatting for non-string types
189            if !self.extracted.string_values.contains_key(key)
190                && !self.extracted.u64_values.contains_key(key)
191                && !self.extracted.i64_values.contains_key(key)
192                && !self.extracted.bool_values.contains_key(key)
193            {
194                self.extracted
195                    .string_values
196                    .insert(key, format!("{:?}", value));
197            }
198        }
199    }
200
201    fn record_u64(&mut self, field: &Field, value: u64) {
202        if let Some(key) = self.is_target(field) {
203            self.extracted.u64_values.insert(key, value);
204        }
205    }
206
207    fn record_i64(&mut self, field: &Field, value: i64) {
208        if let Some(key) = self.is_target(field) {
209            self.extracted.i64_values.insert(key, value);
210        }
211    }
212
213    fn record_bool(&mut self, field: &Field, value: bool) {
214        if let Some(key) = self.is_target(field) {
215            self.extracted.bool_values.insert(key, value);
216        }
217    }
218}