astrid_telemetry/
context.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct RequestContext {
14 pub request_id: Uuid,
16 pub correlation_id: Uuid,
18 pub parent_id: Option<Uuid>,
20 pub session_id: Option<Uuid>,
22 pub user_id: Option<Uuid>,
24 pub started_at: DateTime<Utc>,
26 pub source: String,
28 pub operation: Option<String>,
30 #[serde(default)]
32 pub metadata: HashMap<String, String>,
33}
34
35impl RequestContext {
36 #[must_use]
38 pub fn new(source: impl Into<String>) -> Self {
39 let id = Uuid::new_v4();
40 Self {
41 request_id: id,
42 correlation_id: id,
43 parent_id: None,
44 session_id: None,
45 user_id: None,
46 started_at: Utc::now(),
47 source: source.into(),
48 operation: None,
49 metadata: HashMap::new(),
50 }
51 }
52
53 #[must_use]
55 pub fn child(&self, source: impl Into<String>) -> Self {
56 Self {
57 request_id: Uuid::new_v4(),
58 correlation_id: self.correlation_id,
59 parent_id: Some(self.request_id),
60 session_id: self.session_id,
61 user_id: self.user_id,
62 started_at: Utc::now(),
63 source: source.into(),
64 operation: None,
65 metadata: self.metadata.clone(),
66 }
67 }
68
69 #[must_use]
71 pub fn with_correlation_id(mut self, id: Uuid) -> Self {
72 self.correlation_id = id;
73 self
74 }
75
76 #[must_use]
78 pub fn with_session_id(mut self, id: Uuid) -> Self {
79 self.session_id = Some(id);
80 self
81 }
82
83 #[must_use]
85 pub fn with_user_id(mut self, id: Uuid) -> Self {
86 self.user_id = Some(id);
87 self
88 }
89
90 #[must_use]
92 pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
93 self.operation = Some(operation.into());
94 self
95 }
96
97 #[must_use]
99 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
100 self.metadata.insert(key.into(), value.into());
101 self
102 }
103
104 #[must_use]
106 pub fn elapsed(&self) -> chrono::Duration {
107 #[expect(clippy::arithmetic_side_effects)]
109 let elapsed = Utc::now() - self.started_at;
110 elapsed
111 }
112
113 #[must_use]
115 pub fn elapsed_ms(&self) -> i64 {
116 self.elapsed().num_milliseconds()
117 }
118
119 #[must_use]
121 pub fn span(&self) -> tracing::Span {
122 tracing::info_span!(
123 "request",
124 request_id = %self.request_id,
125 correlation_id = %self.correlation_id,
126 source = %self.source,
127 operation = self.operation.as_deref(),
128 )
129 }
130
131 #[must_use]
133 pub fn has_parent(&self) -> bool {
134 self.parent_id.is_some()
135 }
136
137 #[must_use]
139 pub fn short_id(&self) -> String {
140 self.request_id.to_string()[..8].to_string()
141 }
142}
143
144impl Default for RequestContext {
145 fn default() -> Self {
146 Self::new("unknown")
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_request_context_creation() {
156 let ctx = RequestContext::new("test");
157 assert_eq!(ctx.source, "test");
158 assert_eq!(ctx.request_id, ctx.correlation_id);
159 assert!(ctx.parent_id.is_none());
160 assert!(ctx.session_id.is_none());
161 assert!(ctx.user_id.is_none());
162 }
163
164 #[test]
165 fn test_request_context_builder() {
166 let session = Uuid::new_v4();
167 let user = Uuid::new_v4();
168 let correlation = Uuid::new_v4();
169
170 let ctx = RequestContext::new("test")
171 .with_correlation_id(correlation)
172 .with_session_id(session)
173 .with_user_id(user)
174 .with_operation("test_op")
175 .with_metadata("key", "value");
176
177 assert_eq!(ctx.correlation_id, correlation);
178 assert_eq!(ctx.session_id, Some(session));
179 assert_eq!(ctx.user_id, Some(user));
180 assert_eq!(ctx.operation, Some("test_op".to_string()));
181 assert_eq!(ctx.metadata.get("key"), Some(&"value".to_string()));
182 }
183
184 #[test]
185 fn test_child_context() {
186 let session = Uuid::new_v4();
187 let parent = RequestContext::new("parent")
188 .with_session_id(session)
189 .with_metadata("inherited", "yes");
190
191 let child = parent.child("child");
192
193 assert_ne!(child.request_id, parent.request_id);
195
196 assert_eq!(child.correlation_id, parent.correlation_id);
198
199 assert_eq!(child.parent_id, Some(parent.request_id));
201
202 assert_eq!(child.session_id, Some(session));
204
205 assert_eq!(child.metadata.get("inherited"), Some(&"yes".to_string()));
207 }
208
209 #[test]
210 fn test_elapsed() {
211 let ctx = RequestContext::new("test");
212 std::thread::sleep(std::time::Duration::from_millis(10));
213 assert!(ctx.elapsed_ms() >= 10);
214 }
215
216 #[test]
217 fn test_short_id() {
218 let ctx = RequestContext::new("test");
219 let short = ctx.short_id();
220 assert_eq!(short.len(), 8);
221 }
222
223 #[test]
224 fn test_serialization() {
225 let ctx = RequestContext::new("test")
226 .with_operation("test_op")
227 .with_metadata("key", "value");
228
229 let json = serde_json::to_string(&ctx).unwrap();
230 assert!(json.contains("\"source\":\"test\""));
231 assert!(json.contains("\"operation\":\"test_op\""));
232
233 let parsed: RequestContext = serde_json::from_str(&json).unwrap();
234 assert_eq!(parsed.source, "test");
235 assert_eq!(parsed.operation, Some("test_op".to_string()));
236 }
237}