use crate::observability::gen_ai;
use serde_json::Value;
use std::collections::HashMap;
pub trait OTelAttributeMapper: Send + Sync {
fn map_attributes(&self, input: &HashMap<String, Value>) -> Vec<(String, String)>;
}
#[derive(Debug, Default)]
pub struct GenAIAttributeMapper;
impl GenAIAttributeMapper {
pub const fn new() -> Self {
Self
}
}
const KNOWN_KEYS: &[&str] = &[
gen_ai::OPERATION_NAME,
gen_ai::PROVIDER_NAME,
gen_ai::REQUEST_MODEL,
gen_ai::REQUEST_TEMPERATURE,
gen_ai::REQUEST_MAX_TOKENS,
gen_ai::RESPONSE_MODEL,
gen_ai::RESPONSE_FINISH_REASONS,
gen_ai::RESPONSE_ID,
gen_ai::USAGE_INPUT_TOKENS,
gen_ai::USAGE_OUTPUT_TOKENS,
];
impl OTelAttributeMapper for GenAIAttributeMapper {
fn map_attributes(&self, input: &HashMap<String, Value>) -> Vec<(String, String)> {
let mut out = Vec::with_capacity(input.len());
for (key, value) in input {
let otel_key = if KNOWN_KEYS.contains(&key.as_str()) {
key.clone()
} else {
format!("synwire.custom.{key}")
};
let otel_value = match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => String::new(),
other => other.to_string(),
};
out.push((otel_key, otel_value));
}
out
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn maps_known_keys_directly() {
let mapper = GenAIAttributeMapper::new();
let mut input = HashMap::new();
let _ = input.insert(gen_ai::REQUEST_MODEL.to_owned(), json!("gpt-4"));
let _ = input.insert(gen_ai::USAGE_INPUT_TOKENS.to_owned(), json!(100));
let attrs = mapper.map_attributes(&input);
assert_eq!(attrs.len(), 2);
let model_attr = attrs.iter().find(|(k, _)| k == gen_ai::REQUEST_MODEL);
assert!(model_attr.is_some());
assert_eq!(model_attr.unwrap().1, "gpt-4");
}
#[test]
fn prefixes_unknown_keys() {
let mapper = GenAIAttributeMapper::new();
let mut input = HashMap::new();
let _ = input.insert("my.custom.key".to_owned(), json!("value"));
let attrs = mapper.map_attributes(&input);
assert_eq!(attrs.len(), 1);
assert_eq!(attrs[0].0, "synwire.custom.my.custom.key");
}
#[test]
fn handles_various_value_types() {
let mapper = GenAIAttributeMapper::new();
let mut input = HashMap::new();
let _ = input.insert(gen_ai::REQUEST_TEMPERATURE.to_owned(), json!(0.7));
let _ = input.insert("flag".to_owned(), json!(true));
let _ = input.insert("empty".to_owned(), json!(null));
let attrs = mapper.map_attributes(&input);
assert_eq!(attrs.len(), 3);
}
}