Skip to main content

microclaw_engine/internal/observability/adapters/
langfuse.rs

1use 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}