1use std::sync::Arc;
7use std::time::Duration;
8
9use serde_json::json;
10use tracing::debug;
11
12use crate::state::RequestLog;
13
14#[derive(Clone)]
19pub struct TelemetryClient {
20 inner: Arc<Inner>,
21}
22
23struct Inner {
24 event_url: String,
25 heartbeat_url: String,
26 token: Option<String>,
27 instance: String,
28 client: reqwest::Client,
29}
30
31impl TelemetryClient {
32 pub fn new(base_url: &str, token: Option<String>, instance: String) -> Self {
33 let base = base_url.trim_end_matches('/');
34 let client = reqwest::Client::builder()
35 .timeout(Duration::from_secs(5))
36 .build()
37 .expect("reqwest client");
38
39 Self {
40 inner: Arc::new(Inner {
41 event_url: format!("{base}/event"),
42 heartbeat_url: format!("{base}/heartbeat"),
43 token,
44 instance,
45 client,
46 }),
47 }
48 }
49
50 pub fn push_event(&self, log: &RequestLog) {
52 let inner = Arc::clone(&self.inner);
53 let body = json!({
54 "instance": inner.instance,
55 "ts_ms": log.ts_ms,
56 "account": log.account,
57 "model": log.model,
58 "status": log.status,
59 "duration_ms": log.duration_ms,
60 });
61
62 tokio::spawn(async move {
63 let mut req = inner.client.post(&inner.event_url).json(&body);
64 if let Some(ref t) = inner.token {
65 req = req.bearer_auth(t);
66 }
67 match req.send().await {
68 Ok(r) if !r.status().is_success() => {
69 debug!(status = %r.status(), "relay rejected event");
70 }
71 Err(e) => debug!(err = %e, "relay event send failed"),
72 _ => {}
73 }
74 });
75 }
76
77 pub async fn push_heartbeat(&self, status: serde_json::Value) {
80 let body = json!({
81 "instance": self.inner.instance,
82 "status": status,
83 });
84 let mut req = self.inner.client.post(&self.inner.heartbeat_url).json(&body);
85 if let Some(ref t) = self.inner.token {
86 req = req.bearer_auth(t);
87 }
88 match req.send().await {
89 Ok(r) if !r.status().is_success() => {
90 debug!(status = %r.status(), "relay rejected heartbeat");
91 }
92 Err(e) => debug!(err = %e, "relay heartbeat send failed"),
93 _ => {}
94 }
95 }
96}