1use std::sync::Mutex;
2use crate::event::{Event, IngestEvent};
3use crate::signing;
4use crate::trace::Trace;
5use crate::types::TraceData;
6
7#[derive(Debug, Clone)]
9pub struct BloopClientBuilder {
10 endpoint: Option<String>,
11 project_key: Option<String>,
12 environment: String,
13 release: String,
14 source: String,
15 max_buffer_size: usize,
16}
17
18impl BloopClientBuilder {
19 pub fn new() -> Self {
20 Self {
21 endpoint: None,
22 project_key: None,
23 environment: "production".into(),
24 release: String::new(),
25 source: "rust".into(),
26 max_buffer_size: 20,
27 }
28 }
29
30 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
31 self.endpoint = Some(endpoint.into());
32 self
33 }
34
35 pub fn project_key(mut self, key: impl Into<String>) -> Self {
36 self.project_key = Some(key.into());
37 self
38 }
39
40 pub fn environment(mut self, env: impl Into<String>) -> Self {
41 self.environment = env.into();
42 self
43 }
44
45 pub fn release(mut self, release: impl Into<String>) -> Self {
46 self.release = release.into();
47 self
48 }
49
50 pub fn source(mut self, source: impl Into<String>) -> Self {
51 self.source = source.into();
52 self
53 }
54
55 pub fn max_buffer_size(mut self, size: usize) -> Self {
56 self.max_buffer_size = size;
57 self
58 }
59
60 pub fn build(self) -> Result<BloopClient, String> {
61 let endpoint = self.endpoint.ok_or("endpoint is required")?;
62 let project_key = self.project_key.ok_or("project_key is required")?;
63
64 Ok(BloopClient {
65 endpoint: endpoint.trim_end_matches('/').to_string(),
66 project_key,
67 environment: self.environment,
68 release: self.release,
69 source: self.source,
70 max_buffer_size: self.max_buffer_size,
71 error_buffer: Mutex::new(Vec::new()),
72 trace_buffer: Mutex::new(Vec::new()),
73 })
74 }
75}
76
77impl Default for BloopClientBuilder {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83pub struct BloopClient {
88 endpoint: String,
89 project_key: String,
90 environment: String,
91 release: String,
92 source: String,
93 max_buffer_size: usize,
94 error_buffer: Mutex<Vec<IngestEvent>>,
95 trace_buffer: Mutex<Vec<TraceData>>,
96}
97
98impl BloopClient {
99 pub fn builder() -> BloopClientBuilder {
100 BloopClientBuilder::new()
101 }
102
103 pub fn capture(&self, event: Event) {
105 let now = std::time::SystemTime::now()
106 .duration_since(std::time::UNIX_EPOCH)
107 .unwrap()
108 .as_secs() as i64;
109
110 let ingest = IngestEvent {
111 timestamp: now,
112 source: event.source.unwrap_or_else(|| self.source.clone()),
113 environment: self.environment.clone(),
114 release: self.release.clone(),
115 error_type: event.error_type,
116 message: event.message,
117 route_or_procedure: event.route_or_procedure,
118 screen: event.screen,
119 stack: event.stack,
120 http_status: event.http_status,
121 request_id: event.request_id,
122 user_id_hash: event.user_id_hash,
123 metadata: event.metadata,
124 };
125
126 let mut buf = self.error_buffer.lock().unwrap();
127 buf.push(ingest);
128 if buf.len() >= self.max_buffer_size {
129 let batch = std::mem::take(&mut *buf);
130 drop(buf);
131 self.send_error_batch(batch);
132 }
133 }
134
135 pub fn capture_error(&self, error_type: impl Into<String>, message: impl Into<String>) {
137 self.capture(Event {
138 error_type: error_type.into(),
139 message: message.into(),
140 ..Default::default()
141 });
142 }
143
144 pub fn start_trace(&self, name: impl Into<String>) -> Trace {
146 Trace::new(name)
147 }
148
149 pub fn send_trace(&self, trace: Trace) {
151 let data = trace.to_data();
152 let mut buf = self.trace_buffer.lock().unwrap();
153 buf.push(data);
154 if buf.len() >= self.max_buffer_size {
155 let batch = std::mem::take(&mut *buf);
156 drop(buf);
157 self.send_trace_batch(batch);
158 }
159 }
160
161 pub fn flush(&self) {
163 let errors = {
165 let mut buf = self.error_buffer.lock().unwrap();
166 std::mem::take(&mut *buf)
167 };
168 if !errors.is_empty() {
169 self.send_error_batch(errors);
170 }
171
172 let traces = {
174 let mut buf = self.trace_buffer.lock().unwrap();
175 std::mem::take(&mut *buf)
176 };
177 if !traces.is_empty() {
178 self.send_trace_batch(traces);
179 }
180 }
181
182 pub fn close(&self) {
184 self.flush();
185 }
186
187 fn post(&self, path: &str, body: &[u8]) {
190 let url = format!("{}{}", self.endpoint, path);
191 let signature = signing::sign(&self.project_key, body);
192
193 #[cfg(feature = "blocking")]
194 {
195 let _ = ureq::post(&url)
196 .set("Content-Type", "application/json")
197 .set("X-Signature", &signature)
198 .set("X-Project-Key", &self.project_key)
199 .send_bytes(body);
200 }
201
202 #[cfg(all(feature = "async", not(feature = "blocking")))]
203 {
204 let _ = (url, signature, body);
207 }
208
209 #[cfg(not(any(feature = "blocking", feature = "async")))]
210 {
211 let _ = (url, signature, body);
212 }
213 }
214
215 fn send_error_batch(&self, events: Vec<IngestEvent>) {
216 if let Ok(body) = serde_json::to_vec(&serde_json::json!({ "events": events })) {
217 self.post("/v1/ingest/batch", &body);
218 }
219 }
220
221 fn send_trace_batch(&self, traces: Vec<TraceData>) {
222 if let Ok(body) = serde_json::to_vec(&serde_json::json!({ "traces": traces })) {
223 self.post("/v1/traces/batch", &body);
224 }
225 }
226}
227
228impl std::fmt::Debug for BloopClient {
229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 f.debug_struct("BloopClient")
231 .field("endpoint", &self.endpoint)
232 .field("environment", &self.environment)
233 .field("source", &self.source)
234 .finish()
235 }
236}