microclaw_engine/internal/observability/adapters/
langfuse.rs1use base64::prelude::*;
2use serde_yaml::{Mapping, Value};
3use tracing::warn;
4
5use super::TraceTargetConfig;
6
7fn get_trimmed<'a>(map: &'a Mapping, key: &str) -> Option<&'a str> {
8 map.get(Value::String(key.to_string()))
9 .and_then(Value::as_str)
10 .map(str::trim)
11 .filter(|v| !v.is_empty())
12}
13
14pub fn apply(map: &Mapping, target: &mut TraceTargetConfig) {
15 let Some(pk) = get_trimmed(map, "langfuse_public_key") else {
16 return;
17 };
18 let Some(sk) = get_trimmed(map, "langfuse_secret_key") else {
19 return;
20 };
21 let host = normalize_langfuse_host(
22 get_trimmed(map, "langfuse_host").unwrap_or("https://cloud.langfuse.com"),
23 );
24
25 if target.endpoint.is_none() {
26 target.endpoint = Some(format!(
27 "{}/api/public/otel/v1/traces",
28 host.trim_end_matches('/')
29 ));
30 }
31
32 let auth = BASE64_STANDARD.encode(format!("{pk}:{sk}"));
33 if !target
34 .headers
35 .iter()
36 .any(|(k, _)| k.eq_ignore_ascii_case("authorization"))
37 {
38 target
39 .headers
40 .push(("Authorization".to_string(), format!("Basic {auth}")));
41 }
42}
43
44fn normalize_langfuse_host(raw: &str) -> String {
45 let mut host = raw.trim().trim_end_matches('/').to_string();
46 if let Some((prefix, _)) = host.split_once("/project/") {
47 warn!(
48 langfuse_host = %raw,
49 "langfuse_host appears to be a UI project URL; using host root instead"
50 );
51 host = prefix.to_string();
52 }
53 if host.ends_with("/api/public/otel/v1/traces") {
54 host = host
55 .trim_end_matches("/api/public/otel/v1/traces")
56 .to_string();
57 } else if host.ends_with("/api/public/otel") {
58 host = host.trim_end_matches("/api/public/otel").to_string();
59 }
60 host
61}