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 new(
91 action: Action,
92 author: String,
93 date: DateTime<Utc>,
94 mechanism: Mechanism,
95 metadata: serde_json::Value,
96 object: String,
97 object_id: String,
98 ) -> Self {
99 Event {
100 action,
101 author,
102 date,
103 mechanism,
104 metadata,
105 object,
106 object_id,
107 author_anonymous: None,
108 author_ip: None,
109 author_role: None,
110 author_user_agent: None,
111 session_id: None,
112 }
113 }
114}
115
116pub struct EventResource {
118 base_url: String,
119 http: Client,
120 retry_attempts: u32,
121 pending: Mutex<Vec<JoinHandle<()>>>,
122}
123
124impl EventResource {
125 pub(crate) fn new(base_url: String, http: Client, retry_attempts: u32) -> Self {
126 EventResource {
127 base_url,
128 http,
129 retry_attempts,
130 pending: Mutex::new(Vec::new()),
131 }
132 }
133
134 pub fn create(&self, event: Event) {
145 if !event.metadata.is_object() {
146 return;
147 }
148 let url = format!("{}/event", self.base_url.trim_end_matches('/'));
149 let http = self.http.clone();
150 let retry_attempts = self.retry_attempts;
151 let handle = thread::spawn(move || {
152 for attempt in 0..retry_attempts {
153 match http.post(&url).json(&event).send() {
154 Ok(resp) if resp.status().is_success() => return,
155 _ => {
156 if attempt + 1 < retry_attempts {
157 thread::sleep(Duration::from_millis(100 * 2u64.pow(attempt)));
158 }
159 }
160 }
161 }
162 });
163 if let Ok(mut pending) = self.pending.lock() {
164 pending.retain(|h| !h.is_finished());
165 pending.push(handle);
166 }
167 }
168
169 pub fn flush(&self) {
174 let handles = self
175 .pending
176 .lock()
177 .map(|mut p| std::mem::take(&mut *p))
178 .unwrap_or_default();
179 for handle in handles {
180 let _ = handle.join();
181 }
182 }
183}