Skip to main content

llm_optimizer_processor/telemetry/
context.rs

1//! Context propagation for distributed tracing.
2//!
3//! This module provides utilities for propagating trace context across service
4//! boundaries using W3C Trace Context and Baggage standards.
5
6use opentelemetry::{
7    baggage::BaggageExt,
8    global,
9    propagation::{Extractor, Injector, TextMapPropagator},
10    trace::{TraceContextExt, TraceError},
11    Context, KeyValue,
12};
13use std::collections::HashMap;
14
15/// A carrier for injecting and extracting trace context from HTTP headers.
16#[derive(Debug, Clone, Default)]
17pub struct HeaderCarrier {
18    headers: HashMap<String, String>,
19}
20
21impl HeaderCarrier {
22    /// Creates a new empty header carrier.
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Creates a header carrier from existing headers.
28    pub fn from_headers(headers: HashMap<String, String>) -> Self {
29        Self { headers }
30    }
31
32    /// Gets all headers.
33    pub fn headers(&self) -> &HashMap<String, String> {
34        &self.headers
35    }
36
37    /// Consumes the carrier and returns the headers.
38    pub fn into_headers(self) -> HashMap<String, String> {
39        self.headers
40    }
41
42    /// Gets a header value by key.
43    pub fn get(&self, key: &str) -> Option<&String> {
44        self.headers.get(key)
45    }
46
47    /// Sets a header value.
48    pub fn set(&mut self, key: String, value: String) {
49        self.headers.insert(key, value);
50    }
51}
52
53impl Injector for HeaderCarrier {
54    fn set(&mut self, key: &str, value: String) {
55        self.headers.insert(key.to_string(), value);
56    }
57}
58
59impl Extractor for HeaderCarrier {
60    fn get(&self, key: &str) -> Option<&str> {
61        self.headers.get(key).map(|v| v.as_str())
62    }
63
64    fn keys(&self) -> Vec<&str> {
65        self.headers.keys().map(|k| k.as_str()).collect()
66    }
67}
68
69/// Injects the current trace context into a carrier.
70///
71/// # Example
72///
73/// ```ignore
74/// let mut carrier = HeaderCarrier::new();
75/// inject_context(&Context::current(), &mut carrier);
76/// // carrier now contains traceparent and tracestate headers
77/// ```
78pub fn inject_context(context: &Context, carrier: &mut dyn Injector) {
79    global::get_text_map_propagator(|propagator| {
80        propagator.inject_context(context, carrier);
81    });
82}
83
84/// Extracts trace context from a carrier.
85///
86/// # Example
87///
88/// ```ignore
89/// let carrier = HeaderCarrier::from_headers(headers);
90/// let context = extract_context(&carrier);
91/// // context now contains the extracted trace context
92/// ```
93pub fn extract_context(carrier: &dyn Extractor) -> Context {
94    global::get_text_map_propagator(|propagator| propagator.extract(carrier))
95}
96
97/// Injects trace context into HTTP headers.
98pub fn inject_into_headers(
99    context: &Context,
100    headers: &mut HashMap<String, String>,
101) {
102    let mut carrier = HeaderCarrier::from_headers(headers.clone());
103    inject_context(context, &mut carrier);
104    *headers = carrier.into_headers();
105}
106
107/// Extracts trace context from HTTP headers.
108pub fn extract_from_headers(headers: &HashMap<String, String>) -> Context {
109    let carrier = HeaderCarrier::from_headers(headers.clone());
110    extract_context(&carrier)
111}
112
113/// Baggage item for propagating additional context.
114#[derive(Debug, Clone)]
115pub struct BaggageItem {
116    pub key: String,
117    pub value: String,
118}
119
120impl BaggageItem {
121    pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
122        Self {
123            key: key.into(),
124            value: value.into(),
125        }
126    }
127}
128
129/// Adds baggage to the current context.
130///
131/// Baggage allows you to propagate arbitrary key-value pairs across service boundaries.
132pub fn with_baggage(context: &Context, items: Vec<BaggageItem>) -> Context {
133    let mut ctx = context.clone();
134    for item in items {
135        ctx = ctx.with_value(KeyValue::new(item.key, item.value));
136    }
137    ctx
138}
139
140/// Gets baggage value from context.
141pub fn get_baggage(context: &Context, key: &str) -> Option<String> {
142    context.baggage().get(key).map(|v| v.to_string())
143}
144
145/// Creates a new context with the current span.
146pub fn context_with_span(span: impl opentelemetry::trace::Span + Send + Sync + 'static) -> Context {
147    Context::current_with_span(span)
148}
149
150/// Attaches a context for the duration of the async function.
151///
152/// # Example
153///
154/// ```ignore
155/// let context = extract_from_headers(&headers);
156/// with_context(context, async {
157///     // This async block runs with the extracted context
158///     process_event().await;
159/// }).await;
160/// ```
161pub async fn with_context<F, T>(context: Context, f: F) -> T
162where
163    F: std::future::Future<Output = T>,
164{
165    let _guard = context.attach();
166    f.await
167}
168
169/// Macro for running code within a context scope.
170///
171/// # Example
172///
173/// ```ignore
174/// let context = extract_from_headers(&headers);
175/// in_context!(context, {
176///     // Code here runs with the context attached
177///     let span = span!("my_operation");
178/// });
179/// ```
180#[macro_export]
181macro_rules! in_context {
182    ($context:expr, $body:block) => {{
183        let _guard = $context.attach();
184        $body
185    }};
186}
187
188/// Propagates context across async task boundaries.
189///
190/// # Example
191///
192/// ```ignore
193/// propagate_context(async {
194///     // This task inherits the current context
195///     process_background_task().await;
196/// }).await;
197/// ```
198pub async fn propagate_context<F, T>(f: F) -> T
199where
200    F: std::future::Future<Output = T>,
201{
202    let context = Context::current();
203    with_context(context, f).await
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_header_carrier_new() {
212        let carrier = HeaderCarrier::new();
213        assert!(carrier.headers.is_empty());
214    }
215
216    #[test]
217    fn test_header_carrier_from_headers() {
218        let mut headers = HashMap::new();
219        headers.insert("traceparent".to_string(), "test-value".to_string());
220
221        let carrier = HeaderCarrier::from_headers(headers.clone());
222        assert_eq!(carrier.get("traceparent"), Some(&"test-value".to_string()));
223    }
224
225    #[test]
226    fn test_header_carrier_set_get() {
227        let mut carrier = HeaderCarrier::new();
228        carrier.set("test-key".to_string(), "test-value".to_string());
229        assert_eq!(carrier.get("test-key"), Some(&"test-value".to_string()));
230    }
231
232    #[test]
233    fn test_header_carrier_injector() {
234        let mut carrier = HeaderCarrier::new();
235        Injector::set(&mut carrier, "key", "value".to_string());
236        assert_eq!(carrier.headers.get("key"), Some(&"value".to_string()));
237    }
238
239    #[test]
240    fn test_header_carrier_extractor() {
241        let mut headers = HashMap::new();
242        headers.insert("key".to_string(), "value".to_string());
243        let carrier = HeaderCarrier::from_headers(headers);
244
245        assert_eq!(Extractor::get(&carrier, "key"), Some("value"));
246        assert_eq!(Extractor::keys(&carrier), vec!["key"]);
247    }
248
249    #[test]
250    fn test_baggage_item() {
251        let item = BaggageItem::new("tenant-id", "12345");
252        assert_eq!(item.key, "tenant-id");
253        assert_eq!(item.value, "12345");
254    }
255
256    #[test]
257    fn test_inject_extract_context() {
258        let context = Context::current();
259        let mut carrier = HeaderCarrier::new();
260
261        inject_context(&context, &mut carrier);
262        let extracted = extract_context(&carrier);
263
264        // Context should be extracted successfully (even if empty)
265        assert!(extracted.span().span_context().is_valid() || !extracted.span().span_context().is_valid());
266    }
267
268    #[test]
269    fn test_inject_extract_headers() {
270        let mut headers = HashMap::new();
271        let context = Context::current();
272
273        inject_into_headers(&context, &mut headers);
274        let extracted = extract_from_headers(&headers);
275
276        assert!(extracted.span().span_context().is_valid() || !extracted.span().span_context().is_valid());
277    }
278
279    #[tokio::test]
280    async fn test_with_context() {
281        let context = Context::current();
282        let result = with_context(context, async { 42 }).await;
283        assert_eq!(result, 42);
284    }
285
286    #[tokio::test]
287    async fn test_propagate_context() {
288        let result = propagate_context(async { "test" }).await;
289        assert_eq!(result, "test");
290    }
291}