Skip to main content

datadog_agent_apm/semantics/
lookup.rs

1//! Concept-driven attribute lookups.
2
3use super::accessor::Accessor;
4use super::registry::{Condition, Registry, TagInfo, ValueType};
5use super::Concept;
6
7/// Reports whether a single condition holds against the accessor's raw attribute store.
8///
9/// Conditions read attributes by exact key—no fallback chaining—so a condition may
10/// be true even if a lookup for the same attribute as a concept would resolve it
11/// through a different name.
12fn condition_matches<A: Accessor>(accessor: &A, condition: &Condition) -> bool {
13    let value = accessor.get_string(&condition.attribute);
14    let found = value.is_some_and(|v| !v.is_empty());
15
16    if let Some(present) = condition.present {
17        if found != present {
18            return false;
19        }
20    }
21
22    if let Some(expected) = condition.eq.as_deref() {
23        if !found || value != Some(expected) {
24            return false;
25        }
26    }
27
28    true
29}
30
31/// Reports whether all of a fallback tag's conditions hold (logical AND).
32///
33/// An empty condition list always matches.
34fn conditions_match<A: Accessor>(accessor: &A, tag: &TagInfo) -> bool {
35    tag.when.iter().all(|condition| condition_matches(accessor, condition))
36}
37
38/// Look up a concept as an `i64`.
39///
40/// Mirrors upstream `LookupInt64`:
41/// - `Int64` tags match only integer-typed attributes.
42/// - `Float64` tags accept float-typed attributes whose value is losslessly
43///   representable as an `i64`.
44/// - `String` tags accept string-typed attributes, parsing via `i64::from_str`
45///   first and then a float-parse fallback for integer-valued floats.
46pub fn lookup_int64<A: Accessor>(registry: &Registry, accessor: &A, concept: Concept) -> Option<i64> {
47    let tags = registry.get_attribute_precedence(concept)?;
48    for tag in tags {
49        if !conditions_match(accessor, tag) {
50            continue;
51        }
52        match tag.value_type {
53            ValueType::Int64 => {
54                if let Some(v) = accessor.get_int64(&tag.name) {
55                    return Some(v);
56                }
57            }
58            ValueType::Float64 => {
59                if let Some(v) = accessor.get_float64(&tag.name) {
60                    //Protect against values greater than i64::MAX and less than i64::MIN
61                    if (-9223372036854775808.0..9223372036854775808.0).contains(&v) {
62                        let i = v as i64;
63                        if i as f64 == v {
64                            return Some(i);
65                        }
66                    }
67                }
68            }
69            ValueType::String => {
70                if let Some(s) = accessor.get_string(&tag.name) {
71                    if !s.is_empty() {
72                        if let Ok(i) = s.parse::<i64>() {
73                            return Some(i);
74                        }
75                        if let Ok(f) = s.parse::<f64>() {
76                            let i = f as i64;
77                            if i as f64 == f {
78                                return Some(i);
79                            }
80                        }
81                    }
82                }
83            }
84        }
85    }
86    None
87}
88
89/// Look up a concept as an `f64`. Mirrors upstream `LookupFloat64`.
90pub fn lookup_float64<A: Accessor>(registry: &Registry, accessor: &A, concept: Concept) -> Option<f64> {
91    let tags = registry.get_attribute_precedence(concept)?;
92    for tag in tags {
93        if !conditions_match(accessor, tag) {
94            continue;
95        }
96        match tag.value_type {
97            ValueType::Float64 => {
98                if let Some(v) = accessor.get_float64(&tag.name) {
99                    return Some(v);
100                }
101            }
102            ValueType::Int64 => {
103                if let Some(v) = accessor.get_int64(&tag.name) {
104                    return Some(v as f64);
105                }
106            }
107            ValueType::String => {
108                if let Some(s) = accessor.get_string(&tag.name) {
109                    if !s.is_empty() {
110                        if let Ok(f) = s.parse::<f64>() {
111                            return Some(f);
112                        }
113                    }
114                }
115            }
116        }
117    }
118    None
119}
120
121/// Look up a concept as a `String`. Mirrors upstream `LookupString`.
122///
123/// Non-string-typed tags are stringified via `format!` to match upstream's
124/// `strconv.FormatInt(_, 10)` and `strconv.FormatFloat(_, 'f', -1, 64)` output.
125pub fn lookup_string<A: Accessor>(registry: &Registry, accessor: &A, concept: Concept) -> Option<String> {
126    let tags = registry.get_attribute_precedence(concept)?;
127    for tag in tags {
128        if !conditions_match(accessor, tag) {
129            continue;
130        }
131        match tag.value_type {
132            ValueType::String => {
133                if let Some(s) = accessor.get_string(&tag.name) {
134                    if !s.is_empty() {
135                        return Some(s.to_string());
136                    }
137                }
138            }
139            ValueType::Int64 => {
140                if let Some(v) = accessor.get_int64(&tag.name) {
141                    return Some(v.to_string());
142                }
143            }
144            ValueType::Float64 => {
145                if let Some(v) = accessor.get_float64(&tag.name) {
146                    return Some(format_float(v));
147                }
148            }
149        }
150    }
151    None
152}
153
154// Mirror Go's `strconv.FormatFloat(v, 'f', -1, 64)`: integer-valued floats
155// print without a fractional part, otherwise the shortest decimal form.
156fn format_float(v: f64) -> String {
157    if v.fract() == 0.0 && v.is_finite() {
158        format!("{}", v as i64)
159    } else {
160        format!("{}", v)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::super::registry::{registry_json, EMBEDDED_REGISTRY};
167    use super::*;
168
169    enum Value {
170        String(String),
171        Int(i64),
172        Double(f64),
173    }
174
175    struct TestAccessor<'a>(&'a [(&'a str, Value)]);
176
177    impl<'a> TestAccessor<'a> {
178        fn new(attrs: &'a [(&'a str, Value)]) -> Self {
179            Self(attrs)
180        }
181
182        fn find(&self, key: &str) -> Option<&Value> {
183            self.0.iter().find(|(name, _)| *name == key).map(|(_, value)| value)
184        }
185    }
186
187    impl Accessor for TestAccessor<'_> {
188        fn get_string(&self, key: &str) -> Option<&str> {
189            match self.find(key)? {
190                Value::String(value) => Some(value),
191                _ => None,
192            }
193        }
194
195        fn get_int64(&self, key: &str) -> Option<i64> {
196            match self.find(key)? {
197                Value::Int(value) => Some(*value),
198                _ => None,
199            }
200        }
201
202        fn get_float64(&self, key: &str) -> Option<f64> {
203            match self.find(key)? {
204                Value::Double(value) => Some(*value),
205                _ => None,
206            }
207        }
208    }
209
210    #[test]
211    fn int64_int_attr_matches() {
212        let attrs = vec![("http.status_code", Value::Int(500))];
213        let a = TestAccessor::new(&attrs);
214        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::HttpStatusCode), Some(500));
215    }
216
217    #[test]
218    fn int64_string_attr_parses() {
219        // The lading reproduction: http.response.status_code sent as StringValue.
220        let attrs = vec![("http.response.status_code", Value::String("500".into()))];
221        let a = TestAccessor::new(&attrs);
222        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::HttpStatusCode), Some(500));
223    }
224
225    #[test]
226    fn int64_malformed_string_returns_none() {
227        let attrs = vec![("http.status_code", Value::String("not-a-number".into()))];
228        let a = TestAccessor::new(&attrs);
229        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::HttpStatusCode), None);
230    }
231
232    #[test]
233    fn int64_empty_string_returns_none() {
234        let attrs = vec![("http.status_code", Value::String(String::new()))];
235        let a = TestAccessor::new(&attrs);
236        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::HttpStatusCode), None);
237    }
238
239    #[test]
240    fn int64_string_with_fractional_float_parses() {
241        // "200.0" is an integer-valued float — accept it.
242        let attrs = vec![("http.status_code", Value::String("200.0".into()))];
243        let a = TestAccessor::new(&attrs);
244        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::HttpStatusCode), Some(200));
245    }
246
247    #[test]
248    fn int64_string_with_true_fractional_returns_none() {
249        let attrs = vec![("http.status_code", Value::String("200.5".into()))];
250        let a = TestAccessor::new(&attrs);
251        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::HttpStatusCode), None);
252    }
253
254    #[test]
255    fn int64_int_tag_ignores_string_value_at_same_name() {
256        // If only a string exists for http.status_code, the int-tag entry for
257        // that same name should skip, and the string-tag entry later in the
258        // precedence list should match.
259        let attrs = vec![("http.status_code", Value::String("204".into()))];
260        let a = TestAccessor::new(&attrs);
261        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::HttpStatusCode), Some(204));
262    }
263
264    #[test]
265    fn string_int_attr_stringifies() {
266        // rpc.grpc.status_code is registered as int64 first; looking it up as
267        // a string should format the int.
268        let attrs = vec![("rpc.grpc.status_code", Value::Int(14))];
269        let a = TestAccessor::new(&attrs);
270        assert_eq!(
271            lookup_string(&EMBEDDED_REGISTRY, &a, Concept::RpcGrpcStatusCode).as_deref(),
272            Some("14"),
273        );
274    }
275
276    #[test]
277    fn string_string_attr_matches() {
278        let attrs = vec![("service.name", Value::String("cart".into()))];
279        let a = TestAccessor::new(&attrs);
280        assert_eq!(
281            lookup_string(&EMBEDDED_REGISTRY, &a, Concept::ServiceName).as_deref(),
282            Some("cart"),
283        );
284    }
285
286    #[test]
287    fn float64_float_matches_first() {
288        let attrs = vec![("_dd.top_level", Value::Double(1.0))];
289        let a = TestAccessor::new(&attrs);
290        assert_eq!(lookup_float64(&EMBEDDED_REGISTRY, &a, Concept::DdTopLevel), Some(1.0));
291    }
292
293    #[test]
294    fn int64_float_out_of_range_returns_none() {
295        // `2^63` saturates to `i64::MAX` under a plain cast, and `i64::MAX as f64`
296        // rounds back to exactly `2^63` — the round-trip check alone would wrongly
297        // accept it. The bounds check must reject it first.
298        let attrs = vec![("_dd.top_level", Value::Double(9223372036854775808.0))];
299        let a = TestAccessor::new(&attrs);
300        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::DdTopLevel), None);
301
302        let attrs = vec![("_dd.top_level", Value::Double(1.0e19))];
303        let a = TestAccessor::new(&attrs);
304        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::DdTopLevel), None);
305
306        // `-2^63` is exactly `i64::MIN` and must be accepted.
307        let attrs = vec![("_dd.top_level", Value::Double(-9223372036854775808.0))];
308        let a = TestAccessor::new(&attrs);
309        assert_eq!(
310            lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::DdTopLevel),
311            Some(i64::MIN)
312        );
313    }
314
315    #[test]
316    fn float64_missing_concept_returns_none() {
317        let attrs: Vec<(&str, Value)> = vec![];
318        let a = TestAccessor::new(&attrs);
319        assert_eq!(lookup_float64(&EMBEDDED_REGISTRY, &a, Concept::DdTopLevel), None);
320    }
321
322    #[test]
323    fn int64_when_condition_failure_skips_fallback() {
324        // A non-gRPC span carrying rpc.response.status_code: the `when`
325        // clause (rpc.system == "grpc") must block the fallback, so the lookup
326        // returns None rather than resolving a gRPC status code from it.
327        let attrs = vec![
328            ("rpc.response.status_code", Value::Int(14)),
329            ("rpc.system", Value::String("apache_dubbo".into())),
330        ];
331        let a = TestAccessor::new(&attrs);
332        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::RpcGrpcStatusCode), None);
333    }
334
335    #[test]
336    fn int64_when_condition_pass_accepts_fallback() {
337        let attrs = vec![
338            ("rpc.response.status_code", Value::Int(14)),
339            ("rpc.system", Value::String("grpc".into())),
340        ];
341        let a = TestAccessor::new(&attrs);
342        assert_eq!(
343            lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::RpcGrpcStatusCode),
344            Some(14)
345        );
346    }
347
348    #[test]
349    fn int64_when_condition_missing_attribute_blocks_fallback() {
350        // No rpc.system attribute at all: the `when` clause cannot hold, so the
351        // fallback must be skipped.
352        let attrs = vec![("rpc.response.status_code", Value::Int(14))];
353        let a = TestAccessor::new(&attrs);
354        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::RpcGrpcStatusCode), None);
355    }
356
357    #[test]
358    fn int64_when_requires_all_conditions() {
359        // The dual-condition fallback (rpc.system == "grpc" AND rpc.system.name
360        // absent) requires both. Here rpc.system.name is present, so the
361        // multi-condition fallback is skipped; rpc.grpc.status_code matches.
362        let attrs = vec![
363            ("rpc.system", Value::String("grpc".into())),
364            ("rpc.system.name", Value::String("something".into())),
365            ("rpc.response.status_code", Value::Int(14)),
366            ("rpc.grpc.status_code", Value::Int(3)),
367        ];
368        let a = TestAccessor::new(&attrs);
369        assert_eq!(
370            lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::RpcGrpcStatusCode),
371            Some(3)
372        );
373    }
374
375    #[test]
376    fn int64_when_partial_condition_set_blocks_fallback() {
377        // rpc.system matches "grpc" but rpc.system.name is present: the second
378        // condition (present: false) fails, so the fallback is skipped entirely.
379        let attrs = vec![
380            ("rpc.system", Value::String("grpc".into())),
381            ("rpc.system.name", Value::String("something".into())),
382            ("rpc.response.status_code", Value::Int(14)),
383        ];
384        let a = TestAccessor::new(&attrs);
385        assert_eq!(lookup_int64(&EMBEDDED_REGISTRY, &a, Concept::RpcGrpcStatusCode), None);
386    }
387
388    #[test]
389    fn string_when_condition_failure_skips_fallback() {
390        let attrs = vec![
391            ("rpc.response.status_code", Value::String("14".into())),
392            ("rpc.system", Value::String("apache_dubbo".into())),
393        ];
394        let a = TestAccessor::new(&attrs);
395        assert_eq!(lookup_string(&EMBEDDED_REGISTRY, &a, Concept::RpcGrpcStatusCode), None);
396    }
397
398    #[test]
399    fn string_when_condition_pass_accepts_fallback() {
400        let attrs = vec![
401            ("rpc.response.status_code", Value::String("14".into())),
402            ("rpc.system.name", Value::String("grpc".into())),
403        ];
404        let a = TestAccessor::new(&attrs);
405        assert_eq!(
406            lookup_string(&EMBEDDED_REGISTRY, &a, Concept::RpcGrpcStatusCode).as_deref(),
407            Some("14"),
408        );
409    }
410
411    // A `null` condition matches every span, as in upstream `pkg/trace/semantics/lookup.go`.
412    #[test]
413    fn null_condition_does_not_block_fallback() {
414        let json = registry_json(
415            r#"{"db.statement": {"fallbacks": [
416                {"name": "custom.statement", "provider": "otel", "type": "string", "when": [null]}]}}"#,
417        );
418        let registry = Registry::from_json(&json).expect("a null condition should parse");
419        let attrs = vec![("custom.statement", Value::String("SELECT 1".into()))];
420        let a = TestAccessor::new(&attrs);
421        assert_eq!(
422            lookup_string(&registry, &a, Concept::DbStatement).as_deref(),
423            Some("SELECT 1")
424        );
425    }
426}