shared_framework/logging/
correlation.rs1use crate::ErrorResult;
15use crate::doc::DocumentableDTO;
16use crate::utils::request_parser::{MultipartBody, UploadedFile};
17use crate::validation::Validate;
18use serde_json::Value;
19use std::any::Any;
20use std::collections::HashMap;
21use std::sync::{Arc, Mutex};
22use uuid::Uuid;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CorrelationFlow {
27 Once,
29 Start,
31 Continue,
33 End,
35}
36
37impl std::str::FromStr for CorrelationFlow {
38 type Err = String;
39 fn from_str(s: &str) -> Result<Self, Self::Err> {
40 match s.trim().to_uppercase().as_str() {
41 "ONCE" => Ok(Self::Once),
42 "START" => Ok(Self::Start),
43 "CONTINUE" => Ok(Self::Continue),
44 "END" => Ok(Self::End),
45 other => Err(format!("unknown flow {other}")),
46 }
47 }
48}
49
50impl std::fmt::Display for CorrelationFlow {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 let s = match self {
53 Self::Once => "ONCE",
54 Self::Start => "START",
55 Self::Continue => "CONTINUE",
56 Self::End => "END",
57 };
58 write!(f, "{s}")
59 }
60}
61
62#[derive(Debug, Clone)]
65pub struct CorrelationContext {
66 inner: Arc<Mutex<Inner>>,
67}
68
69#[derive(Debug)]
70struct Inner {
71 correlation_id: String,
72 request_id: String,
73 flow: CorrelationFlow,
74 user_id: Option<String>,
75 body: Option<Vec<u8>>,
76 headers: http::HeaderMap,
77 query_params: HashMap<String, String>,
78 multipart: Option<MultipartBody>,
79 data: HashMap<String, Box<dyn Any + Send + Sync>>,
80 path_params: HashMap<String, String>,
81 pagination_cursor: Option<String>,
83 pagination_limit: usize,
84}
85
86impl CorrelationContext {
87 pub fn new() -> Self {
90 let corr = Uuid::new_v4().to_string();
91 let req = hex::encode(rand::random::<[u8; 8]>());
92 Self {
93 inner: Arc::new(Mutex::new(Inner {
94 correlation_id: corr,
95 request_id: req,
96 flow: CorrelationFlow::Once,
97 user_id: None,
98 body: None,
99 headers: http::HeaderMap::new(),
100 query_params: HashMap::new(),
101 path_params: HashMap::new(),
102 multipart: None,
103 data: HashMap::new(),
104 pagination_cursor: None,
105 pagination_limit: 15,
106 })),
107 }
108 }
109
110 pub fn with_ids(correlation_id: &str, request_id: &str) -> Self {
113 let ctx = Self::new();
114 {
115 let mut inner = ctx.inner.lock().unwrap();
116 inner.correlation_id = correlation_id.to_string();
117 inner.request_id = request_id.to_string();
118 }
119 ctx
120 }
121
122 pub(crate) fn set_body(&self, body: Vec<u8>) {
123 self.inner.lock().unwrap().body = Some(body);
124 }
125
126 pub(crate) fn set_headers(&self, headers: http::HeaderMap) {
127 self.inner.lock().unwrap().headers = headers;
128 }
129
130 pub(crate) fn set_params(&self, params: HashMap<String, String>) {
131 self.inner.lock().unwrap().path_params = params;
132 }
133
134 pub(crate) fn set_query_params(&self, params: HashMap<String, String>) {
135 self.inner.lock().unwrap().query_params = params;
136 }
137
138 pub(crate) fn set_multipart(&self, multipart: MultipartBody) {
139 self.inner.lock().unwrap().multipart = Some(multipart);
140 }
141
142 pub fn is_multipart(&self) -> bool {
144 self.inner.lock().unwrap().multipart.is_some()
145 }
146
147 pub fn multipart(&self) -> Option<MultipartBody> {
149 self.inner.lock().unwrap().multipart.clone()
150 }
151
152 pub fn form_field(&self, name: &str) -> Option<String> {
154 self.inner
155 .lock()
156 .unwrap()
157 .multipart
158 .as_ref()
159 .and_then(|mp| mp.field(name))
160 .map(|s| s.to_string())
161 }
162
163 pub fn form_field_all(&self, name: &str) -> Vec<String> {
165 self.inner
166 .lock()
167 .unwrap()
168 .multipart
169 .as_ref()
170 .and_then(|mp| mp.fields.get(name).cloned())
171 .unwrap_or_default()
172 }
173
174 pub fn files(&self) -> Vec<UploadedFile> {
176 self.inner
177 .lock()
178 .unwrap()
179 .multipart
180 .as_ref()
181 .map(|mp| mp.files.clone())
182 .unwrap_or_default()
183 }
184
185 pub fn files_for(&self, field: &str) -> Vec<UploadedFile> {
187 self.files()
188 .into_iter()
189 .filter(|f| f.field_name == field)
190 .collect()
191 }
192
193 pub fn body_bytes(&self) -> Option<Vec<u8>> {
195 self.inner.lock().unwrap().body.clone()
196 }
197
198 pub fn body_string(&self) -> Result<String, ErrorResult> {
204 if let Some(payload) = self.form_field("body") {
205 return Ok(payload);
206 }
207 let body_bytes_opt = self.inner.lock().unwrap().body.clone();
208 let Some(body_bytes) = body_bytes_opt else {
209 return Err(ErrorResult::bad_request("no body"));
210 };
211 String::from_utf8(body_bytes).map_err(|_| ErrorResult::bad_request("invalid body"))
212 }
213
214 pub fn body<T>(&self) -> Result<T, ErrorResult>
225 where
226 T: DocumentableDTO + Validate,
227 {
228 if let Some(mp) = self.inner.lock().unwrap().multipart.clone() {
231 return self.multipart_body(&mp);
232 }
233
234 let body_bytes_opt = self.inner.lock().unwrap().body.clone();
236 let Some(body_bytes) = body_bytes_opt else {
237 return Err(ErrorResult::bad_request("no body"));
238 };
239
240 let parsed_body: Option<T> = serde_json::from_slice(&body_bytes).ok().or_else(|| {
243 serde_urlencoded::from_bytes::<Value>(&body_bytes)
244 .ok()
245 .and_then(|v| serde_json::from_value(v).ok())
246 });
247 let Some(parsed_body) = parsed_body else {
248 return Err(ErrorResult::bad_request("invalid body"));
249 };
250
251 if let Err(validated) = parsed_body.validate() {
253 return Err(ErrorResult::new(
254 validated.message,
255 Some(Value::String(validated.field)),
256 400,
257 ));
258 }
259
260 Ok(parsed_body.clone())
262 }
263
264 fn multipart_body<T>(&self, mp: &MultipartBody) -> Result<T, ErrorResult>
266 where
267 T: DocumentableDTO + Validate,
268 {
269 if let Some(payload) = mp.field("body") {
272 let parsed: T = serde_json::from_str(payload)
273 .map_err(|_| ErrorResult::bad_request("invalid body"))?;
274 return self.validated(parsed);
275 }
276 let mut map = serde_json::Map::new();
279 for (k, vs) in &mp.fields {
280 let v = if vs.len() == 1 {
281 serde_json::Value::String(vs[0].clone())
282 } else {
283 serde_json::Value::Array(
284 vs.iter().cloned().map(serde_json::Value::String).collect(),
285 )
286 };
287 map.insert(k.clone(), v);
288 }
289 if map.is_empty() {
290 return Err(ErrorResult::bad_request("no body"));
291 }
292 let parsed: T = serde_json::from_value(serde_json::Value::Object(map))
293 .map_err(|_| ErrorResult::bad_request("invalid body"))?;
294 self.validated(parsed)
295 }
296
297 fn validated<T>(&self, parsed: T) -> Result<T, ErrorResult>
298 where
299 T: DocumentableDTO + Validate,
300 {
301 if let Err(e) = parsed.validate() {
302 return Err(ErrorResult::bad_request(e.message));
303 }
304 Ok(parsed)
305 }
306
307 pub fn headers(&self) -> http::HeaderMap {
309 self.inner.lock().unwrap().headers.clone()
310 }
311
312 pub fn header(&self, name: &str) -> Option<String> {
314 self.inner
315 .lock()
316 .unwrap()
317 .headers
318 .get(name)
319 .and_then(|v| v.to_str().ok())
320 .map(|s| s.to_string())
321 }
322
323 pub fn query_params(&self) -> HashMap<String, String> {
325 self.inner.lock().unwrap().query_params.clone()
326 }
327
328 pub fn query_param(&self, name: &str) -> Option<String> {
330 self.inner.lock().unwrap().query_params.get(name).cloned()
331 }
332
333 pub fn query_param_or(&self, name: &str, default: &str) -> String {
335 self.query_param(name)
336 .unwrap_or_else(|| default.to_string())
337 }
338
339 pub fn path_params(&self) -> HashMap<String, String> {
341 self.inner.lock().unwrap().path_params.clone()
342 }
343
344 pub fn path_param(&self, name: &str) -> Option<String> {
346 self.inner.lock().unwrap().path_params.get(name).cloned()
347 }
348
349 pub fn path_param_or(&self, name: &str, default: &str) -> String {
351 self.query_param(name)
352 .unwrap_or_else(|| default.to_string())
353 }
354
355 pub fn correlation_id(&self) -> String {
357 self.inner.lock().unwrap().correlation_id.clone()
358 }
359
360 pub fn request_id(&self) -> String {
362 self.inner.lock().unwrap().request_id.clone()
363 }
364
365 pub fn set_request_id(&self, id: &str) {
367 self.inner.lock().unwrap().request_id = id.to_string();
368 }
369
370 pub fn set_correlation_id(&self, id: &str) {
372 self.inner.lock().unwrap().correlation_id = id.to_string();
373 }
374
375 pub fn flow(&self) -> CorrelationFlow {
377 self.inner.lock().unwrap().flow
378 }
379
380 pub fn set_flow(&self, flow: CorrelationFlow) {
382 self.inner.lock().unwrap().flow = flow;
383 }
384
385 pub fn with_flow(self, flow: CorrelationFlow) -> Self {
387 self.set_flow(flow);
388 self
389 }
390
391 pub fn with_correlation_id(self, id: &str) -> Self {
393 self.set_correlation_id(id);
394 self
395 }
396
397 pub fn user_id(&self) -> Option<String> {
399 self.inner.lock().unwrap().user_id.clone()
400 }
401
402 pub fn set_user_id(&self, id: Option<String>) {
404 self.inner.lock().unwrap().user_id = id;
405 }
406
407 pub fn set<T>(&self, key: &str, value: T)
409 where
410 T: Any + Send + Sync,
411 {
412 self.inner
413 .lock()
414 .unwrap()
415 .data
416 .insert(key.to_string(), Box::new(value));
417 }
418
419 pub fn get<T>(&self, key: &str) -> Option<T>
422 where
423 T: Any + Clone,
424 {
425 self.inner
426 .lock()
427 .unwrap()
428 .data
429 .get(key)
430 .and_then(|value| value.downcast_ref::<T>())
431 .cloned()
432 }
433
434 pub fn set_string(&self, key: &str, value: impl Into<String>) {
436 self.set(key, value.into());
437 }
438
439 pub fn get_string(&self, key: &str) -> Option<String> {
441 self.get(key)
442 }
443
444 pub fn set_bool(&self, key: &str, value: bool) {
446 self.set(key, value);
447 }
448
449 pub fn get_bool(&self, key: &str) -> Option<bool> {
451 self.get(key)
452 }
453
454 pub fn set_number(&self, key: &str, value: f64) {
456 self.set(key, value);
457 }
458
459 pub fn get_number(&self, key: &str) -> Option<f64> {
461 self.get(key)
462 }
463
464 pub fn pagination_cursor(&self) -> Option<String> {
466 self.inner.lock().unwrap().pagination_cursor.clone()
467 }
468
469 pub fn pagination_limit(&self) -> usize {
471 self.inner.lock().unwrap().pagination_limit
472 }
473
474 pub fn set_pagination(&self, cursor: Option<String>, limit: usize) {
476 let mut inner = self.inner.lock().unwrap();
477 inner.pagination_cursor = cursor;
478 inner.pagination_limit = limit;
479 }
480
481 pub fn client_ip(
484 headers: &http::HeaderMap,
485 remote_addr: Option<std::net::SocketAddr>,
486 ) -> String {
487 if let Some(v) = headers.get("x-forwarded-for").and_then(|h| h.to_str().ok()) {
488 if let Some(first) = v.split(',').next() {
489 let ip = first.trim();
490 if !ip.is_empty() {
491 return ip.to_string();
492 }
493 }
494 }
495 if let Some(v) = headers.get("x-real-ip").and_then(|h| h.to_str().ok()) {
496 return v.to_string();
497 }
498 remote_addr
499 .map(|a| a.ip().to_string())
500 .unwrap_or_else(|| "unknown".to_string())
501 }
502}
503
504impl Default for CorrelationContext {
505 fn default() -> Self {
506 Self::new()
507 }
508}
509
510tokio::task_local! {
511 pub static CORRELATION_CTX: CorrelationContext;
513}
514
515#[cfg(test)]
516mod tests {
517 use super::CorrelationContext;
518
519 #[test]
520 fn stores_and_returns_owned_typed_values() {
521 let context = CorrelationContext::new();
522 context.set("count", 42_u32);
523
524 assert_eq!(context.get::<u32>("count"), Some(42));
525 assert_eq!(context.get::<String>("count"), None);
526 assert_eq!(context.get::<u32>("missing"), None);
527 }
528
529 #[test]
530 fn convenience_accessors_store_and_return_values() {
531 let context = CorrelationContext::new();
532 context.set_string("name", "Ada");
533 context.set_bool("enabled", true);
534 context.set_number("ratio", 1.5);
535
536 assert_eq!(context.get_string("name"), Some("Ada".to_string()));
537 assert_eq!(context.get_bool("enabled"), Some(true));
538 assert_eq!(context.get_number("ratio"), Some(1.5));
539 }
540}