use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::{debug, warn};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionMetrics {
pub signals_generated: u64,
pub signals_filtered: u64,
pub avg_confidence: f64,
pub p50_latency_us: u64,
pub p99_latency_us: u64,
pub regime: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SessionMetricsClient {
inner: Option<ActiveClient>,
}
#[derive(Debug, Clone)]
struct ActiveClient {
base_url: String,
http: reqwest::Client,
}
impl SessionMetricsClient {
pub fn from_env() -> Self {
let url = std::env::var("JANUS_AI_URL").ok().filter(|s| !s.is_empty());
let inner = url.map(|base_url| {
let timeout_secs = std::env::var("JANUS_AI_TIMEOUT_SECS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2);
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
ActiveClient {
base_url: base_url.trim_end_matches('/').to_string(),
http,
}
});
Self { inner }
}
pub fn is_enabled(&self) -> bool {
self.inner.is_some()
}
pub async fn push(&self, session_id: &str, metrics: &SessionMetrics) {
let Some(client) = &self.inner else {
debug!(
session_id,
"session metrics push skipped — JANUS_AI_URL not set"
);
return;
};
let url = format!(
"{base}/api/janus-ai/sessions/{session_id}/metrics",
base = client.base_url
);
match client.http.post(&url).json(metrics).send().await {
Ok(resp) if resp.status().is_success() => {
debug!(session_id, status = %resp.status(), "session metrics pushed");
}
Ok(resp) => {
warn!(
session_id,
status = %resp.status(),
"session metrics push: non-success response"
);
}
Err(e) => {
warn!(session_id, error = %e, "session metrics push failed");
}
}
}
}
impl Default for SessionMetricsClient {
fn default() -> Self {
Self::from_env()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_when_env_unset() {
let prev = std::env::var("JANUS_AI_URL").ok();
unsafe { std::env::remove_var("JANUS_AI_URL") };
let client = SessionMetricsClient::from_env();
assert!(!client.is_enabled());
if let Some(v) = prev {
unsafe { std::env::set_var("JANUS_AI_URL", v) };
}
}
#[tokio::test]
async fn push_no_ops_when_disabled() {
let prev = std::env::var("JANUS_AI_URL").ok();
unsafe { std::env::remove_var("JANUS_AI_URL") };
let client = SessionMetricsClient::from_env();
client.push("session-abc", &SessionMetrics::default()).await;
if let Some(v) = prev {
unsafe { std::env::set_var("JANUS_AI_URL", v) };
}
}
}