datadog_agent_apm/semantics/
lookup.rs1use super::accessor::Accessor;
4use super::registry::{Condition, Registry, TagInfo, ValueType};
5use super::Concept;
6
7fn 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
31fn conditions_match<A: Accessor>(accessor: &A, tag: &TagInfo) -> bool {
35 tag.when.iter().all(|condition| condition_matches(accessor, condition))
36}
37
38pub 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 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
89pub 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
121pub 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
154fn 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 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 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 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 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 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 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 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 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 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 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 #[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(®istry, &a, Concept::DbStatement).as_deref(),
423 Some("SELECT 1")
424 );
425 }
426}