Skip to main content

armature_core/
error_correlation.rs

1//! Error Correlation Module
2//!
3//! Provides error correlation and distributed tracing capabilities for tracking
4//! errors across services and request chains.
5//!
6//! # Features
7//!
8//! - ✅ Correlation ID generation and propagation
9//! - ✅ Trace/Span ID support for distributed tracing
10//! - ✅ Error chain tracking (parent-child relationships)
11//! - ✅ Causation chain for root cause analysis
12//! - ✅ Correlation context propagation
13//! - ✅ Middleware for automatic correlation
14//! - ✅ OpenTelemetry-compatible trace context
15//!
16//! # Example
17//!
18//! ```rust,ignore
19//! use armature_core::error_correlation::*;
20//!
21//! // Create correlation context
22//! let ctx = CorrelationContext::new()
23//!     .with_user_id("user-123")
24//!     .with_service("auth-service");
25//!
26//! // Track a correlated error
27//! let error = CorrelatedError::new("Database connection failed")
28//!     .with_context(ctx)
29//!     .caused_by("Connection timeout");
30//! ```
31
32use crate::middleware::{Middleware, Next};
33use crate::{Error, HttpRequest, HttpResponse};
34use async_trait::async_trait;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use std::sync::Arc;
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::time::{SystemTime, UNIX_EPOCH};
40use tokio::sync::RwLock;
41
42// ============================================================================
43// Correlation ID Generation
44// ============================================================================
45
46/// Unique ID generator with different strategies.
47#[derive(Debug, Clone, Copy, Default)]
48pub enum IdGenerationStrategy {
49    /// UUID v4 (random)
50    #[default]
51    UuidV4,
52    /// UUID v7 (time-ordered)
53    UuidV7,
54    /// Snowflake-style ID (timestamp + machine + sequence)
55    Snowflake,
56    /// ULID (Universally Unique Lexicographically Sortable Identifier)
57    Ulid,
58    /// Short ID (8 characters, base62)
59    Short,
60}
61
62/// Counter for snowflake IDs
63static SEQUENCE_COUNTER: AtomicU64 = AtomicU64::new(0);
64
65impl IdGenerationStrategy {
66    /// Generate a new ID using this strategy.
67    pub fn generate(&self) -> String {
68        match self {
69            IdGenerationStrategy::UuidV4 => uuid::Uuid::new_v4().to_string(),
70            IdGenerationStrategy::UuidV7 => {
71                // UUID v7 - time-ordered UUID
72                let timestamp = SystemTime::now()
73                    .duration_since(UNIX_EPOCH)
74                    .unwrap_or_default()
75                    .as_millis() as u64;
76
77                let random_bytes: [u8; 10] = rand_bytes();
78                let mut bytes = [0u8; 16];
79
80                // First 6 bytes: timestamp (48 bits)
81                bytes[0..6].copy_from_slice(&timestamp.to_be_bytes()[2..8]);
82                // Set version 7
83                bytes[6] = (random_bytes[0] & 0x0F) | 0x70;
84                bytes[7] = random_bytes[1];
85                // Set variant
86                bytes[8] = (random_bytes[2] & 0x3F) | 0x80;
87                bytes[9..16].copy_from_slice(&random_bytes[3..10]);
88
89                uuid::Uuid::from_bytes(bytes).to_string()
90            }
91            IdGenerationStrategy::Snowflake => {
92                let timestamp = SystemTime::now()
93                    .duration_since(UNIX_EPOCH)
94                    .unwrap_or_default()
95                    .as_millis() as u64;
96
97                let seq = SEQUENCE_COUNTER.fetch_add(1, Ordering::SeqCst) & 0xFFF;
98                let machine_id = std::process::id() as u64 & 0x3FF;
99
100                // 41 bits timestamp + 10 bits machine + 12 bits sequence
101                let id = ((timestamp & 0x1FFFFFFFFFF) << 22) | (machine_id << 12) | seq;
102                format!("{:016x}", id)
103            }
104            IdGenerationStrategy::Ulid => {
105                let timestamp = SystemTime::now()
106                    .duration_since(UNIX_EPOCH)
107                    .unwrap_or_default()
108                    .as_millis() as u64;
109
110                let random: [u8; 10] = rand_bytes();
111
112                // Encode timestamp (6 bytes) + random (10 bytes) in Crockford base32
113                let mut result = String::with_capacity(26);
114                let alphabet = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
115
116                // Encode timestamp (10 chars)
117                for i in (0..10).rev() {
118                    let shift = i * 5;
119                    if shift < 48 {
120                        let idx = ((timestamp >> shift) & 0x1F) as usize;
121                        result.push(alphabet[idx] as char);
122                    }
123                }
124
125                // Encode random (16 chars)
126                let mut bits: u128 = 0;
127                for &b in &random {
128                    bits = (bits << 8) | b as u128;
129                }
130                for i in (0..16).rev() {
131                    let idx = ((bits >> (i * 5)) & 0x1F) as usize;
132                    result.push(alphabet[idx] as char);
133                }
134
135                result
136            }
137            IdGenerationStrategy::Short => {
138                let random: [u8; 6] = rand_bytes::<6>();
139                let alphabet = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
140                let mut result = String::with_capacity(8);
141
142                for b in random {
143                    result.push(alphabet[(b % 62) as usize] as char);
144                }
145                // Add 2 more chars from timestamp
146                let ts = SystemTime::now()
147                    .duration_since(UNIX_EPOCH)
148                    .unwrap_or_default()
149                    .as_nanos() as u64;
150                result.push(alphabet[(ts % 62) as usize] as char);
151                result.push(alphabet[((ts / 62) % 62) as usize] as char);
152
153                result
154            }
155        }
156    }
157}
158
159/// Generate random bytes
160/// Generate a random lowercase-hex identifier of exactly `hex_len` characters.
161fn random_hex_id(hex_len: usize) -> String {
162    let mut s = String::with_capacity(hex_len);
163    while s.len() < hex_len {
164        let bytes: [u8; 16] = rand_bytes();
165        for b in bytes {
166            use std::fmt::Write;
167            let _ = write!(s, "{:02x}", b);
168        }
169    }
170    s.truncate(hex_len);
171    s
172}
173
174/// Global counter mixed into every `rand_bytes` seed so that two calls can
175/// never derive the same PRNG state, even when they land on the same
176/// timestamp reading (e.g. concurrent calls from different threads, or a
177/// tight same-thread loop where clock resolution is coarser than call
178/// latency).
179static RAND_BYTES_COUNTER: AtomicU64 = AtomicU64::new(0);
180
181fn rand_bytes<const N: usize>() -> [u8; N] {
182    let mut bytes = [0u8; N];
183    // Simple PRNG seeded from timestamp XORed with a constant, additionally
184    // mixed with a monotonically increasing atomic counter. The counter
185    // guarantees uniqueness of the seed (and therefore the output) across
186    // calls regardless of clock resolution or thread interleaving.
187    let seed = SystemTime::now()
188        .duration_since(UNIX_EPOCH)
189        .unwrap_or_default()
190        .as_nanos() as u64;
191    let counter = RAND_BYTES_COUNTER.fetch_add(1, Ordering::Relaxed);
192
193    let mut state = seed ^ 0xDEADBEEF ^ counter.wrapping_mul(0x9E3779B97F4A7C15);
194    for b in bytes.iter_mut() {
195        state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
196        *b = (state >> 33) as u8;
197    }
198    bytes
199}
200
201// ============================================================================
202// Correlation Context
203// ============================================================================
204
205/// Standard HTTP headers for correlation.
206pub mod headers {
207    /// Correlation ID header (custom)
208    pub const CORRELATION_ID: &str = "X-Correlation-ID";
209    /// Request ID header (custom)
210    pub const REQUEST_ID: &str = "X-Request-ID";
211    /// Trace ID header (W3C Trace Context)
212    pub const TRACE_PARENT: &str = "traceparent";
213    /// Trace state header (W3C Trace Context)
214    pub const TRACE_STATE: &str = "tracestate";
215    /// B3 trace ID (Zipkin)
216    pub const B3_TRACE_ID: &str = "X-B3-TraceId";
217    /// B3 span ID (Zipkin)
218    pub const B3_SPAN_ID: &str = "X-B3-SpanId";
219    /// B3 parent span ID (Zipkin)
220    pub const B3_PARENT_SPAN_ID: &str = "X-B3-ParentSpanId";
221    /// B3 sampled (Zipkin)
222    pub const B3_SAMPLED: &str = "X-B3-Sampled";
223    /// Causation ID header
224    pub const CAUSATION_ID: &str = "X-Causation-ID";
225    /// Session ID header
226    pub const SESSION_ID: &str = "X-Session-ID";
227}
228
229/// Correlation context that can be propagated across services.
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct CorrelationContext {
232    /// Correlation ID - groups related requests/errors
233    pub correlation_id: String,
234    /// Request ID - unique per request
235    pub request_id: String,
236    /// Trace ID (for distributed tracing)
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub trace_id: Option<String>,
239    /// Span ID (current span in trace)
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub span_id: Option<String>,
242    /// Parent span ID
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub parent_span_id: Option<String>,
245    /// Causation ID (what caused this request)
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub causation_id: Option<String>,
248    /// Session ID
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub session_id: Option<String>,
251    /// Service name
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub service: Option<String>,
254    /// Service version
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub service_version: Option<String>,
257    /// User ID (if authenticated)
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub user_id: Option<String>,
260    /// Tenant ID (for multi-tenant systems)
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub tenant_id: Option<String>,
263    /// Custom baggage items
264    #[serde(skip_serializing_if = "HashMap::is_empty", default)]
265    pub baggage: HashMap<String, String>,
266    /// Sampling decision for traces
267    pub sampled: bool,
268    /// Timestamp when context was created
269    pub created_at: u64,
270}
271
272impl Default for CorrelationContext {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278impl CorrelationContext {
279    /// Create a new correlation context with generated IDs.
280    pub fn new() -> Self {
281        let strategy = IdGenerationStrategy::UuidV4;
282        Self {
283            correlation_id: strategy.generate(),
284            request_id: strategy.generate(),
285            trace_id: None,
286            span_id: None,
287            parent_span_id: None,
288            causation_id: None,
289            session_id: None,
290            service: None,
291            service_version: None,
292            user_id: None,
293            tenant_id: None,
294            baggage: HashMap::new(),
295            sampled: true,
296            created_at: SystemTime::now()
297                .duration_since(UNIX_EPOCH)
298                .unwrap_or_default()
299                .as_millis() as u64,
300        }
301    }
302
303    /// Create context with a specific ID strategy.
304    pub fn with_strategy(strategy: IdGenerationStrategy) -> Self {
305        Self {
306            correlation_id: strategy.generate(),
307            request_id: strategy.generate(),
308            ..Default::default()
309        }
310    }
311
312    /// Create a child context (for downstream calls).
313    pub fn child(&self) -> Self {
314        let strategy = IdGenerationStrategy::UuidV4;
315        Self {
316            correlation_id: self.correlation_id.clone(),
317            request_id: strategy.generate(),
318            trace_id: self.trace_id.clone(),
319            span_id: Some(strategy.generate()),
320            parent_span_id: self.span_id.clone(),
321            causation_id: Some(self.request_id.clone()),
322            session_id: self.session_id.clone(),
323            service: self.service.clone(),
324            service_version: self.service_version.clone(),
325            user_id: self.user_id.clone(),
326            tenant_id: self.tenant_id.clone(),
327            baggage: self.baggage.clone(),
328            sampled: self.sampled,
329            created_at: SystemTime::now()
330                .duration_since(UNIX_EPOCH)
331                .unwrap_or_default()
332                .as_millis() as u64,
333        }
334    }
335
336    /// Set the correlation ID.
337    pub fn correlation_id(mut self, id: impl Into<String>) -> Self {
338        self.correlation_id = id.into();
339        self
340    }
341
342    /// Set the trace ID.
343    pub fn trace_id(mut self, id: impl Into<String>) -> Self {
344        self.trace_id = Some(id.into());
345        self
346    }
347
348    /// Set the span ID.
349    pub fn span_id(mut self, id: impl Into<String>) -> Self {
350        self.span_id = Some(id.into());
351        self
352    }
353
354    /// Set the causation ID.
355    pub fn with_causation(mut self, id: impl Into<String>) -> Self {
356        self.causation_id = Some(id.into());
357        self
358    }
359
360    /// Set the session ID.
361    pub fn with_session(mut self, id: impl Into<String>) -> Self {
362        self.session_id = Some(id.into());
363        self
364    }
365
366    /// Set the service name.
367    pub fn with_service(mut self, service: impl Into<String>) -> Self {
368        self.service = Some(service.into());
369        self
370    }
371
372    /// Set the service version.
373    pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
374        self.service_version = Some(version.into());
375        self
376    }
377
378    /// Set the user ID.
379    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
380        self.user_id = Some(user_id.into());
381        self
382    }
383
384    /// Set the tenant ID.
385    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
386        self.tenant_id = Some(tenant_id.into());
387        self
388    }
389
390    /// Add a baggage item.
391    pub fn with_baggage(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
392        self.baggage.insert(key.into(), value.into());
393        self
394    }
395
396    /// Set sampling decision.
397    pub fn with_sampled(mut self, sampled: bool) -> Self {
398        self.sampled = sampled;
399        self
400    }
401
402    /// Extract correlation context from HTTP request headers.
403    pub fn from_request(req: &HttpRequest) -> Self {
404        let mut ctx = Self::new();
405
406        // Extract correlation ID
407        if let Some(id) = req.headers.get(headers::CORRELATION_ID).or_else(|| {
408            req.headers
409                .get(headers::CORRELATION_ID.to_lowercase().as_str())
410        }) {
411            ctx.correlation_id = id.to_owned();
412        }
413
414        // Extract request ID
415        if let Some(id) = req
416            .headers
417            .get(headers::REQUEST_ID)
418            .or_else(|| req.headers.get(headers::REQUEST_ID.to_lowercase().as_str()))
419        {
420            ctx.request_id = id.to_owned();
421        }
422
423        // Extract W3C trace context
424        if let Some(traceparent) = req.headers.get(headers::TRACE_PARENT)
425            && let Some((trace_id, span_id, sampled)) = parse_traceparent(traceparent)
426        {
427            ctx.trace_id = Some(trace_id);
428            ctx.parent_span_id = Some(span_id);
429            ctx.sampled = sampled;
430            // Generate new span ID for this service
431            ctx.span_id = Some(IdGenerationStrategy::Short.generate());
432        }
433
434        // Fall back to B3 headers (Zipkin)
435        if ctx.trace_id.is_none()
436            && let Some(id) = req.headers.get(headers::B3_TRACE_ID)
437        {
438            ctx.trace_id = Some(id.to_owned());
439        }
440        if ctx.span_id.is_none()
441            && let Some(id) = req.headers.get(headers::B3_SPAN_ID)
442        {
443            ctx.parent_span_id = Some(id.to_owned());
444            ctx.span_id = Some(IdGenerationStrategy::Short.generate());
445        }
446
447        // Extract causation ID
448        if let Some(id) = req.headers.get(headers::CAUSATION_ID) {
449            ctx.causation_id = Some(id.to_owned());
450        }
451
452        // Extract session ID
453        if let Some(id) = req.headers.get(headers::SESSION_ID) {
454            ctx.session_id = Some(id.to_owned());
455        }
456
457        ctx
458    }
459
460    /// Inject correlation context into HTTP request headers.
461    pub fn inject_into_request(&self, req: &mut HttpRequest) {
462        // Names are `&str` and values are borrowed where they can be: the header
463        // map interns the name and copies the value once, so a `to_string` on
464        // either side would be a second allocation for nothing.
465        req.headers
466            .insert(headers::CORRELATION_ID, self.correlation_id.as_str());
467        req.headers
468            .insert(headers::REQUEST_ID, self.request_id.as_str());
469
470        if let Some(ref trace_id) = self.trace_id {
471            let span_id = self.span_id.as_deref().unwrap_or("0000000000000000");
472            let sampled = if self.sampled { "01" } else { "00" };
473            let traceparent = format!("00-{trace_id}-{span_id}-{sampled}");
474            req.headers.insert(headers::TRACE_PARENT, traceparent);
475
476            // Also add B3 headers for Zipkin compatibility
477            req.headers.insert(headers::B3_TRACE_ID, trace_id.as_str());
478            req.headers.insert(headers::B3_SPAN_ID, span_id);
479            if let Some(ref parent) = self.parent_span_id {
480                req.headers
481                    .insert(headers::B3_PARENT_SPAN_ID, parent.as_str());
482            }
483            req.headers
484                .insert(headers::B3_SAMPLED, if self.sampled { "1" } else { "0" });
485        }
486
487        if let Some(ref causation_id) = self.causation_id {
488            req.headers
489                .insert(headers::CAUSATION_ID, causation_id.as_str());
490        }
491
492        if let Some(ref session_id) = self.session_id {
493            req.headers.insert(headers::SESSION_ID, session_id.as_str());
494        }
495    }
496
497    /// Inject correlation context into HTTP response headers.
498    pub fn inject_into_response(&self, res: &mut HttpResponse) {
499        res.headers.insert(
500            headers::CORRELATION_ID.to_string(),
501            self.correlation_id.clone(),
502        );
503        res.headers
504            .insert(headers::REQUEST_ID.to_string(), self.request_id.clone());
505
506        if let Some(ref trace_id) = self.trace_id {
507            let span_id = self.span_id.as_deref().unwrap_or("0000000000000000");
508            let sampled = if self.sampled { "01" } else { "00" };
509            let traceparent = format!("00-{}-{}-{}", trace_id, span_id, sampled);
510            res.headers
511                .insert(headers::TRACE_PARENT.to_string(), traceparent);
512        }
513    }
514
515    /// Convert to W3C traceparent header format.
516    pub fn to_traceparent(&self) -> Option<String> {
517        let trace_id = self.trace_id.as_ref()?;
518        let span_id = self.span_id.as_deref().unwrap_or("0000000000000000");
519        let sampled = if self.sampled { "01" } else { "00" };
520        Some(format!("00-{}-{}-{}", trace_id, span_id, sampled))
521    }
522}
523
524/// Parse W3C traceparent header.
525fn parse_traceparent(value: &str) -> Option<(String, String, bool)> {
526    let parts: Vec<&str> = value.split('-').collect();
527    if parts.len() >= 4 && parts[0] == "00" {
528        let trace_id = parts[1].to_string();
529        let span_id = parts[2].to_string();
530        let sampled = parts[3] == "01";
531        Some((trace_id, span_id, sampled))
532    } else {
533        None
534    }
535}
536
537// ============================================================================
538// Correlated Error
539// ============================================================================
540
541/// An error with full correlation information.
542#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct CorrelatedError {
544    /// Error ID (unique to this error occurrence)
545    pub error_id: String,
546    /// Correlation context
547    pub context: CorrelationContext,
548    /// Error message
549    pub message: String,
550    /// Error code
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub code: Option<String>,
553    /// Error category/type
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub error_type: Option<String>,
556    /// HTTP status code
557    pub status: u16,
558    /// Source service
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub source_service: Option<String>,
561    /// Source location (file:line)
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub source_location: Option<String>,
564    /// Causation chain (list of error IDs that led to this error)
565    #[serde(skip_serializing_if = "Vec::is_empty", default)]
566    pub causation_chain: Vec<String>,
567    /// Related error IDs
568    #[serde(skip_serializing_if = "Vec::is_empty", default)]
569    pub related_errors: Vec<String>,
570    /// Stack trace
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub stack_trace: Option<String>,
573    /// Additional metadata
574    #[serde(skip_serializing_if = "HashMap::is_empty", default)]
575    pub metadata: HashMap<String, serde_json::Value>,
576    /// Timestamp
577    pub timestamp: u64,
578    /// Retry information
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub retry_info: Option<RetryInfo>,
581}
582
583/// Retry information for recoverable errors.
584#[derive(Debug, Clone, Serialize, Deserialize)]
585pub struct RetryInfo {
586    /// Whether the error is retryable
587    pub retryable: bool,
588    /// Suggested retry delay in milliseconds
589    pub retry_delay_ms: Option<u64>,
590    /// Maximum retry attempts
591    pub max_retries: Option<u32>,
592    /// Current retry attempt
593    pub current_attempt: u32,
594}
595
596impl CorrelatedError {
597    /// Create a new correlated error.
598    pub fn new(message: impl Into<String>) -> Self {
599        Self {
600            error_id: IdGenerationStrategy::UuidV4.generate(),
601            context: CorrelationContext::new(),
602            message: message.into(),
603            code: None,
604            error_type: None,
605            status: 500,
606            source_service: None,
607            source_location: None,
608            causation_chain: Vec::new(),
609            related_errors: Vec::new(),
610            stack_trace: None,
611            metadata: HashMap::new(),
612            timestamp: SystemTime::now()
613                .duration_since(UNIX_EPOCH)
614                .unwrap_or_default()
615                .as_millis() as u64,
616            retry_info: None,
617        }
618    }
619
620    /// Create from an existing Error with context.
621    pub fn from_error(error: &Error, context: CorrelationContext) -> Self {
622        let status = error.status_code();
623        let error_type = match error {
624            Error::BadRequest(_) => "BAD_REQUEST",
625            Error::Unauthorized(_) => "UNAUTHORIZED",
626            Error::Forbidden(_) => "FORBIDDEN",
627            Error::NotFound(_) => "NOT_FOUND",
628            Error::Validation(_) => "VALIDATION_ERROR",
629            Error::Internal(_) => "INTERNAL_ERROR",
630            Error::Conflict(_) => "CONFLICT",
631            Error::TooManyRequests(_) => "RATE_LIMITED",
632            Error::ServiceUnavailable(_) => "SERVICE_UNAVAILABLE",
633            Error::RequestTimeout(_) => "TIMEOUT",
634            _ => "ERROR",
635        };
636
637        Self {
638            error_id: IdGenerationStrategy::UuidV4.generate(),
639            context,
640            message: error.to_string(),
641            code: None,
642            error_type: Some(error_type.to_string()),
643            status,
644            source_service: None,
645            source_location: None,
646            causation_chain: Vec::new(),
647            related_errors: Vec::new(),
648            stack_trace: None,
649            metadata: HashMap::new(),
650            timestamp: SystemTime::now()
651                .duration_since(UNIX_EPOCH)
652                .unwrap_or_default()
653                .as_millis() as u64,
654            retry_info: None,
655        }
656    }
657
658    /// Set the correlation context.
659    pub fn with_context(mut self, context: CorrelationContext) -> Self {
660        self.context = context;
661        self
662    }
663
664    /// Set the error code.
665    pub fn with_code(mut self, code: impl Into<String>) -> Self {
666        self.code = Some(code.into());
667        self
668    }
669
670    /// Set the error type.
671    pub fn with_type(mut self, error_type: impl Into<String>) -> Self {
672        self.error_type = Some(error_type.into());
673        self
674    }
675
676    /// Set the HTTP status.
677    pub fn with_status(mut self, status: u16) -> Self {
678        self.status = status;
679        self
680    }
681
682    /// Set the source service.
683    pub fn with_source_service(mut self, service: impl Into<String>) -> Self {
684        self.source_service = Some(service.into());
685        self
686    }
687
688    /// Set the source location.
689    pub fn with_source_location(mut self, location: impl Into<String>) -> Self {
690        self.source_location = Some(location.into());
691        self
692    }
693
694    /// Add a causing error to the chain.
695    pub fn caused_by(mut self, cause: impl Into<String>) -> Self {
696        self.causation_chain.push(cause.into());
697        self
698    }
699
700    /// Add a related error.
701    pub fn related_to(mut self, error_id: impl Into<String>) -> Self {
702        self.related_errors.push(error_id.into());
703        self
704    }
705
706    /// Add a stack trace.
707    pub fn with_stack_trace(mut self, trace: impl Into<String>) -> Self {
708        self.stack_trace = Some(trace.into());
709        self
710    }
711
712    /// Add metadata.
713    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
714        if let Ok(json_value) = serde_json::to_value(value) {
715            self.metadata.insert(key.into(), json_value);
716        }
717        self
718    }
719
720    /// Set retry information.
721    pub fn with_retry_info(mut self, info: RetryInfo) -> Self {
722        self.retry_info = Some(info);
723        self
724    }
725
726    /// Mark as retryable.
727    pub fn retryable(mut self, delay_ms: u64, max_retries: u32) -> Self {
728        self.retry_info = Some(RetryInfo {
729            retryable: true,
730            retry_delay_ms: Some(delay_ms),
731            max_retries: Some(max_retries),
732            current_attempt: 0,
733        });
734        self
735    }
736
737    /// Convert to JSON.
738    pub fn to_json(&self) -> String {
739        serde_json::to_string_pretty(self).unwrap_or_else(|_| {
740            format!(
741                r#"{{"error_id":"{}","message":"{}","status":{}}}"#,
742                self.error_id, self.message, self.status
743            )
744        })
745    }
746}
747
748// ============================================================================
749// Error Registry
750// ============================================================================
751
752/// Registry for tracking correlated errors.
753pub struct ErrorRegistry {
754    /// Maximum number of errors to keep in memory
755    max_size: usize,
756    /// Errors indexed by error ID
757    errors: RwLock<HashMap<String, CorrelatedError>>,
758    /// Errors grouped by correlation ID
759    by_correlation: RwLock<HashMap<String, Vec<String>>>,
760    /// Errors grouped by trace ID
761    by_trace: RwLock<HashMap<String, Vec<String>>>,
762}
763
764impl ErrorRegistry {
765    /// Create a new error registry.
766    pub fn new(max_size: usize) -> Self {
767        Self {
768            max_size,
769            errors: RwLock::new(HashMap::new()),
770            by_correlation: RwLock::new(HashMap::new()),
771            by_trace: RwLock::new(HashMap::new()),
772        }
773    }
774
775    /// Register an error.
776    pub async fn register(&self, error: CorrelatedError) {
777        let error_id = error.error_id.clone();
778        let correlation_id = error.context.correlation_id.clone();
779        let trace_id = error.context.trace_id.clone();
780
781        // Check size limit
782        let mut errors = self.errors.write().await;
783        let evicted = if errors.len() >= self.max_size {
784            // Remove an arbitrary error to enforce the size bound. `HashMap`
785            // iteration order is unspecified, so this is NOT oldest-first
786            // (LRU) eviction — no insertion/access order is tracked.
787            errors
788                .keys()
789                .next()
790                .cloned()
791                .and_then(|id| errors.remove(&id).map(|e| (id, e)))
792        } else {
793            None
794        };
795        errors.insert(error_id.clone(), error);
796        drop(errors);
797
798        // Drop the evicted error's index entries so the indexes stay bounded
799        // by max_size alongside the error store itself.
800        if let Some((evicted_id, evicted)) = evicted {
801            let mut by_correlation = self.by_correlation.write().await;
802            if let Some(ids) = by_correlation.get_mut(&evicted.context.correlation_id) {
803                ids.retain(|id| id != &evicted_id);
804                if ids.is_empty() {
805                    by_correlation.remove(&evicted.context.correlation_id);
806                }
807            }
808            drop(by_correlation);
809
810            if let Some(ref evicted_trace) = evicted.context.trace_id {
811                let mut by_trace = self.by_trace.write().await;
812                if let Some(ids) = by_trace.get_mut(evicted_trace) {
813                    ids.retain(|id| id != &evicted_id);
814                    if ids.is_empty() {
815                        by_trace.remove(evicted_trace);
816                    }
817                }
818            }
819        }
820
821        // Index by correlation ID
822        let mut by_correlation = self.by_correlation.write().await;
823        by_correlation
824            .entry(correlation_id)
825            .or_insert_with(Vec::new)
826            .push(error_id.clone());
827        drop(by_correlation);
828
829        // Index by trace ID
830        if let Some(trace_id) = trace_id {
831            let mut by_trace = self.by_trace.write().await;
832            by_trace
833                .entry(trace_id)
834                .or_insert_with(Vec::new)
835                .push(error_id);
836        }
837    }
838
839    /// Get an error by ID.
840    pub async fn get(&self, error_id: &str) -> Option<CorrelatedError> {
841        self.errors.read().await.get(error_id).cloned()
842    }
843
844    /// Get all errors for a correlation ID.
845    pub async fn get_by_correlation(&self, correlation_id: &str) -> Vec<CorrelatedError> {
846        let by_correlation = self.by_correlation.read().await;
847        let error_ids = by_correlation.get(correlation_id);
848
849        if let Some(ids) = error_ids {
850            let errors = self.errors.read().await;
851            ids.iter()
852                .filter_map(|id| errors.get(id).cloned())
853                .collect()
854        } else {
855            Vec::new()
856        }
857    }
858
859    /// Get all errors for a trace ID.
860    pub async fn get_by_trace(&self, trace_id: &str) -> Vec<CorrelatedError> {
861        let by_trace = self.by_trace.read().await;
862        let error_ids = by_trace.get(trace_id);
863
864        if let Some(ids) = error_ids {
865            let errors = self.errors.read().await;
866            ids.iter()
867                .filter_map(|id| errors.get(id).cloned())
868                .collect()
869        } else {
870            Vec::new()
871        }
872    }
873
874    /// Build the causation tree for an error.
875    pub async fn build_causation_tree(&self, error_id: &str) -> Option<ErrorTree> {
876        let error = self.get(error_id).await?;
877
878        let mut children = Vec::new();
879        for child_id in &error.related_errors {
880            if let Some(child_tree) = Box::pin(self.build_causation_tree(child_id)).await {
881                children.push(child_tree);
882            }
883        }
884
885        Some(ErrorTree { error, children })
886    }
887
888    /// Clear all errors.
889    pub async fn clear(&self) {
890        self.errors.write().await.clear();
891        self.by_correlation.write().await.clear();
892        self.by_trace.write().await.clear();
893    }
894
895    /// Get error count.
896    pub async fn len(&self) -> usize {
897        self.errors.read().await.len()
898    }
899
900    /// Check if registry is empty.
901    pub async fn is_empty(&self) -> bool {
902        self.errors.read().await.is_empty()
903    }
904}
905
906impl Default for ErrorRegistry {
907    fn default() -> Self {
908        Self::new(10000)
909    }
910}
911
912/// Tree structure for error causation.
913#[derive(Debug, Clone, Serialize)]
914pub struct ErrorTree {
915    /// The error at this node
916    pub error: CorrelatedError,
917    /// Child errors (caused by this error)
918    pub children: Vec<ErrorTree>,
919}
920
921// ============================================================================
922// Correlation Middleware
923// ============================================================================
924
925/// Configuration for correlation middleware.
926#[derive(Debug, Clone)]
927pub struct CorrelationConfig {
928    /// ID generation strategy
929    pub id_strategy: IdGenerationStrategy,
930    /// Service name to tag in context
931    pub service_name: Option<String>,
932    /// Service version
933    pub service_version: Option<String>,
934    /// Whether to generate trace IDs if not present
935    pub generate_trace_id: bool,
936    /// Whether to propagate context in response headers
937    pub propagate_in_response: bool,
938    /// Header name for correlation ID (customize if needed)
939    pub correlation_header: String,
940    /// Header name for request ID
941    pub request_header: String,
942}
943
944impl Default for CorrelationConfig {
945    fn default() -> Self {
946        Self {
947            id_strategy: IdGenerationStrategy::UuidV4,
948            service_name: None,
949            service_version: None,
950            generate_trace_id: true,
951            propagate_in_response: true,
952            correlation_header: headers::CORRELATION_ID.to_string(),
953            request_header: headers::REQUEST_ID.to_string(),
954        }
955    }
956}
957
958impl CorrelationConfig {
959    /// Create new configuration.
960    pub fn new() -> Self {
961        Self::default()
962    }
963
964    /// Set service name.
965    pub fn service(mut self, name: impl Into<String>) -> Self {
966        self.service_name = Some(name.into());
967        self
968    }
969
970    /// Set service version.
971    pub fn version(mut self, version: impl Into<String>) -> Self {
972        self.service_version = Some(version.into());
973        self
974    }
975
976    /// Set ID generation strategy.
977    pub fn strategy(mut self, strategy: IdGenerationStrategy) -> Self {
978        self.id_strategy = strategy;
979        self
980    }
981
982    /// Enable/disable trace ID generation.
983    pub fn generate_traces(mut self, enabled: bool) -> Self {
984        self.generate_trace_id = enabled;
985        self
986    }
987
988    /// Enable/disable response header propagation.
989    pub fn propagate_response(mut self, enabled: bool) -> Self {
990        self.propagate_in_response = enabled;
991        self
992    }
993}
994
995/// Middleware that handles correlation context.
996pub struct CorrelationMiddleware {
997    config: CorrelationConfig,
998    registry: Option<Arc<ErrorRegistry>>,
999}
1000
1001impl CorrelationMiddleware {
1002    /// Create new correlation middleware.
1003    pub fn new(config: CorrelationConfig) -> Self {
1004        Self {
1005            config,
1006            registry: None,
1007        }
1008    }
1009
1010    /// Create with default configuration.
1011    pub fn default_config() -> Self {
1012        Self::new(CorrelationConfig::default())
1013    }
1014
1015    /// Attach an error registry.
1016    pub fn with_registry(mut self, registry: Arc<ErrorRegistry>) -> Self {
1017        self.registry = Some(registry);
1018        self
1019    }
1020}
1021
1022#[async_trait]
1023impl Middleware for CorrelationMiddleware {
1024    async fn handle(&self, mut req: HttpRequest, next: Next) -> Result<HttpResponse, Error> {
1025        // Extract or create correlation context
1026        let mut ctx = CorrelationContext::from_request(&req);
1027
1028        // Generate new IDs if not present
1029        if !req.headers.contains_key(&self.config.correlation_header) {
1030            ctx.correlation_id = self.config.id_strategy.generate();
1031        }
1032        ctx.request_id = self.config.id_strategy.generate();
1033
1034        // Generate trace ID if configured and not present. W3C trace/span IDs
1035        // are fixed-length lowercase hex, independent of the configured ID
1036        // strategy (whose output may be shorter than the required length).
1037        if self.config.generate_trace_id && ctx.trace_id.is_none() {
1038            ctx.trace_id = Some(random_hex_id(32));
1039            ctx.span_id = Some(random_hex_id(16));
1040        }
1041
1042        // Add service info
1043        if let Some(ref service) = self.config.service_name {
1044            ctx.service = Some(service.clone());
1045        }
1046        if let Some(ref version) = self.config.service_version {
1047            ctx.service_version = Some(version.clone());
1048        }
1049
1050        // Inject context into request
1051        ctx.inject_into_request(&mut req);
1052
1053        // Process request
1054        let result = next(req).await;
1055
1056        match result {
1057            Ok(mut response) => {
1058                // Inject correlation headers into response
1059                if self.config.propagate_in_response {
1060                    ctx.inject_into_response(&mut response);
1061                }
1062                Ok(response)
1063            }
1064            Err(error) => {
1065                // Register error if registry is attached
1066                if let Some(ref registry) = self.registry {
1067                    let correlated_error = CorrelatedError::from_error(&error, ctx.clone());
1068                    registry.register(correlated_error).await;
1069                }
1070                Err(error)
1071            }
1072        }
1073    }
1074}
1075
1076// ============================================================================
1077// Extension Traits
1078// ============================================================================
1079
1080/// Extension trait for HttpRequest to access correlation context.
1081pub trait CorrelatedRequest {
1082    /// Get the correlation context from the request.
1083    fn correlation_context(&self) -> CorrelationContext;
1084    /// Get the correlation ID.
1085    fn correlation_id(&self) -> Option<String>;
1086    /// Get the request ID.
1087    fn request_id(&self) -> Option<String>;
1088    /// Get the trace ID.
1089    fn trace_id(&self) -> Option<String>;
1090    /// Get the span ID.
1091    fn span_id(&self) -> Option<String>;
1092}
1093
1094impl CorrelatedRequest for HttpRequest {
1095    fn correlation_context(&self) -> CorrelationContext {
1096        CorrelationContext::from_request(self)
1097    }
1098
1099    fn correlation_id(&self) -> Option<String> {
1100        // One lookup: header names intern case-insensitively, so the
1101        // lowercased retry was always redundant.
1102        self.headers.get(headers::CORRELATION_ID).map(str::to_owned)
1103    }
1104
1105    fn request_id(&self) -> Option<String> {
1106        self.headers.get(headers::REQUEST_ID).map(str::to_owned)
1107    }
1108
1109    fn trace_id(&self) -> Option<String> {
1110        // Try W3C traceparent first
1111        if let Some(traceparent) = self.headers.get(headers::TRACE_PARENT)
1112            && let Some((trace_id, _, _)) = parse_traceparent(traceparent)
1113        {
1114            return Some(trace_id);
1115        }
1116        // Fall back to B3
1117        self.headers.get(headers::B3_TRACE_ID).map(str::to_owned)
1118    }
1119
1120    fn span_id(&self) -> Option<String> {
1121        // Try W3C traceparent first
1122        if let Some(traceparent) = self.headers.get(headers::TRACE_PARENT)
1123            && let Some((_, span_id, _)) = parse_traceparent(traceparent)
1124        {
1125            return Some(span_id);
1126        }
1127        // Fall back to B3
1128        self.headers.get(headers::B3_SPAN_ID).map(str::to_owned)
1129    }
1130}
1131
1132/// Extension trait for Error to create correlated errors.
1133pub trait CorrelatedErrorExt {
1134    /// Convert to a correlated error with context.
1135    fn correlate(&self, context: CorrelationContext) -> CorrelatedError;
1136    /// Convert to a correlated error from request.
1137    fn correlate_with_request(&self, request: &HttpRequest) -> CorrelatedError;
1138}
1139
1140impl CorrelatedErrorExt for Error {
1141    fn correlate(&self, context: CorrelationContext) -> CorrelatedError {
1142        CorrelatedError::from_error(self, context)
1143    }
1144
1145    fn correlate_with_request(&self, request: &HttpRequest) -> CorrelatedError {
1146        let context = CorrelationContext::from_request(request);
1147        CorrelatedError::from_error(self, context)
1148    }
1149}
1150
1151// ============================================================================
1152// Tests
1153// ============================================================================
1154
1155#[cfg(test)]
1156mod tests {
1157    use super::*;
1158
1159    #[test]
1160    fn test_id_generation_uuid_v4() {
1161        let id1 = IdGenerationStrategy::UuidV4.generate();
1162        let id2 = IdGenerationStrategy::UuidV4.generate();
1163        assert_ne!(id1, id2);
1164        assert_eq!(id1.len(), 36); // UUID format
1165    }
1166
1167    #[test]
1168    fn test_id_generation_snowflake() {
1169        let id1 = IdGenerationStrategy::Snowflake.generate();
1170        let id2 = IdGenerationStrategy::Snowflake.generate();
1171        assert_ne!(id1, id2);
1172        assert_eq!(id1.len(), 16); // 16 hex chars
1173    }
1174
1175    #[test]
1176    fn test_id_generation_short() {
1177        let id = IdGenerationStrategy::Short.generate();
1178        assert_eq!(id.len(), 8);
1179    }
1180
1181    #[test]
1182    fn test_correlation_context_new() {
1183        let ctx = CorrelationContext::new();
1184        assert!(!ctx.correlation_id.is_empty());
1185        assert!(!ctx.request_id.is_empty());
1186        assert!(ctx.sampled);
1187    }
1188
1189    #[test]
1190    fn test_correlation_context_child() {
1191        let parent = CorrelationContext::new()
1192            .trace_id("trace-123")
1193            .span_id("span-456")
1194            .with_user_id("user-1");
1195
1196        let child = parent.child();
1197
1198        assert_eq!(child.correlation_id, parent.correlation_id);
1199        assert_ne!(child.request_id, parent.request_id);
1200        assert_eq!(child.trace_id, parent.trace_id);
1201        assert_eq!(child.parent_span_id, parent.span_id);
1202        assert_eq!(child.causation_id, Some(parent.request_id.clone()));
1203        assert_eq!(child.user_id, parent.user_id);
1204    }
1205
1206    #[test]
1207    fn test_correlation_context_from_request() {
1208        let mut req = HttpRequest::new("GET", "/test".to_string());
1209        req.headers
1210            .insert(headers::CORRELATION_ID, "corr-123".to_string());
1211        req.headers
1212            .insert(headers::REQUEST_ID, "req-456".to_string());
1213        req.headers.insert(
1214            headers::TRACE_PARENT,
1215            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string(),
1216        );
1217
1218        let ctx = CorrelationContext::from_request(&req);
1219
1220        assert_eq!(ctx.correlation_id, "corr-123");
1221        assert_eq!(ctx.request_id, "req-456");
1222        assert_eq!(
1223            ctx.trace_id,
1224            Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string())
1225        );
1226        assert_eq!(ctx.parent_span_id, Some("00f067aa0ba902b7".to_string()));
1227        assert!(ctx.sampled);
1228    }
1229
1230    #[test]
1231    fn test_traceparent_format() {
1232        let ctx = CorrelationContext::new()
1233            .trace_id("4bf92f3577b34da6a3ce929d0e0e4736")
1234            .span_id("00f067aa0ba902b7")
1235            .with_sampled(true);
1236
1237        let traceparent = ctx.to_traceparent().unwrap();
1238        assert!(traceparent.starts_with("00-"));
1239        assert!(traceparent.ends_with("-01"));
1240    }
1241
1242    #[test]
1243    fn test_correlated_error() {
1244        let ctx = CorrelationContext::new()
1245            .with_service("test-service")
1246            .with_user_id("user-123");
1247
1248        let error = CorrelatedError::new("Something went wrong")
1249            .with_context(ctx)
1250            .with_code("ERR_001")
1251            .with_type("VALIDATION_ERROR")
1252            .with_status(400)
1253            .caused_by("Invalid input")
1254            .with_metadata("field", "email");
1255
1256        assert_eq!(error.message, "Something went wrong");
1257        assert_eq!(error.status, 400);
1258        assert_eq!(error.code, Some("ERR_001".to_string()));
1259        assert_eq!(error.causation_chain.len(), 1);
1260        assert!(error.metadata.contains_key("field"));
1261    }
1262
1263    #[test]
1264    fn test_retry_info() {
1265        let error = CorrelatedError::new("Temporary failure").retryable(1000, 3);
1266
1267        let retry = error.retry_info.unwrap();
1268        assert!(retry.retryable);
1269        assert_eq!(retry.retry_delay_ms, Some(1000));
1270        assert_eq!(retry.max_retries, Some(3));
1271    }
1272
1273    #[tokio::test]
1274    async fn test_error_registry() {
1275        let registry = ErrorRegistry::new(100);
1276
1277        let ctx = CorrelationContext::new();
1278        let correlation_id = ctx.correlation_id.clone();
1279
1280        let error1 = CorrelatedError::new("Error 1").with_context(ctx.clone());
1281        let error2 = CorrelatedError::new("Error 2").with_context(ctx.child());
1282
1283        registry.register(error1.clone()).await;
1284        registry.register(error2.clone()).await;
1285
1286        // Get by ID
1287        let retrieved = registry.get(&error1.error_id).await.unwrap();
1288        assert_eq!(retrieved.message, "Error 1");
1289
1290        // Get by correlation ID
1291        let errors = registry.get_by_correlation(&correlation_id).await;
1292        assert_eq!(errors.len(), 2);
1293    }
1294
1295    #[test]
1296    fn test_correlated_request_extension() {
1297        let mut req = HttpRequest::new("GET", "/test".to_string());
1298        req.headers
1299            .insert(headers::CORRELATION_ID, "corr-123".to_string());
1300        req.headers
1301            .insert(headers::REQUEST_ID, "req-456".to_string());
1302
1303        assert_eq!(req.correlation_id(), Some("corr-123".to_string()));
1304        assert_eq!(req.request_id(), Some("req-456".to_string()));
1305    }
1306
1307    #[test]
1308    fn test_correlation_config() {
1309        let config = CorrelationConfig::new()
1310            .service("my-service")
1311            .version("1.0.0")
1312            .strategy(IdGenerationStrategy::UuidV7)
1313            .generate_traces(true)
1314            .propagate_response(true);
1315
1316        assert_eq!(config.service_name, Some("my-service".to_string()));
1317        assert_eq!(config.service_version, Some("1.0.0".to_string()));
1318        assert!(config.generate_trace_id);
1319        assert!(config.propagate_in_response);
1320    }
1321
1322    #[test]
1323    fn test_inject_headers() {
1324        let ctx = CorrelationContext::new()
1325            .correlation_id("corr-123")
1326            .trace_id("trace-456")
1327            .span_id("span-789")
1328            .with_session("session-abc");
1329
1330        let mut req = HttpRequest::new("POST", "/api".to_string());
1331        ctx.inject_into_request(&mut req);
1332
1333        assert_eq!(req.headers.get(headers::CORRELATION_ID), Some("corr-123"));
1334        assert!(req.headers.get(headers::TRACE_PARENT).is_some());
1335        assert_eq!(req.headers.get(headers::SESSION_ID), Some("session-abc"));
1336    }
1337
1338    #[test]
1339    fn test_parse_traceparent() {
1340        let (trace_id, span_id, sampled) =
1341            parse_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").unwrap();
1342
1343        assert_eq!(trace_id, "4bf92f3577b34da6a3ce929d0e0e4736");
1344        assert_eq!(span_id, "00f067aa0ba902b7");
1345        assert!(sampled);
1346
1347        // Not sampled
1348        let (_, _, sampled) =
1349            parse_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00").unwrap();
1350        assert!(!sampled);
1351    }
1352
1353    #[test]
1354    fn test_correlated_error_to_json() {
1355        let error = CorrelatedError::new("Test error")
1356            .with_code("TEST_001")
1357            .with_status(400);
1358
1359        let json = error.to_json();
1360        assert!(json.contains("Test error"));
1361        assert!(json.contains("TEST_001"));
1362        assert!(json.contains("400"));
1363    }
1364}