1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4use std::task::{Context, Poll};
5use std::time::Instant;
6
7use opentelemetry::trace::{SpanKind, Status, TraceContextExt, Tracer};
8use opentelemetry::{Context as OtelContext, KeyValue, global};
9use tower::Service;
10use tower::ServiceExt;
11use tracing::Instrument;
12
13use crate::shared::observability::domain::DetailLevel;
14use camel_api::metrics::MetricsCollector;
15use camel_api::{Body, BoxProcessor, CamelError, Exchange};
16
17struct SpanEndGuard(OtelContext);
21
22impl Drop for SpanEndGuard {
23 fn drop(&mut self) {
24 self.0.span().end();
25 }
26}
27
28fn body_type_name(body: &Body) -> &'static str {
30 match body {
31 Body::Empty => "empty",
32 Body::Bytes(_) => "bytes",
33 Body::Text(_) => "text",
34 Body::Json(_) => "json",
35 Body::Xml(_) => "xml",
36 Body::Stream(_) => "stream",
37 }
38}
39
40pub struct TracingProcessor {
49 inner: BoxProcessor,
50 route_id: String,
51 step_id: String,
52 span_name: String,
53 step_index: usize,
54 detail_level: DetailLevel,
55 metrics: Option<Arc<dyn MetricsCollector>>,
56}
57
58impl TracingProcessor {
59 pub fn new(
61 inner: BoxProcessor,
62 route_id: String,
63 step_index: usize,
64 detail_level: DetailLevel,
65 metrics: Option<Arc<dyn MetricsCollector>>,
66 ) -> Self {
67 let step_id = format!("step-{}", step_index);
68 let span_name = format!("{route_id}:{step_id}");
69 Self {
70 inner,
71 route_id,
72 step_id,
73 span_name,
74 step_index,
75 detail_level,
76 metrics,
77 }
78 }
79}
80
81impl Service<Exchange> for TracingProcessor {
82 type Response = Exchange;
83 type Error = CamelError;
84 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
85
86 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
87 self.inner.poll_ready(cx)
88 }
89
90 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
91 let start = Instant::now();
92 let span_name = self.span_name.clone();
93
94 let tracer = global::tracer("camel-core");
96
97 let parent_cx = exchange.otel_context.clone();
99
100 let mut attributes = [
102 KeyValue::new("messaging.system", "camel"),
103 KeyValue::new(
104 "correlation_id",
105 capped_correlation_id(exchange.correlation_id()).to_string(),
106 ),
107 KeyValue::new("route_id", self.route_id.clone()),
108 KeyValue::new("step_id", self.step_id.clone()),
109 KeyValue::new("step_index", self.step_index as i64),
110 KeyValue::new("headers_count", 0i64),
111 KeyValue::new("body_type", ""),
112 KeyValue::new("has_error", false),
113 ];
114 let mut attr_count = 5;
115
116 if self.detail_level >= DetailLevel::Medium {
117 attributes[5] = KeyValue::new("headers_count", exchange.input.headers.len() as i64);
118 attributes[6] = KeyValue::new("body_type", body_type_name(&exchange.input.body));
119 attributes[7] = KeyValue::new("has_error", exchange.has_error());
120 attr_count = 8;
121 }
122
123 let span = tracer
125 .span_builder(span_name)
126 .with_kind(SpanKind::Internal)
127 .with_attributes(attributes[..attr_count].iter().cloned())
128 .start_with_context(&tracer, &parent_cx);
129
130 let cx = OtelContext::current_with_span(span);
132
133 exchange.otel_context = cx.clone();
135
136 let tracing_span = tracing::info_span!(
138 target: "camel_tracer",
139 "step",
140 correlation_id = %exchange.correlation_id(),
141 route_id = %self.route_id,
142 step_id = %self.step_id,
143 step_index = self.step_index,
144 duration_ms = tracing::field::Empty,
145 status = tracing::field::Empty,
146 headers_count = tracing::field::Empty,
147 body_type = tracing::field::Empty,
148 has_error = tracing::field::Empty,
149 output_body_type = tracing::field::Empty,
150 header_0 = tracing::field::Empty,
151 header_1 = tracing::field::Empty,
152 header_2 = tracing::field::Empty,
153 error = tracing::field::Empty,
154 error_type = tracing::field::Empty,
155 );
156
157 if self.detail_level >= DetailLevel::Medium {
158 tracing_span.record("headers_count", exchange.input.headers.len() as u64);
159 tracing_span.record("body_type", body_type_name(&exchange.input.body));
160 tracing_span.record("has_error", exchange.has_error());
161 }
162
163 if self.detail_level >= DetailLevel::Full {
164 let headers: Vec<_> = exchange.input.headers.iter().take(3).collect();
165 if let Some((k, v)) = headers.first() {
166 tracing_span.record("header_0", format!("{k}={v:?}"));
167 }
168 if let Some((k, v)) = headers.get(1) {
169 tracing_span.record("header_1", format!("{k}={v:?}"));
170 }
171 if let Some((k, v)) = headers.get(2) {
172 tracing_span.record("header_2", format!("{k}={v:?}"));
173 }
174 }
175
176 let mut inner = self.inner.clone();
177 let detail_level = self.detail_level.clone();
178 let metrics = self.metrics.clone();
179 let route_id = self.route_id.clone();
180
181 Box::pin(
182 async move {
183 let _guard = SpanEndGuard(cx.clone());
189
190 let result = inner.ready().await?.call(exchange).await;
191
192 let duration = start.elapsed();
193 let duration_ms = duration.as_millis() as u64;
194 tracing::Span::current().record("duration_ms", duration_ms);
195
196 cx.span()
198 .set_attribute(KeyValue::new("duration_ms", duration_ms as i64));
199
200 if let Some(ref metrics) = metrics {
202 metrics.record_exchange_duration(&route_id, duration);
203 metrics.increment_exchanges(&route_id);
204
205 if let Err(e) = &result {
206 metrics.increment_errors(&route_id, e.classify());
207 }
208 }
209
210 match &result {
211 Ok(ex) => {
212 tracing::Span::current().record("status", "success");
213 cx.span().set_status(Status::Ok);
214
215 if detail_level >= DetailLevel::Medium {
216 tracing::Span::current()
217 .record("output_body_type", body_type_name(&ex.input.body));
218 cx.span().set_attribute(KeyValue::new(
219 "output_body_type",
220 body_type_name(&ex.input.body),
221 ));
222 }
223 }
224 Err(e) => {
225 let error_class = e.classify();
226 cx.span().set_status(Status::error(e.to_string()));
227 cx.span().add_event(
228 "error",
229 vec![
230 KeyValue::new("error.type", error_class.to_string()),
231 KeyValue::new("error.message", e.to_string()),
232 ],
233 );
234 tracing::Span::current().record("status", "error");
235 tracing::Span::current().record("error", e.to_string());
236 tracing::Span::current().record("error_type", error_class);
237 }
238 }
239
240 result
242 }
243 .instrument(tracing_span),
244 )
245 }
246}
247
248impl Clone for TracingProcessor {
249 fn clone(&self) -> Self {
250 Self {
251 inner: self.inner.clone(),
252 route_id: self.route_id.clone(),
253 step_id: self.step_id.clone(),
254 span_name: self.span_name.clone(),
255 step_index: self.step_index,
256 detail_level: self.detail_level.clone(),
257 metrics: self.metrics.clone(),
258 }
259 }
260}
261
262fn capped_correlation_id(id: &str) -> &str {
264 const CAP: usize = 128;
265 if id.len() > CAP {
266 "<oversized:correlation_id>"
267 } else {
268 id
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
286 use camel_api::{BoxProcessorExt, IdentityProcessor, Message, Value};
287 use opentelemetry::trace::{SpanContext, SpanId, TraceFlags, TraceId, TraceState};
288 use tower::ServiceExt;
289
290 #[tokio::test]
291 async fn test_tracing_processor_minimal() {
292 let inner = BoxProcessor::new(IdentityProcessor);
293 let mut tracer = TracingProcessor::new(
294 inner,
295 "test-route".to_string(),
296 0,
297 DetailLevel::Minimal,
298 None,
299 );
300
301 let exchange = Exchange::new(Message::default());
302 let result = tracer.ready().await.unwrap().call(exchange).await;
303
304 assert!(result.is_ok());
305 }
306
307 #[tokio::test]
308 async fn test_tracing_processor_medium_detail() {
309 let inner = BoxProcessor::new(IdentityProcessor);
310 let mut tracer = TracingProcessor::new(
311 inner,
312 "test-route".to_string(),
313 0,
314 DetailLevel::Medium,
315 None,
316 );
317
318 let exchange = Exchange::new(Message::default());
319 let result = tracer.ready().await.unwrap().call(exchange).await;
320
321 assert!(result.is_ok());
322 }
323
324 #[tokio::test]
325 async fn test_tracing_processor_full_detail() {
326 let inner = BoxProcessor::new(IdentityProcessor);
327 let mut tracer =
328 TracingProcessor::new(inner, "test-route".to_string(), 0, DetailLevel::Full, None);
329
330 let mut exchange = Exchange::new(Message::default());
331 exchange
332 .input
333 .headers
334 .insert("test".to_string(), Value::String("value".into()));
335
336 let result = tracer.ready().await.unwrap().call(exchange).await;
337
338 assert!(result.is_ok());
339 }
340
341 #[tokio::test]
342 async fn test_tracing_processor_clone() {
343 let inner = BoxProcessor::new(IdentityProcessor);
344 let tracer = TracingProcessor::new(
345 inner,
346 "test-route".to_string(),
347 1,
348 DetailLevel::Minimal,
349 None,
350 );
351
352 let mut cloned = tracer.clone();
353 let exchange = Exchange::new(Message::default());
354 let result = cloned.ready().await.unwrap().call(exchange).await;
355 assert!(result.is_ok());
356 }
357
358 #[tokio::test]
359 async fn test_tracing_processor_propagates_otel_context() {
360 let inner = BoxProcessor::new(IdentityProcessor);
361 let mut tracer = TracingProcessor::new(
362 inner,
363 "test-route".to_string(),
364 0,
365 DetailLevel::Minimal,
366 None,
367 );
368
369 let exchange = Exchange::new(Message::default());
371 assert!(
372 !exchange.otel_context.span().span_context().is_valid(),
373 "Initial context should have invalid span"
374 );
375
376 let result = tracer.ready().await.unwrap().call(exchange).await;
377
378 let output_exchange = result.unwrap();
380
381 let _span_context = output_exchange.otel_context.span().span_context();
386 }
387
388 #[tokio::test]
389 async fn test_tracing_processor_with_parent_context() {
390 let inner = BoxProcessor::new(IdentityProcessor);
391 let mut tracer = TracingProcessor::new(
392 inner,
393 "test-route".to_string(),
394 0,
395 DetailLevel::Minimal,
396 None,
397 );
398
399 let trace_id = TraceId::from_hex("12345678901234567890123456789012").unwrap();
401 let span_id = SpanId::from_hex("1234567890123456").unwrap();
402 let parent_span_context = SpanContext::new(
403 trace_id,
404 span_id,
405 TraceFlags::SAMPLED,
406 true, TraceState::default(),
408 );
409
410 let mut exchange = Exchange::new(Message::default());
412 exchange.otel_context = OtelContext::new().with_remote_span_context(parent_span_context);
413
414 let initial_span_context = exchange.otel_context.span().span_context().clone();
416
417 assert!(
419 exchange.otel_context.span().span_context().is_valid(),
420 "Parent context should be valid"
421 );
422 let _parent_trace_id = exchange.otel_context.span().span_context().trace_id();
423
424 let result = tracer.ready().await.unwrap().call(exchange).await;
425
426 let output_exchange = result.unwrap();
427
428 let output_span = output_exchange.otel_context.span();
431 let _output_trace_id = output_span.span_context().trace_id();
434
435 let output_span_context = output_span.span_context();
439 assert!(
442 !std::ptr::eq(&initial_span_context, output_span_context),
443 "exchange.otel_context should have been updated with a new child span context"
444 );
445 }
446
447 #[tokio::test]
448 async fn test_tracing_processor_records_error() {
449 let failing_processor = BoxProcessor::from_fn(|_ex: Exchange| async move {
451 Err(CamelError::ProcessorError("intentional test error".into()))
452 });
453
454 let mut tracer = TracingProcessor::new(
455 failing_processor,
456 "test-route".to_string(),
457 0,
458 DetailLevel::Minimal,
459 None,
460 );
461
462 let exchange = Exchange::new(Message::default());
463 let result = tracer.ready().await.unwrap().call(exchange).await;
464
465 assert!(result.is_err());
467 let err = result.unwrap_err();
468 assert!(err.to_string().contains("intentional test error"));
469
470 }
475
476 #[tokio::test]
477 async fn test_tracing_processor_span_name_format() {
478 let inner = BoxProcessor::new(IdentityProcessor);
479 let tracer =
480 TracingProcessor::new(inner, "my-route".to_string(), 5, DetailLevel::Minimal, None);
481
482 assert_eq!(tracer.span_name, "my-route:step-5");
483 }
484
485 #[tokio::test]
486 async fn test_tracing_processor_chained_propagation() {
487 let processor1 = BoxProcessor::new(IdentityProcessor);
489 let mut tracer1 = TracingProcessor::new(
490 processor1,
491 "route1".to_string(),
492 0,
493 DetailLevel::Minimal,
494 None,
495 );
496
497 let processor2 = BoxProcessor::new(IdentityProcessor);
498 let mut tracer2 = TracingProcessor::new(
499 processor2,
500 "route2".to_string(),
501 1,
502 DetailLevel::Minimal,
503 None,
504 );
505
506 let exchange = Exchange::new(Message::default());
507 let result1 = tracer1.ready().await.unwrap().call(exchange).await;
508 let exchange1 = result1.unwrap();
509
510 let result2 = tracer2.ready().await.unwrap().call(exchange1).await;
512 let exchange2 = result2.unwrap();
513
514 let _ = exchange2.otel_context;
517 }
518
519 #[test]
520 fn capped_correlation_id_uses_sentinel_for_oversized() {
521 assert_eq!(
522 capped_correlation_id(&"x".repeat(200)),
523 "<oversized:correlation_id>"
524 );
525 assert_eq!(
526 capped_correlation_id(&"y".repeat(300)),
527 "<oversized:correlation_id>"
528 );
529 assert_eq!(capped_correlation_id("abc-123"), "abc-123");
530 }
531}