use std::sync::Mutex;
use std::thread::{self, JoinHandle};
use std::time::Duration;
use chrono::{DateTime, Utc};
use reqwest::blocking::Client;
use serde::Serialize;
#[non_exhaustive]
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Action {
Create,
Read,
Update,
Delete,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize)]
pub enum Mechanism {
#[serde(rename = "API")]
Api,
#[serde(rename = "BTS")]
Bts,
#[serde(rename = "DESKTOP")]
Desktop,
#[serde(rename = "EMAIL")]
Email,
#[serde(rename = "FIXES")]
Fixes,
#[serde(rename = "FORCES")]
Forces,
#[serde(rename = "JIRA")]
Jira,
#[serde(rename = "MCP")]
Mcp,
#[serde(rename = "MELTS")]
Melts,
#[serde(rename = "MESSAGING")]
Messaging,
#[serde(rename = "MIGRATION")]
Migration,
#[serde(rename = "RETRIEVES")]
Retrieves,
#[serde(rename = "SCHEDULER")]
Scheduler,
#[serde(rename = "SMELLS")]
Smells,
#[serde(rename = "TASK")]
Task,
#[serde(rename = "WEB")]
Web,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize)]
pub struct Event {
pub action: Action,
pub author: String,
pub date: DateTime<Utc>,
pub mechanism: Mechanism,
pub metadata: serde_json::Value,
pub object: String,
pub object_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub author_anonymous: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author_ip: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author_role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author_user_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
impl Event {
pub fn builder(
action: Action,
author: String,
mechanism: Mechanism,
object: String,
object_id: String,
) -> EventBuilder {
EventBuilder {
event: Self {
action,
author,
date: Utc::now(),
mechanism,
metadata: serde_json::Value::Object(serde_json::Map::new()),
object,
object_id,
author_anonymous: None,
author_ip: None,
author_role: None,
author_user_agent: None,
session_id: None,
},
}
}
}
pub struct EventBuilder {
event: Event,
}
impl EventBuilder {
#[must_use]
pub const fn date(mut self, date: DateTime<Utc>) -> Self {
self.event.date = date;
self
}
#[must_use]
pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
self.event.metadata = metadata;
self
}
#[must_use]
pub const fn author_anonymous(mut self, value: bool) -> Self {
self.event.author_anonymous = Some(value);
self
}
#[must_use]
pub fn author_ip(mut self, value: String) -> Self {
self.event.author_ip = Some(value);
self
}
#[must_use]
pub fn author_role(mut self, value: String) -> Self {
self.event.author_role = Some(value);
self
}
#[must_use]
pub fn author_user_agent(mut self, value: String) -> Self {
self.event.author_user_agent = Some(value);
self
}
#[must_use]
pub fn session_id(mut self, value: String) -> Self {
self.event.session_id = Some(value);
self
}
pub fn build(self) -> Event {
self.event
}
}
fn send_with_retry(http: &Client, url: &str, event: &Event, retry_attempts: u32) {
for attempt in 0..retry_attempts {
let ok = http
.post(url)
.json(event)
.send()
.is_ok_and(|r| r.status().is_success());
if ok {
return;
}
if attempt.saturating_add(1) < retry_attempts {
let delay = 100u64.saturating_mul(2u64.saturating_pow(attempt));
thread::sleep(Duration::from_millis(delay));
}
}
}
pub struct EventResource {
base_url: String,
http: Client,
retry_attempts: u32,
pending: Mutex<Vec<JoinHandle<()>>>,
}
impl EventResource {
pub(crate) const fn new(base_url: String, http: Client, retry_attempts: u32) -> Self {
Self {
base_url,
http,
retry_attempts,
pending: Mutex::new(Vec::new()),
}
}
pub fn create(&self, event: Event) {
if !event.metadata.is_object() {
return;
}
let url = format!("{}/event", self.base_url.trim_end_matches('/'));
let http = self.http.clone();
let retry_attempts = self.retry_attempts;
let handle = thread::spawn(move || {
send_with_retry(&http, &url, &event, retry_attempts);
});
if let Ok(mut pending) = self.pending.lock() {
pending.retain(|h| !h.is_finished());
pending.push(handle);
}
}
pub fn flush(&self) {
let handles = self
.pending
.lock()
.map(|mut p| std::mem::take(&mut *p))
.unwrap_or_default();
for handle in handles {
let _ = handle.join();
}
}
}