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 tracing::Instrument;
11
12use crate::shared::observability::domain::DetailLevel;
13use camel_api::metrics::MetricsCollector;
14use camel_api::{Body, BoxProcessor, CamelError, Exchange};
15
16struct SpanEndGuard(OtelContext);
20
21impl Drop for SpanEndGuard {
22 fn drop(&mut self) {
23 self.0.span().end();
24 }
25}
26
27fn body_type_name(body: &Body) -> &'static str {
29 match body {
30 Body::Empty => "empty",
31 Body::Bytes(_) => "bytes",
32 Body::Text(_) => "text",
33 Body::Json(_) => "json",
34 Body::Xml(_) => "xml",
35 Body::Stream(_) => "stream",
36 _ => "unknown",
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 fresh = self.inner.clone();
182 let mut inner = std::mem::replace(&mut self.inner, fresh);
183 let detail_level = self.detail_level.clone();
184 let metrics = self.metrics.clone();
185 let route_id = self.route_id.clone();
186
187 Box::pin(
188 async move {
189 let _guard = SpanEndGuard(cx.clone());
195
196 let result = inner.call(exchange).await;
197
198 let duration = start.elapsed();
199 let duration_ms = duration.as_millis() as u64;
200 tracing::Span::current().record("duration_ms", duration_ms);
201
202 cx.span()
204 .set_attribute(KeyValue::new("duration_ms", duration_ms as i64));
205
206 if let Some(ref metrics) = metrics {
208 metrics.record_exchange_duration(&route_id, duration);
209 metrics.increment_exchanges(&route_id);
210
211 if let Err(e) = &result {
212 metrics.increment_errors(&route_id, e.classify());
213 }
214 }
215
216 match &result {
217 Ok(ex) => {
218 tracing::Span::current().record("status", "success");
219 cx.span().set_status(Status::Ok);
220
221 if detail_level >= DetailLevel::Medium {
222 tracing::Span::current()
223 .record("output_body_type", body_type_name(&ex.input.body));
224 cx.span().set_attribute(KeyValue::new(
225 "output_body_type",
226 body_type_name(&ex.input.body),
227 ));
228 }
229 }
230 Err(e) => {
231 let error_class = e.classify();
232 cx.span().set_status(Status::error(e.to_string()));
233 cx.span().add_event(
234 "error",
235 vec![
236 KeyValue::new("error.type", error_class.to_string()),
237 KeyValue::new("error.message", e.to_string()),
238 ],
239 );
240 tracing::Span::current().record("status", "error");
241 tracing::Span::current().record("error", e.to_string());
242 tracing::Span::current().record("error_type", error_class);
243 }
244 }
245
246 result
248 }
249 .instrument(tracing_span),
250 )
251 }
252}
253
254impl Clone for TracingProcessor {
255 fn clone(&self) -> Self {
256 Self {
257 inner: self.inner.clone(),
258 route_id: self.route_id.clone(),
259 step_id: self.step_id.clone(),
260 span_name: self.span_name.clone(),
261 step_index: self.step_index,
262 detail_level: self.detail_level.clone(),
263 metrics: self.metrics.clone(),
264 }
265 }
266}
267
268fn capped_correlation_id(id: &str) -> &str {
270 const CAP: usize = 128;
271 if id.len() > CAP {
272 "<oversized:correlation_id>"
273 } else {
274 id
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
292 use camel_api::{BoxProcessorExt, IdentityProcessor, Message, Value};
293 use opentelemetry::trace::{SpanContext, SpanId, TraceFlags, TraceId, TraceState};
294 use std::time::Duration;
295 use tokio::sync::{OwnedSemaphorePermit, Semaphore};
296 use tower::ServiceExt;
297
298 #[tokio::test]
299 async fn test_tracing_processor_minimal() {
300 let inner = BoxProcessor::new(IdentityProcessor);
301 let mut tracer = TracingProcessor::new(
302 inner,
303 "test-route".to_string(),
304 0,
305 DetailLevel::Minimal,
306 None,
307 );
308
309 let exchange = Exchange::new(Message::default());
310 let result = tracer.ready().await.unwrap().call(exchange).await;
311
312 assert!(result.is_ok());
313 }
314
315 #[tokio::test]
316 async fn test_tracing_processor_medium_detail() {
317 let inner = BoxProcessor::new(IdentityProcessor);
318 let mut tracer = TracingProcessor::new(
319 inner,
320 "test-route".to_string(),
321 0,
322 DetailLevel::Medium,
323 None,
324 );
325
326 let exchange = Exchange::new(Message::default());
327 let result = tracer.ready().await.unwrap().call(exchange).await;
328
329 assert!(result.is_ok());
330 }
331
332 #[tokio::test]
333 async fn test_tracing_processor_full_detail() {
334 let inner = BoxProcessor::new(IdentityProcessor);
335 let mut tracer =
336 TracingProcessor::new(inner, "test-route".to_string(), 0, DetailLevel::Full, None);
337
338 let mut exchange = Exchange::new(Message::default());
339 exchange
340 .input
341 .headers
342 .insert("test".to_string(), Value::String("value".into()));
343
344 let result = tracer.ready().await.unwrap().call(exchange).await;
345
346 assert!(result.is_ok());
347 }
348
349 #[tokio::test]
350 async fn test_tracing_processor_clone() {
351 let inner = BoxProcessor::new(IdentityProcessor);
352 let tracer = TracingProcessor::new(
353 inner,
354 "test-route".to_string(),
355 1,
356 DetailLevel::Minimal,
357 None,
358 );
359
360 let mut cloned = tracer.clone();
361 let exchange = Exchange::new(Message::default());
362 let result = cloned.ready().await.unwrap().call(exchange).await;
363 assert!(result.is_ok());
364 }
365
366 #[tokio::test]
367 async fn test_tracing_processor_propagates_otel_context() {
368 let inner = BoxProcessor::new(IdentityProcessor);
369 let mut tracer = TracingProcessor::new(
370 inner,
371 "test-route".to_string(),
372 0,
373 DetailLevel::Minimal,
374 None,
375 );
376
377 let exchange = Exchange::new(Message::default());
379 assert!(
380 !exchange.otel_context.span().span_context().is_valid(),
381 "Initial context should have invalid span"
382 );
383
384 let result = tracer.ready().await.unwrap().call(exchange).await;
385
386 let output_exchange = result.unwrap();
388
389 let _span_context = output_exchange.otel_context.span().span_context();
394 }
395
396 #[tokio::test]
397 async fn test_tracing_processor_with_parent_context() {
398 let inner = BoxProcessor::new(IdentityProcessor);
399 let mut tracer = TracingProcessor::new(
400 inner,
401 "test-route".to_string(),
402 0,
403 DetailLevel::Minimal,
404 None,
405 );
406
407 let trace_id = TraceId::from_hex("12345678901234567890123456789012").unwrap();
409 let span_id = SpanId::from_hex("1234567890123456").unwrap();
410 let parent_span_context = SpanContext::new(
411 trace_id,
412 span_id,
413 TraceFlags::SAMPLED,
414 true, TraceState::default(),
416 );
417
418 let mut exchange = Exchange::new(Message::default());
420 exchange.otel_context = OtelContext::new().with_remote_span_context(parent_span_context);
421
422 let initial_span_context = exchange.otel_context.span().span_context().clone();
424
425 assert!(
427 exchange.otel_context.span().span_context().is_valid(),
428 "Parent context should be valid"
429 );
430 let _parent_trace_id = exchange.otel_context.span().span_context().trace_id();
431
432 let result = tracer.ready().await.unwrap().call(exchange).await;
433
434 let output_exchange = result.unwrap();
435
436 let output_span = output_exchange.otel_context.span();
439 let _output_trace_id = output_span.span_context().trace_id();
442
443 let output_span_context = output_span.span_context();
447 assert!(
450 !std::ptr::eq(&initial_span_context, output_span_context),
451 "exchange.otel_context should have been updated with a new child span context"
452 );
453 }
454
455 #[tokio::test]
456 async fn test_tracing_processor_records_error() {
457 let failing_processor = BoxProcessor::from_fn(|_ex: Exchange| async move {
459 Err(CamelError::ProcessorError("intentional test error".into()))
460 });
461
462 let mut tracer = TracingProcessor::new(
463 failing_processor,
464 "test-route".to_string(),
465 0,
466 DetailLevel::Minimal,
467 None,
468 );
469
470 let exchange = Exchange::new(Message::default());
471 let result = tracer.ready().await.unwrap().call(exchange).await;
472
473 assert!(result.is_err());
475 let err = result.unwrap_err();
476 assert!(err.to_string().contains("intentional test error"));
477
478 }
483
484 #[tokio::test]
485 async fn test_tracing_processor_span_name_format() {
486 let inner = BoxProcessor::new(IdentityProcessor);
487 let tracer =
488 TracingProcessor::new(inner, "my-route".to_string(), 5, DetailLevel::Minimal, None);
489
490 assert_eq!(tracer.span_name, "my-route:step-5");
491 }
492
493 #[tokio::test]
494 async fn test_tracing_processor_chained_propagation() {
495 let processor1 = BoxProcessor::new(IdentityProcessor);
497 let mut tracer1 = TracingProcessor::new(
498 processor1,
499 "route1".to_string(),
500 0,
501 DetailLevel::Minimal,
502 None,
503 );
504
505 let processor2 = BoxProcessor::new(IdentityProcessor);
506 let mut tracer2 = TracingProcessor::new(
507 processor2,
508 "route2".to_string(),
509 1,
510 DetailLevel::Minimal,
511 None,
512 );
513
514 let exchange = Exchange::new(Message::default());
515 let result1 = tracer1.ready().await.unwrap().call(exchange).await;
516 let exchange1 = result1.unwrap();
517
518 let result2 = tracer2.ready().await.unwrap().call(exchange1).await;
520 let exchange2 = result2.unwrap();
521
522 let _ = exchange2.otel_context;
525 }
526
527 #[test]
528 fn capped_correlation_id_uses_sentinel_for_oversized() {
529 assert_eq!(
530 capped_correlation_id(&"x".repeat(200)),
531 "<oversized:correlation_id>"
532 );
533 assert_eq!(
534 capped_correlation_id(&"y".repeat(300)),
535 "<oversized:correlation_id>"
536 );
537 assert_eq!(capped_correlation_id("abc-123"), "abc-123");
538 }
539
540 struct PermitGateInner {
546 semaphore: Arc<Semaphore>,
547 pending_permit: Option<OwnedSemaphorePermit>,
548 }
549
550 impl PermitGateInner {
551 fn new() -> Self {
552 Self {
553 semaphore: Arc::new(Semaphore::new(1)),
554 pending_permit: None,
555 }
556 }
557 }
558
559 impl Clone for PermitGateInner {
560 fn clone(&self) -> Self {
561 Self {
562 semaphore: Arc::clone(&self.semaphore),
563 pending_permit: None,
564 }
565 }
566 }
567
568 impl Service<Exchange> for PermitGateInner {
569 type Response = Exchange;
570 type Error = CamelError;
571 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
572
573 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
574 if self.pending_permit.is_some() {
576 return Poll::Ready(Ok(()));
577 }
578 let mut fut = std::pin::pin!(Arc::clone(&self.semaphore).acquire_owned());
579 match fut.as_mut().poll(cx) {
580 Poll::Ready(Ok(permit)) => {
581 self.pending_permit = Some(permit);
582 Poll::Ready(Ok(()))
583 }
584 Poll::Pending => Poll::Pending,
585 Poll::Ready(Err(err)) => {
587 Poll::Ready(Err(CamelError::ProcessorError(err.to_string())))
588 }
589 }
590 }
591
592 fn call(&mut self, exchange: Exchange) -> Self::Future {
593 let permit = self.pending_permit.take();
594 Box::pin(async move {
595 match permit {
596 Some(_permit) => Ok(exchange),
597 None => Err(CamelError::ProcessorError(
598 "call() invoked without a reserved permit".into(),
599 )),
600 }
601 })
602 }
603 }
604
605 #[tokio::test]
606 async fn tracing_processor_does_not_re_ready_clone() {
607 let mock_inner = BoxProcessor::new(PermitGateInner::new());
608 let mut tracing_proc =
609 TracingProcessor::new(mock_inner, "r".to_string(), 0, DetailLevel::Minimal, None);
610
611 let exchange = Exchange::new(Message::default());
612 let outcome = tokio::time::timeout(Duration::from_secs(5), async {
613 tracing_proc.ready().await.unwrap().call(exchange).await
614 })
615 .await
616 .expect("deadlock: TracingProcessor re-readied a clone whose permit was dropped");
617
618 assert!(outcome.is_ok());
619 }
620
621 #[tokio::test]
622 async fn tracing_processor_reusable_across_sequential_cycles() {
623 let mock_inner = BoxProcessor::new(PermitGateInner::new());
624 let mut tracing_proc =
625 TracingProcessor::new(mock_inner, "r".to_string(), 0, DetailLevel::Minimal, None);
626
627 let ex_a = Exchange::new(Message::default());
628 let outcome_a = tokio::time::timeout(Duration::from_secs(5), async {
629 tracing_proc.ready().await.unwrap().call(ex_a).await
630 })
631 .await
632 .expect("first cycle timed out");
633 assert!(outcome_a.is_ok());
634
635 let ex_b = Exchange::new(Message::default());
636 let outcome_b = tokio::time::timeout(Duration::from_secs(5), async {
637 tracing_proc.ready().await.unwrap().call(ex_b).await
638 })
639 .await
640 .expect("second cycle timed out");
641 assert!(outcome_b.is_ok());
642 }
643}