Skip to main content

defect_cli/
observability.rs

1//! Translates a typed Langfuse config from `defect-config` into an observer for
2//! `defect-obs`.
3//!
4//! Translation and validation happen at CLI assembly time: `defect-obs` does not depend
5//! on `defect-config` (preserving a one-way dependency), and policy checks like "enabled
6//! but missing key" naturally belong in the assembly layer.
7//!
8//! Observability setup — tracing and Langfuse integration.
9
10use std::time::Duration;
11
12use defect_config::LangfuseConfig;
13use defect_obs::LangfuseObserver;
14use defect_obs::langfuse::{
15    DEFAULT_FLUSH_INTERVAL, DEFAULT_HOST, DEFAULT_MAX_BATCH, LangfuseSetup, build_observer,
16};
17
18/// Builds an observer from a typed `LangfuseConfig`.
19///
20/// Returns `Ok(None)` when the observer is not enabled (config missing or `enabled =
21/// false`) — the caller should not attach an observer. When `enabled = true` but
22/// `public_key` / `secret_key` are missing, a warning is emitted and the observer is
23/// **disabled** (returns `Ok(None)`). This does not error or silently succeed, matching
24/// the explicit validation contract.
25///
26/// # Errors
27///
28/// Returns an error if the HTTP stack fails to build (e.g., TLS roots or proxy URL
29/// parsing).
30pub fn build_langfuse_observer(
31    config: Option<&LangfuseConfig>,
32    http_stack_config: defect_http::HttpStackConfig,
33) -> anyhow::Result<Option<LangfuseObserver>> {
34    let Some(cfg) = config.filter(|c| c.enabled) else {
35        return Ok(None);
36    };
37
38    let (Some(public_key), Some(secret_key)) = (cfg.public_key.clone(), cfg.secret_key.clone())
39    else {
40        tracing::warn!(
41            "tracing.langfuse.enabled = true but public_key / secret_key missing; \
42             langfuse reporting disabled"
43        );
44        return Ok(None);
45    };
46
47    let http = defect_http::build_http_stack(http_stack_config)
48        .map_err(|e| anyhow::anyhow!("langfuse http stack init failed: {e}"))?;
49
50    let setup = LangfuseSetup {
51        host: cfg.host.clone().unwrap_or_else(|| DEFAULT_HOST.to_string()),
52        public_key,
53        secret_key,
54        flush_interval: cfg
55            .flush_interval_ms
56            .map_or(DEFAULT_FLUSH_INTERVAL, Duration::from_millis),
57        max_batch: cfg.max_batch.unwrap_or(DEFAULT_MAX_BATCH),
58    };
59
60    Ok(Some(build_observer(setup, http)))
61}