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