synwire_core/observability/
attribute_mapper.rs1use crate::observability::gen_ai;
4use serde_json::Value;
5use std::collections::HashMap;
6
7pub trait OTelAttributeMapper: Send + Sync {
10 fn map_attributes(&self, input: &HashMap<String, Value>) -> Vec<(String, String)>;
12}
13
14#[derive(Debug, Default)]
19pub struct GenAIAttributeMapper;
20
21impl GenAIAttributeMapper {
22 pub const fn new() -> Self {
24 Self
25 }
26}
27
28const KNOWN_KEYS: &[&str] = &[
30 gen_ai::OPERATION_NAME,
31 gen_ai::PROVIDER_NAME,
32 gen_ai::REQUEST_MODEL,
33 gen_ai::REQUEST_TEMPERATURE,
34 gen_ai::REQUEST_MAX_TOKENS,
35 gen_ai::RESPONSE_MODEL,
36 gen_ai::RESPONSE_FINISH_REASONS,
37 gen_ai::RESPONSE_ID,
38 gen_ai::USAGE_INPUT_TOKENS,
39 gen_ai::USAGE_OUTPUT_TOKENS,
40];
41
42impl OTelAttributeMapper for GenAIAttributeMapper {
43 fn map_attributes(&self, input: &HashMap<String, Value>) -> Vec<(String, String)> {
44 let mut out = Vec::with_capacity(input.len());
45 for (key, value) in input {
46 let otel_key = if KNOWN_KEYS.contains(&key.as_str()) {
47 key.clone()
48 } else {
49 format!("synwire.custom.{key}")
50 };
51
52 let otel_value = match value {
53 Value::String(s) => s.clone(),
54 Value::Number(n) => n.to_string(),
55 Value::Bool(b) => b.to_string(),
56 Value::Null => String::new(),
57 other => other.to_string(),
58 };
59
60 out.push((otel_key, otel_value));
61 }
62 out
63 }
64}
65
66#[cfg(test)]
67#[allow(clippy::unwrap_used)]
68mod tests {
69 use super::*;
70 use serde_json::json;
71
72 #[test]
73 fn maps_known_keys_directly() {
74 let mapper = GenAIAttributeMapper::new();
75 let mut input = HashMap::new();
76 let _ = input.insert(gen_ai::REQUEST_MODEL.to_owned(), json!("gpt-4"));
77 let _ = input.insert(gen_ai::USAGE_INPUT_TOKENS.to_owned(), json!(100));
78
79 let attrs = mapper.map_attributes(&input);
80 assert_eq!(attrs.len(), 2);
81
82 let model_attr = attrs.iter().find(|(k, _)| k == gen_ai::REQUEST_MODEL);
83 assert!(model_attr.is_some());
84 assert_eq!(model_attr.unwrap().1, "gpt-4");
85 }
86
87 #[test]
88 fn prefixes_unknown_keys() {
89 let mapper = GenAIAttributeMapper::new();
90 let mut input = HashMap::new();
91 let _ = input.insert("my.custom.key".to_owned(), json!("value"));
92
93 let attrs = mapper.map_attributes(&input);
94 assert_eq!(attrs.len(), 1);
95 assert_eq!(attrs[0].0, "synwire.custom.my.custom.key");
96 }
97
98 #[test]
99 fn handles_various_value_types() {
100 let mapper = GenAIAttributeMapper::new();
101 let mut input = HashMap::new();
102 let _ = input.insert(gen_ai::REQUEST_TEMPERATURE.to_owned(), json!(0.7));
103 let _ = input.insert("flag".to_owned(), json!(true));
104 let _ = input.insert("empty".to_owned(), json!(null));
105
106 let attrs = mapper.map_attributes(&input);
107 assert_eq!(attrs.len(), 3);
108 }
109}