shared_logging/
context.rs

1//! Context propagation for structured logging.
2//!
3//! Provides context fields that are automatically included in all log events:
4//! - trace_id: OpenTelemetry trace ID
5//! - span_id: OpenTelemetry span ID
6//! - request_id: HTTP request identifier
7//! - user_id: Authenticated user identifier
8//! - tenant_id: Multi-tenant organization identifier
9
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13/// Context fields that are propagated through log events.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Context {
16    /// OpenTelemetry trace ID
17    pub trace_id: Option<String>,
18    /// OpenTelemetry span ID
19    pub span_id: Option<String>,
20    /// HTTP request identifier
21    pub request_id: Option<String>,
22    /// Authenticated user identifier
23    pub user_id: Option<String>,
24    /// Multi-tenant organization identifier
25    pub tenant_id: Option<String>,
26}
27
28impl Context {
29    /// Create a new empty context.
30    pub fn new() -> Self {
31        Self {
32            trace_id: None,
33            span_id: None,
34            request_id: None,
35            user_id: None,
36            tenant_id: None,
37        }
38    }
39
40    /// Generate a new request ID.
41    pub fn generate_request_id() -> String {
42        Uuid::new_v4().to_string()
43    }
44
45    /// Set the trace ID.
46    pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
47        self.trace_id = Some(trace_id.into());
48        self
49    }
50
51    /// Set the span ID.
52    pub fn with_span_id(mut self, span_id: impl Into<String>) -> Self {
53        self.span_id = Some(span_id.into());
54        self
55    }
56
57    /// Set the request ID.
58    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
59        self.request_id = Some(request_id.into());
60        self
61    }
62
63    /// Set the user ID.
64    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
65        self.user_id = Some(user_id.into());
66        self
67    }
68
69    /// Set the tenant ID.
70    pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
71        self.tenant_id = Some(tenant_id.into());
72        self
73    }
74
75    /// Extract context from the current tracing span.
76    ///
77    /// Note: This is a simplified implementation. For full OpenTelemetry support,
78    /// use the `otel` module's `extract_context_from_otel()` function.
79    pub fn from_span() -> Self {
80        // For now, return empty context
81        // Actual OTel trace/span IDs should be extracted using the otel module
82        Self::new()
83    }
84
85    /// Merge another context into this one, preferring non-None values.
86    pub fn merge(mut self, other: Context) -> Self {
87        if other.trace_id.is_some() {
88            self.trace_id = other.trace_id;
89        }
90        if other.span_id.is_some() {
91            self.span_id = other.span_id;
92        }
93        if other.request_id.is_some() {
94            self.request_id = other.request_id;
95        }
96        if other.user_id.is_some() {
97            self.user_id = other.user_id;
98        }
99        if other.tenant_id.is_some() {
100            self.tenant_id = other.tenant_id;
101        }
102        self
103    }
104
105    /// Convert context to a key-value map for logging.
106    pub fn to_fields(&self) -> Vec<(&'static str, String)> {
107        let mut fields = Vec::new();
108        if let Some(ref trace_id) = self.trace_id {
109            fields.push(("trace_id", trace_id.clone()));
110        }
111        if let Some(ref span_id) = self.span_id {
112            fields.push(("span_id", span_id.clone()));
113        }
114        if let Some(ref request_id) = self.request_id {
115            fields.push(("request_id", request_id.clone()));
116        }
117        if let Some(ref user_id) = self.user_id {
118            fields.push(("user_id", user_id.clone()));
119        }
120        if let Some(ref tenant_id) = self.tenant_id {
121            fields.push(("tenant_id", tenant_id.clone()));
122        }
123        fields
124    }
125}
126
127impl Default for Context {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133/// Builder for creating context instances.
134#[derive(Debug, Default)]
135pub struct ContextBuilder {
136    context: Context,
137}
138
139impl ContextBuilder {
140    /// Create a new context builder.
141    pub fn new() -> Self {
142        Self {
143            context: Context::new(),
144        }
145    }
146
147    /// Set the trace ID.
148    pub fn trace_id(mut self, trace_id: impl Into<String>) -> Self {
149        self.context.trace_id = Some(trace_id.into());
150        self
151    }
152
153    /// Set the span ID.
154    pub fn span_id(mut self, span_id: impl Into<String>) -> Self {
155        self.context.span_id = Some(span_id.into());
156        self
157    }
158
159    /// Set the request ID.
160    pub fn request_id(mut self, request_id: impl Into<String>) -> Self {
161        self.context.request_id = Some(request_id.into());
162        self
163    }
164
165    /// Generate and set a new request ID.
166    pub fn generate_request_id(mut self) -> Self {
167        self.context.request_id = Some(Context::generate_request_id());
168        self
169    }
170
171    /// Set the user ID.
172    pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
173        self.context.user_id = Some(user_id.into());
174        self
175    }
176
177    /// Set the tenant ID.
178    pub fn tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
179        self.context.tenant_id = Some(tenant_id.into());
180        self
181    }
182
183    /// Build the context.
184    pub fn build(self) -> Context {
185        self.context
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_context_builder() {
195        let ctx = ContextBuilder::new()
196            .trace_id("trace123")
197            .request_id("req456")
198            .user_id("user789")
199            .build();
200
201        assert_eq!(ctx.trace_id, Some("trace123".to_string()));
202        assert_eq!(ctx.request_id, Some("req456".to_string()));
203        assert_eq!(ctx.user_id, Some("user789".to_string()));
204    }
205
206    #[test]
207    fn test_context_merge() {
208        let ctx1 = ContextBuilder::new()
209            .trace_id("trace1")
210            .request_id("req1")
211            .build();
212
213        let ctx2 = ContextBuilder::new()
214            .trace_id("trace2")
215            .user_id("user1")
216            .build();
217
218        let merged = ctx1.merge(ctx2);
219        assert_eq!(merged.trace_id, Some("trace2".to_string()));
220        assert_eq!(merged.request_id, Some("req1".to_string()));
221        assert_eq!(merged.user_id, Some("user1".to_string()));
222    }
223}
224