use reqwest::Client as HttpClient;
use serde::Serialize;
use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::sync::Notify;
use tokio::task::JoinHandle;
use uuid::Uuid;
#[derive(Clone)]
pub struct Config {
pub api_key: String,
pub sli_id: String,
pub base_url: String,
pub batch_size: usize,
pub flush_interval: Duration,
}
impl Default for Config {
fn default() -> Self {
Config {
api_key: String::new(),
sli_id: String::new(),
base_url: "https://api.levelops.io".into(),
batch_size: 100,
flush_interval: Duration::from_secs(1),
}
}
}
#[derive(Default)]
pub struct Event {
pub event_id: Option<String>,
pub timestamp: Option<String>,
pub fields: HashMap<String, Value>,
pub dimensions: HashMap<String, String>,
}
pub struct MeasureOpts {
pub fields: HashMap<String, Value>,
pub dimensions: HashMap<String, String>,
pub event_id: Option<String>,
}
impl MeasureOpts {
pub fn new(dims: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
MeasureOpts {
fields: HashMap::new(),
dimensions: dims.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
event_id: None,
}
}
pub fn event_id(mut self, id: impl Into<String>) -> Self {
self.event_id = Some(id.into());
self
}
pub fn fields(mut self, f: HashMap<String, Value>) -> Self {
self.fields = f;
self
}
}
#[derive(Serialize)]
struct WireEvent<'a> {
event_id: &'a str,
sli_id: &'a str,
timestamp: &'a str,
fields: &'a HashMap<String, Value>,
dimensions: &'a HashMap<String, String>,
}
#[derive(Serialize)]
struct Batch<'a> {
events: Vec<WireEvent<'a>>,
}
struct BufferedEvent {
event_id: String,
sli_id: String,
timestamp: String,
fields: HashMap<String, Value>,
dimensions: HashMap<String, String>,
}
struct Inner {
api_key: String,
sli_id: String,
base_url: String,
batch_size: usize,
buffer: Mutex<Vec<BufferedEvent>>,
http: HttpClient,
notify: Notify,
}
#[derive(Clone)]
pub struct Client {
inner: Arc<Inner>,
_bg: Arc<JoinHandle<()>>,
}
impl Client {
pub fn new(cfg: Config) -> Result<Self, Box<dyn std::error::Error>> {
let base_url = cfg.base_url.trim_end_matches('/').to_string();
let inner = Arc::new(Inner {
api_key: cfg.api_key,
sli_id: cfg.sli_id,
base_url,
batch_size: cfg.batch_size,
buffer: Mutex::new(Vec::new()),
http: HttpClient::builder().timeout(Duration::from_secs(10)).build()?,
notify: Notify::new(),
});
let bg_inner = Arc::clone(&inner);
let interval = cfg.flush_interval;
let handle = tokio::spawn(async move {
loop {
tokio::time::sleep(interval).await;
flush_inner(&bg_inner).await;
}
});
Ok(Client {
inner,
_bg: Arc::new(handle),
})
}
pub async fn record(&self, event: Event) -> Result<(), Box<dyn std::error::Error>> {
let event_id = event.event_id.unwrap_or_else(|| Uuid::new_v4().to_string());
let timestamp = event.timestamp.unwrap_or_else(utc_now);
let dimensions = event.dimensions;
let ev = BufferedEvent {
event_id,
sli_id: self.inner.sli_id.clone(),
timestamp,
fields: event.fields,
dimensions,
};
let full = {
let mut buf = self.inner.buffer.lock().unwrap();
buf.push(ev);
buf.len() >= self.inner.batch_size
};
if full {
self.flush().await?;
}
Ok(())
}
pub async fn measure<F, Fut>(&self, f: F, opts: MeasureOpts) -> Result<(), Box<dyn std::error::Error>>
where
F: FnOnce(MeasureContext) -> Fut,
Fut: Future<Output = Result<(), Box<dyn std::error::Error>>>,
{
let event_id = opts.event_id.unwrap_or_else(|| Uuid::new_v4().to_string());
let ctx = MeasureContext::new();
let start = Instant::now();
let result = f(ctx.clone()).await;
let latency_ms = start.elapsed().as_millis() as u64;
let res = ctx.result();
let mut fields = opts.fields;
fields.insert("result".into(), Value::String(res));
fields.insert("latency_ms".into(), Value::Number(latency_ms.into()));
self.record(Event {
event_id: Some(event_id),
fields,
dimensions: opts.dimensions,
..Default::default()
})
.await?;
result
}
pub async fn flush(&self) -> Result<(), Box<dyn std::error::Error>> {
flush_inner(&self.inner).await;
Ok(())
}
}
#[derive(Clone)]
pub struct MeasureContext {
result: Arc<Mutex<String>>,
}
impl MeasureContext {
fn new() -> Self {
MeasureContext {
result: Arc::new(Mutex::new("ok".into())),
}
}
pub fn set_result(&self, r: &str) {
*self.result.lock().unwrap() = r.to_string();
}
fn result(&self) -> String {
self.result.lock().unwrap().clone()
}
}
async fn flush_inner(inner: &Arc<Inner>) {
let batch: Vec<BufferedEvent> = {
let mut buf = inner.buffer.lock().unwrap();
if buf.is_empty() {
return;
}
std::mem::take(&mut *buf)
};
let wire: Vec<WireEvent> = batch
.iter()
.map(|e| WireEvent {
event_id: &e.event_id,
sli_id: &e.sli_id,
timestamp: &e.timestamp,
fields: &e.fields,
dimensions: &e.dimensions,
})
.collect();
let payload = match serde_json::to_string(&Batch { events: wire }) {
Ok(p) => p,
Err(_) => return,
};
for attempt in 0u32..3 {
match inner
.http
.post(format!("{}/events", inner.base_url))
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", inner.api_key))
.body(payload.clone())
.send()
.await
{
Ok(resp) => {
if resp.status().as_u16() == 204 {
return;
}
if resp.status().as_u16() == 429 {
let wait = resp
.headers()
.get("Retry-After")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(2f64.powi(attempt as i32));
tokio::time::sleep(Duration::from_secs_f64(wait)).await;
continue;
}
return; }
Err(_) => {
if attempt < 2 {
tokio::time::sleep(Duration::from_secs(2u64.pow(attempt))).await;
}
}
}
}
}
fn utc_now() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let millis = now.subsec_millis();
let (y, mo, d, h, mi, s) = epoch_to_parts(secs);
format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z", y, mo, d, h, mi, s, millis)
}
fn epoch_to_parts(secs: u64) -> (u64, u64, u64, u64, u64, u64) {
let s = secs % 60;
let m = (secs / 60) % 60;
let h = (secs / 3600) % 24;
let days = secs / 86400;
let (year, month, day) = days_to_ymd(days);
(year, month, day, h, m, s)
}
fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
let mut y = 1970u64;
loop {
let dy = if is_leap(y) { 366 } else { 365 };
if days < dy { break; }
days -= dy;
y += 1;
}
let months = if is_leap(y) {
[31u64, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31u64, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut mo = 1u64;
for &dm in &months {
if days < dm { break; }
days -= dm;
mo += 1;
}
(y, mo, days + 1)
}
fn is_leap(y: u64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}