1use reqwest::Client as HttpClient;
2use serde::Serialize;
3use serde_json::Value;
4use std::collections::HashMap;
5use std::future::Future;
6use std::sync::{Arc, Mutex};
7use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
8use tokio::sync::Notify;
9use tokio::task::JoinHandle;
10use uuid::Uuid;
11
12#[derive(Clone)]
17pub struct Config {
18 pub api_key: String,
19 pub sli_id: String,
20 pub base_url: String,
21 pub batch_size: usize,
22 pub flush_interval: Duration,
23}
24
25impl Default for Config {
26 fn default() -> Self {
27 Config {
28 api_key: String::new(),
29 sli_id: String::new(),
30 base_url: "https://api.levelops.io".into(),
31 batch_size: 100,
32 flush_interval: Duration::from_secs(1),
33 }
34 }
35}
36
37#[derive(Default)]
38pub struct Event {
39 pub event_id: Option<String>,
40 pub timestamp: Option<String>,
41 pub fields: HashMap<String, Value>,
42 pub dimensions: HashMap<String, String>,
43}
44
45pub struct MeasureOpts {
46 pub fields: HashMap<String, Value>,
47 pub dimensions: HashMap<String, String>,
48 pub event_id: Option<String>,
49}
50
51impl MeasureOpts {
52 pub fn new(dims: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
53 MeasureOpts {
54 fields: HashMap::new(),
55 dimensions: dims.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
56 event_id: None,
57 }
58 }
59
60 pub fn event_id(mut self, id: impl Into<String>) -> Self {
61 self.event_id = Some(id.into());
62 self
63 }
64
65 pub fn fields(mut self, f: HashMap<String, Value>) -> Self {
66 self.fields = f;
67 self
68 }
69}
70
71#[derive(Serialize)]
76struct WireEvent<'a> {
77 event_id: &'a str,
78 sli_id: &'a str,
79 timestamp: &'a str,
80 fields: &'a HashMap<String, Value>,
81 dimensions: &'a HashMap<String, String>,
82}
83
84#[derive(Serialize)]
85struct Batch<'a> {
86 events: Vec<WireEvent<'a>>,
87}
88
89struct BufferedEvent {
90 event_id: String,
91 sli_id: String,
92 timestamp: String,
93 fields: HashMap<String, Value>,
94 dimensions: HashMap<String, String>,
95}
96
97struct Inner {
102 api_key: String,
103 sli_id: String,
104 base_url: String,
105 batch_size: usize,
106 buffer: Mutex<Vec<BufferedEvent>>,
107 http: HttpClient,
108 notify: Notify,
109}
110
111#[derive(Clone)]
112pub struct Client {
113 inner: Arc<Inner>,
114 _bg: Arc<JoinHandle<()>>,
115}
116
117impl Client {
118 pub fn new(cfg: Config) -> Result<Self, Box<dyn std::error::Error>> {
119 let base_url = cfg.base_url.trim_end_matches('/').to_string();
120 let inner = Arc::new(Inner {
121 api_key: cfg.api_key,
122 sli_id: cfg.sli_id,
123 base_url,
124 batch_size: cfg.batch_size,
125 buffer: Mutex::new(Vec::new()),
126 http: HttpClient::builder().timeout(Duration::from_secs(10)).build()?,
127 notify: Notify::new(),
128 });
129
130 let bg_inner = Arc::clone(&inner);
131 let interval = cfg.flush_interval;
132 let handle = tokio::spawn(async move {
133 loop {
134 tokio::time::sleep(interval).await;
135 flush_inner(&bg_inner).await;
136 }
137 });
138
139 Ok(Client {
140 inner,
141 _bg: Arc::new(handle),
142 })
143 }
144
145 pub async fn record(&self, event: Event) -> Result<(), Box<dyn std::error::Error>> {
146 let event_id = event.event_id.unwrap_or_else(|| Uuid::new_v4().to_string());
147 let timestamp = event.timestamp.unwrap_or_else(utc_now);
148 let dimensions = event.dimensions;
149
150 let ev = BufferedEvent {
151 event_id,
152 sli_id: self.inner.sli_id.clone(),
153 timestamp,
154 fields: event.fields,
155 dimensions,
156 };
157
158 let full = {
159 let mut buf = self.inner.buffer.lock().unwrap();
160 buf.push(ev);
161 buf.len() >= self.inner.batch_size
162 };
163
164 if full {
165 self.flush().await?;
166 }
167 Ok(())
168 }
169
170 pub async fn measure<F, Fut>(&self, f: F, opts: MeasureOpts) -> Result<(), Box<dyn std::error::Error>>
179 where
180 F: FnOnce(MeasureContext) -> Fut,
181 Fut: Future<Output = Result<(), Box<dyn std::error::Error>>>,
182 {
183 let event_id = opts.event_id.unwrap_or_else(|| Uuid::new_v4().to_string());
184 let ctx = MeasureContext::new();
185 let start = Instant::now();
186
187 let result = f(ctx.clone()).await;
188
189 let latency_ms = start.elapsed().as_millis() as u64;
190 let res = ctx.result();
191 let mut fields = opts.fields;
192 fields.insert("result".into(), Value::String(res));
193 fields.insert("latency_ms".into(), Value::Number(latency_ms.into()));
194 self.record(Event {
195 event_id: Some(event_id),
196 fields,
197 dimensions: opts.dimensions,
198 ..Default::default()
199 })
200 .await?;
201
202 result
203 }
204
205 pub async fn flush(&self) -> Result<(), Box<dyn std::error::Error>> {
206 flush_inner(&self.inner).await;
207 Ok(())
208 }
209}
210
211#[derive(Clone)]
216pub struct MeasureContext {
217 result: Arc<Mutex<String>>,
218}
219
220impl MeasureContext {
221 fn new() -> Self {
222 MeasureContext {
223 result: Arc::new(Mutex::new("ok".into())),
224 }
225 }
226
227 pub fn set_result(&self, r: &str) {
228 *self.result.lock().unwrap() = r.to_string();
229 }
230
231 fn result(&self) -> String {
232 self.result.lock().unwrap().clone()
233 }
234}
235
236async fn flush_inner(inner: &Arc<Inner>) {
241 let batch: Vec<BufferedEvent> = {
242 let mut buf = inner.buffer.lock().unwrap();
243 if buf.is_empty() {
244 return;
245 }
246 std::mem::take(&mut *buf)
247 };
248
249 let wire: Vec<WireEvent> = batch
250 .iter()
251 .map(|e| WireEvent {
252 event_id: &e.event_id,
253 sli_id: &e.sli_id,
254 timestamp: &e.timestamp,
255 fields: &e.fields,
256 dimensions: &e.dimensions,
257 })
258 .collect();
259
260 let payload = match serde_json::to_string(&Batch { events: wire }) {
261 Ok(p) => p,
262 Err(_) => return,
263 };
264
265 for attempt in 0u32..3 {
266 match inner
267 .http
268 .post(format!("{}/events", inner.base_url))
269 .header("Content-Type", "application/json")
270 .header("Authorization", format!("Bearer {}", inner.api_key))
271 .body(payload.clone())
272 .send()
273 .await
274 {
275 Ok(resp) => {
276 if resp.status().as_u16() == 204 {
277 return;
278 }
279 if resp.status().as_u16() == 429 {
280 let wait = resp
281 .headers()
282 .get("Retry-After")
283 .and_then(|v| v.to_str().ok())
284 .and_then(|s| s.parse::<f64>().ok())
285 .unwrap_or(2f64.powi(attempt as i32));
286 tokio::time::sleep(Duration::from_secs_f64(wait)).await;
287 continue;
288 }
289 return; }
291 Err(_) => {
292 if attempt < 2 {
293 tokio::time::sleep(Duration::from_secs(2u64.pow(attempt))).await;
294 }
295 }
296 }
297 }
298}
299
300fn utc_now() -> String {
301 let now = SystemTime::now()
302 .duration_since(UNIX_EPOCH)
303 .unwrap_or_default();
304 let secs = now.as_secs();
305 let millis = now.subsec_millis();
306 let (y, mo, d, h, mi, s) = epoch_to_parts(secs);
308 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z", y, mo, d, h, mi, s, millis)
309}
310
311fn epoch_to_parts(secs: u64) -> (u64, u64, u64, u64, u64, u64) {
312 let s = secs % 60;
313 let m = (secs / 60) % 60;
314 let h = (secs / 3600) % 24;
315 let days = secs / 86400;
316 let (year, month, day) = days_to_ymd(days);
318 (year, month, day, h, m, s)
319}
320
321fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
322 let mut y = 1970u64;
323 loop {
324 let dy = if is_leap(y) { 366 } else { 365 };
325 if days < dy { break; }
326 days -= dy;
327 y += 1;
328 }
329 let months = if is_leap(y) {
330 [31u64, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
331 } else {
332 [31u64, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
333 };
334 let mut mo = 1u64;
335 for &dm in &months {
336 if days < dm { break; }
337 days -= dm;
338 mo += 1;
339 }
340 (y, mo, days + 1)
341}
342
343fn is_leap(y: u64) -> bool {
344 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
345}