fluidattacks_tracks/resources/
event.rs1use std::sync::Mutex;
4use std::thread::{self, JoinHandle};
5use std::time::Duration;
6
7use chrono::{DateTime, Utc};
8use reqwest::blocking::Client;
9use serde::Serialize;
10
11#[non_exhaustive]
13#[derive(Debug, Clone, Serialize)]
14#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
15pub enum Action {
16 Create,
17 Read,
18 Update,
19 Delete,
20}
21
22#[non_exhaustive]
24#[derive(Debug, Clone, Serialize)]
25pub enum Mechanism {
26 #[serde(rename = "API")]
27 Api,
28 #[serde(rename = "BTS")]
29 Bts,
30 #[serde(rename = "DESKTOP")]
31 Desktop,
32 #[serde(rename = "EMAIL")]
33 Email,
34 #[serde(rename = "FIXES")]
35 Fixes,
36 #[serde(rename = "FORCES")]
37 Forces,
38 #[serde(rename = "JIRA")]
39 Jira,
40 #[serde(rename = "MCP")]
41 Mcp,
42 #[serde(rename = "MELTS")]
43 Melts,
44 #[serde(rename = "MESSAGING")]
45 Messaging,
46 #[serde(rename = "MIGRATION")]
47 Migration,
48 #[serde(rename = "RETRIEVES")]
49 Retrieves,
50 #[serde(rename = "SCHEDULER")]
51 Scheduler,
52 #[serde(rename = "SMELLS")]
53 Smells,
54 #[serde(rename = "TASK")]
55 Task,
56 #[serde(rename = "WEB")]
57 Web,
58}
59
60#[non_exhaustive]
62#[derive(Debug, Clone, Serialize)]
63pub struct Event {
64 pub action: Action,
65 pub author: String,
66 pub date: DateTime<Utc>,
67 pub mechanism: Mechanism,
68 pub metadata: serde_json::Value,
74 pub object: String,
75 pub object_id: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub author_anonymous: Option<bool>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub author_ip: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub author_role: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub author_user_agent: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub session_id: Option<String>,
86}
87
88impl Event {
89 pub fn builder(
93 action: Action,
94 author: String,
95 mechanism: Mechanism,
96 object: String,
97 object_id: String,
98 ) -> EventBuilder {
99 EventBuilder {
100 event: Self {
101 action,
102 author,
103 date: Utc::now(),
104 mechanism,
105 metadata: serde_json::Value::Object(serde_json::Map::new()),
106 object,
107 object_id,
108 author_anonymous: None,
109 author_ip: None,
110 author_role: None,
111 author_user_agent: None,
112 session_id: None,
113 },
114 }
115 }
116}
117
118pub struct EventBuilder {
120 event: Event,
121}
122
123impl EventBuilder {
124 #[must_use]
125 pub const fn date(mut self, date: DateTime<Utc>) -> Self {
126 self.event.date = date;
127 self
128 }
129 #[must_use]
130 pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
131 self.event.metadata = metadata;
132 self
133 }
134
135 #[must_use]
136 pub const fn author_anonymous(mut self, value: bool) -> Self {
137 self.event.author_anonymous = Some(value);
138 self
139 }
140
141 #[must_use]
142 pub fn author_ip(mut self, value: String) -> Self {
143 self.event.author_ip = Some(value);
144 self
145 }
146
147 #[must_use]
148 pub fn author_role(mut self, value: String) -> Self {
149 self.event.author_role = Some(value);
150 self
151 }
152
153 #[must_use]
154 pub fn author_user_agent(mut self, value: String) -> Self {
155 self.event.author_user_agent = Some(value);
156 self
157 }
158
159 #[must_use]
160 pub fn session_id(mut self, value: String) -> Self {
161 self.event.session_id = Some(value);
162 self
163 }
164
165 pub fn build(self) -> Event {
166 self.event
167 }
168}
169
170fn send_with_retry(http: &Client, url: &str, event: &Event, retry_attempts: u32) {
171 for attempt in 0..retry_attempts {
172 let ok = http
173 .post(url)
174 .json(event)
175 .send()
176 .is_ok_and(|r| r.status().is_success());
177 if ok {
178 return;
179 }
180 if attempt.saturating_add(1) < retry_attempts {
181 let delay = 100u64.saturating_mul(2u64.saturating_pow(attempt));
182 thread::sleep(Duration::from_millis(delay));
183 }
184 }
185}
186
187pub struct EventResource {
189 base_url: String,
190 http: Client,
191 retry_attempts: u32,
192 pending: Mutex<Vec<JoinHandle<()>>>,
193}
194
195impl EventResource {
196 pub(crate) const fn new(base_url: String, http: Client, retry_attempts: u32) -> Self {
197 Self {
198 base_url,
199 http,
200 retry_attempts,
201 pending: Mutex::new(Vec::new()),
202 }
203 }
204
205 pub fn create(&self, event: Event) {
216 if !event.metadata.is_object() {
217 return;
218 }
219 let url = format!("{}/event", self.base_url.trim_end_matches('/'));
220 let http = self.http.clone();
221 let retry_attempts = self.retry_attempts;
222 let handle = thread::spawn(move || {
223 send_with_retry(&http, &url, &event, retry_attempts);
224 });
225 if let Ok(mut pending) = self.pending.lock() {
226 pending.retain(|h| !h.is_finished());
227 pending.push(handle);
228 }
229 }
230
231 pub fn flush(&self) {
236 let handles = self
237 .pending
238 .lock()
239 .map(|mut p| std::mem::take(&mut *p))
240 .unwrap_or_default();
241 for handle in handles {
242 let _ = handle.join();
243 }
244 }
245}