1 use smbcloud_gresiq_sdk::{Environment, GresiqClient, GresiqCredentials};
2
3 use super::events::{InferenceEvent, ModelLoadedEvent};
4
5 /// GresIQ credentials embedded at SDK build time — one pair per environment.
6 /// Consumer apps never set these — they're Onde Inference's own credentials.
7 const EMBEDDED_API_KEY_DEV: Option<&str> = option_env!("GRESIQ_API_KEY_DEV");
8 const EMBEDDED_API_SECRET_DEV: Option<&str> = option_env!("GRESIQ_API_SECRET_DEV");
9 const EMBEDDED_API_KEY_PRODUCTION: Option<&str> = option_env!("GRESIQ_API_KEY_PRODUCTION");
10 const EMBEDDED_API_SECRET_SECRET_PRODUCTION: Option<&str> =
11 option_env!("GRESIQ_API_SECRET_PRODUCTION");
12
13 /// Onde telemetry client. Wraps GresiqClient so pulse events land in the
14 /// right GresIQ-managed tables without consumer apps knowing anything about
15 /// the GresIQ auth layer underneath.
16 ///
17 /// GresIQ credentials are embedded in the SDK at build time.
18 /// Consumer apps only provide an `edge_id` (stable device identifier).
19 ///
20 /// Cheap to clone: the inner GresiqClient holds an Arc-backed reqwest::Client,
21 /// so cloning is a pointer bump, not a new TCP connection.
22 #[derive(Debug, Clone)]
23 pub struct PulseClient {
24 inner: GresiqClient,
25 edge_id: String,
26 }
27
28 impl PulseClient {
29 /// Build a pulse client using the GresIQ credentials embedded in the SDK.
30 ///
31 /// Returns `None` if the SDK was compiled without `GRESIQ_API_KEY` /
32 /// `GRESIQ_API_SECRET` (e.g. a local dev build of onde without `.env`).
33 /// In that case telemetry is silently disabled — no panic, no partial state.
34 ///
35 /// `edge_id` is a stable device identifier (installation UUID).
36 /// Pass an empty string to default to `"onde-unknown"`.
37 pub fn new(environment: Environment, edge_id: String) -> Option<Self> {
38 let (api_key, api_secret) = match environment {
39 Environment::Dev => (EMBEDDED_API_KEY_DEV?, EMBEDDED_API_SECRET_DEV?),
40 Environment::Production => (
41 EMBEDDED_API_KEY_PRODUCTION?,
42 EMBEDDED_API_SECRET_SECRET_PRODUCTION?,
43 ),
44 };
45
46 let edge_id = if edge_id.is_empty() {
47 "onde-unknown".to_string()
48 } else {
49 edge_id
50 };
51
52 let credentials = GresiqCredentials {
53 api_key,
54 api_secret,
55 };
56
57 let inner = GresiqClient::from_credentials(environment, credentials);
58
59 Some(PulseClient { inner, edge_id })
60 }
61
62 /// Spawns a background task that writes the model-load event to the
63 /// pulse/model_loaded table, then returns immediately. Slow network
64 /// responses don't block the first inference request.
65 ///
66 /// Failed writes emit a warn! log line — no retry, no queue,
67 /// and no effect on the caller.
68 pub fn record_model_loaded(&self, model_id: String, model_name: String, load_duration_ms: u64) {
69 let client = self.clone();
70 tokio::spawn(async move {
71 let event = ModelLoadedEvent {
72 edge_id: client.edge_id.clone(),
73 model_id,
74 model_name,
75 load_duration_ms,
76 };
77 if let Err(error) = client.inner.insert("pulse/model_loaded", &event).await {
78 log::warn!("pulse: model_loaded failed: {}", error);
79 }
80 });
81 }
82
83 /// Same fire-and-forget pattern as record_model_loaded but for inference
84 /// completions. Writes to pulse/inference_event. Logs on failure, no retry.
85 pub fn record_inference(
86 &self,
87 model_id: String,
88 request_id: String,
89 duration_ms: u64,
90 status: String,
91 ) {
92 let client = self.clone();
93 tokio::spawn(async move {
94 let event = InferenceEvent {
95 edge_id: client.edge_id.clone(),
96 model_id,
97 request_id,
98 duration_ms,
99 status,
100 };
101 if let Err(error) = client.inner.insert("pulse/inference_event", &event).await {
102 log::warn!("pulse: inference_event failed: {}", error);
103 }
104 });
105 }
106 }