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, SpanRef, Status, TraceContextExt, Tracer};
8use opentelemetry::{Context as OtelContext, InstrumentationScope, KeyValue, global};
9use tower::Service;
10use tracing::Instrument;
11
12use crate::shared::observability::domain::{DetailLevel, MetricsLeversConfig};
13use camel_api::metrics::MetricsCollector;
14use camel_api::{BoxProcessor, CIRCUIT_OPEN, CamelError, Exchange, SpanKindHint, body_type_name};
15
16pub(crate) struct SpanEndGuard(pub(crate) OtelContext);
21
22impl Drop for SpanEndGuard {
23 fn drop(&mut self) {
24 self.0.span().end();
25 }
26}
27
28pub struct TracingProcessor {
37 inner: BoxProcessor,
38 route_id: String,
39 step_id: String,
40 span_name: String,
41 step_index: usize,
42 detail_level: DetailLevel,
43 metrics: Option<Arc<dyn MetricsCollector>>,
44 to_uri: Option<Arc<str>>,
50 span_kind: SpanKind,
52 spans_enabled: bool,
56 metric_levers: MetricsLeversConfig,
58}
59
60pub(crate) fn step_id_for(index: usize) -> String {
63 format!("step-{index}")
64}
65
66impl TracingProcessor {
67 #[allow(clippy::too_many_arguments)]
76 pub fn new(
77 inner: BoxProcessor,
78 route_id: String,
79 step_index: usize,
80 detail_level: DetailLevel,
81 metrics: Option<Arc<dyn MetricsCollector>>,
82 label: Option<Arc<str>>,
83 to_uri: Option<Arc<str>>,
84 kind_hint: SpanKindHint,
85 ) -> Self {
86 let step_id = step_id_for(step_index);
87 let span_name = format!("{route_id}:{}", label.as_deref().unwrap_or(&step_id));
88 let span_kind = match kind_hint {
89 SpanKindHint::Internal => SpanKind::Internal,
90 SpanKindHint::Producer => SpanKind::Producer,
91 SpanKindHint::Consumer => SpanKind::Consumer,
92 SpanKindHint::Client => SpanKind::Client,
93 SpanKindHint::Server => SpanKind::Server,
94 _ => SpanKind::Internal,
97 };
98 Self {
99 inner,
100 route_id,
101 step_id,
102 span_name,
103 step_index,
104 detail_level,
105 metrics,
106 to_uri,
107 span_kind,
108 spans_enabled: true,
112 metric_levers: MetricsLeversConfig::default(),
113 }
114 }
115
116 pub fn with_spans_enabled(mut self, enabled: bool) -> Self {
118 self.spans_enabled = enabled;
119 self
120 }
121
122 pub fn with_metric_levers(mut self, levers: MetricsLeversConfig) -> Self {
124 self.metric_levers = levers;
125 self
126 }
127
128 fn call_metrics_only(
132 &mut self,
133 exchange: Exchange,
134 start: Instant,
135 ) -> Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>> {
136 let fresh = self.inner.clone();
137 let mut inner = std::mem::replace(&mut self.inner, fresh);
138 let metrics = self.metrics.clone();
139 let route_id = self.route_id.clone();
140 let to_uri = self.to_uri.clone();
141 let levers = self.metric_levers.clone();
142 Box::pin(async move {
143 let result = inner.call(exchange).await;
144 record_step_metrics(
145 metrics.as_ref(),
146 &route_id,
147 to_uri.as_deref(),
148 &levers,
149 start.elapsed(),
150 &result,
151 true,
152 );
153 result
154 })
155 }
156}
157
158fn record_step_metrics(
176 metrics: Option<&Arc<dyn MetricsCollector>>,
177 route_id: &str,
178 to_uri: Option<&str>,
179 levers: &MetricsLeversConfig,
180 duration: std::time::Duration,
181 result: &Result<Exchange, CamelError>,
182 include_duration: bool,
183) {
184 let Some(metrics) = metrics else { return };
185 if include_duration && levers.durations_enabled() {
186 metrics.record_exchange_duration(route_id, duration);
187 if let Some(uri) = to_uri {
188 metrics.record_histogram(
190 "step_duration_secs",
191 duration.as_secs_f64(),
192 &[("route", route_id), ("to_uri", uri)],
193 );
194 }
195 }
196 if levers.exchanges_enabled() {
197 metrics.increment_exchanges(route_id);
198 }
199 if let Err(e) = result {
200 let error_class = e.classify();
201 if error_class != CIRCUIT_OPEN {
202 metrics.increment_errors(route_id, error_class);
204 }
205 }
206}
207
208impl Service<Exchange> for TracingProcessor {
209 type Response = Exchange;
210 type Error = CamelError;
211 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
212
213 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
214 let start = Instant::now();
215 match self.inner.poll_ready(cx) {
216 Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
217 Poll::Pending => Poll::Pending,
218 Poll::Ready(Err(e)) => {
219 record_step_metrics(
229 self.metrics.as_ref(),
230 &self.route_id,
231 self.to_uri.as_deref(),
232 &self.metric_levers,
233 start.elapsed(),
234 &Err(e.clone()),
235 false,
236 );
237 Poll::Ready(Err(e))
238 }
239 }
240 }
241
242 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
243 let start = Instant::now();
244
245 if !self.spans_enabled {
249 return self.call_metrics_only(exchange, start);
250 }
251
252 let span_name = self.span_name.clone();
253 let span_kind = self.span_kind.clone();
254
255 let tracer = global::tracer_with_scope(
257 InstrumentationScope::builder("camel-core")
258 .with_version(env!("CARGO_PKG_VERSION"))
259 .build(),
260 );
261
262 let parent_cx = exchange.otel_context.clone();
264
265 let mut attributes =
267 step_span_attributes(&self.route_id, self.step_index, exchange.correlation_id());
268
269 if self.detail_level >= DetailLevel::Medium {
270 attributes.push(KeyValue::new(
271 "headers_count",
272 exchange.input.headers.len() as i64,
273 ));
274 attributes.push(KeyValue::new(
275 "body_type",
276 body_type_name(&exchange.input.body),
277 ));
278 attributes.push(KeyValue::new("has_error", exchange.has_error()));
279 }
280
281 let span = tracer
283 .span_builder(span_name)
284 .with_kind(span_kind)
285 .with_attributes(attributes.iter().cloned())
286 .start_with_context(&tracer, &parent_cx);
287
288 let cx = parent_cx.with_span(span);
292
293 exchange.otel_context = cx.clone();
295
296 let tracing_span = tracing::info_span!(
298 target: "camel_tracer",
299 "step",
300 correlation_id = %exchange.correlation_id(),
301 route_id = %self.route_id,
302 step_id = %self.step_id,
303 step_index = self.step_index,
304 duration_ms = tracing::field::Empty,
305 status = tracing::field::Empty,
306 headers_count = tracing::field::Empty,
307 body_type = tracing::field::Empty,
308 has_error = tracing::field::Empty,
309 output_body_type = tracing::field::Empty,
310 header_0 = tracing::field::Empty,
311 header_1 = tracing::field::Empty,
312 header_2 = tracing::field::Empty,
313 error = tracing::field::Empty,
314 error_type = tracing::field::Empty,
315 );
316
317 if self.detail_level >= DetailLevel::Medium {
318 tracing_span.record("headers_count", exchange.input.headers.len() as u64);
319 tracing_span.record("body_type", body_type_name(&exchange.input.body));
320 tracing_span.record("has_error", exchange.has_error());
321 }
322
323 if self.detail_level >= DetailLevel::Full {
324 let headers: Vec<_> = exchange.input.headers.iter().take(3).collect();
325 if let Some((k, v)) = headers.first() {
326 tracing_span.record("header_0", format!("{k}={v:?}"));
327 }
328 if let Some((k, v)) = headers.get(1) {
329 tracing_span.record("header_1", format!("{k}={v:?}"));
330 }
331 if let Some((k, v)) = headers.get(2) {
332 tracing_span.record("header_2", format!("{k}={v:?}"));
333 }
334 }
335
336 let fresh = self.inner.clone();
342 let mut inner = std::mem::replace(&mut self.inner, fresh);
343 let detail_level = self.detail_level.clone();
344 let metrics = self.metrics.clone();
345 let route_id = self.route_id.clone();
346 let to_uri = self.to_uri.clone();
347 let levers = self.metric_levers.clone();
348
349 Box::pin(
350 async move {
351 let _guard = SpanEndGuard(cx.clone());
357
358 let result = inner.call(exchange).await;
359
360 let duration = start.elapsed();
361 let duration_ms = duration.as_millis() as u64;
362 tracing::Span::current().record("duration_ms", duration_ms);
363
364 record_step_metrics(
366 metrics.as_ref(),
367 &route_id,
368 to_uri.as_deref(),
369 &levers,
370 duration,
371 &result,
372 true,
373 );
374
375 match result {
376 Ok(mut ex) => {
377 tracing::Span::current().record("status", "success");
378 cx.span().set_status(Status::Ok);
379
380 if detail_level >= DetailLevel::Medium {
381 tracing::Span::current()
382 .record("output_body_type", body_type_name(&ex.input.body));
383 cx.span().set_attribute(KeyValue::new(
384 "output_body_type",
385 body_type_name(&ex.input.body),
386 ));
387 }
388
389 ex.otel_context = parent_cx.clone();
392 Ok(ex)
393 }
394 Err(e) => {
395 record_exception(&cx.span(), &e);
396 let error_class = e.classify();
397 tracing::Span::current().record("status", "error");
398 tracing::Span::current().record("error", e.to_string());
399 tracing::Span::current().record("error_type", error_class);
400 Err(e)
401 }
402 }
403 }
404 .instrument(tracing_span),
405 )
406 }
407}
408
409impl Clone for TracingProcessor {
410 fn clone(&self) -> Self {
411 Self {
412 inner: self.inner.clone(),
413 route_id: self.route_id.clone(),
414 step_id: self.step_id.clone(),
415 span_name: self.span_name.clone(),
416 step_index: self.step_index,
417 detail_level: self.detail_level.clone(),
418 metrics: self.metrics.clone(),
419 to_uri: self.to_uri.clone(),
420 span_kind: self.span_kind.clone(),
421 spans_enabled: self.spans_enabled,
422 metric_levers: self.metric_levers.clone(),
423 }
424 }
425}
426
427pub(crate) fn capped_correlation_id(id: &str) -> &str {
429 const CAP: usize = 128;
430 if id.len() > CAP {
431 "<oversized:correlation_id>"
432 } else {
433 id
434 }
435}
436
437pub(crate) fn step_span_attributes(
444 route_id: &str,
445 step_index: usize,
446 correlation_id: &str,
447) -> Vec<KeyValue> {
448 vec![
449 KeyValue::new("messaging.system", "camel"),
450 KeyValue::new(
451 "correlation_id",
452 capped_correlation_id(correlation_id).to_string(),
453 ),
454 KeyValue::new("route_id", route_id.to_string()),
455 KeyValue::new("step_index", step_index as i64),
456 ]
457}
458
459pub(crate) fn record_exception(span: &SpanRef<'_>, e: &CamelError) {
460 let error_class = e.classify();
461 span.set_status(Status::error(e.to_string()));
462 span.add_event(
463 "exception",
464 vec![
465 KeyValue::new("exception.type", error_class.to_string()),
466 KeyValue::new("exception.message", e.to_string()),
467 ],
468 );
469}
470
471#[cfg(test)]
472#[path = "tracer_tests.rs"]
473mod tests;