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 span_kind: SpanKind,
46 spans_enabled: bool,
50 metric_levers: MetricsLeversConfig,
52}
53
54pub(crate) fn step_id_for(index: usize) -> String {
57 format!("step-{index}")
58}
59
60impl TracingProcessor {
61 pub fn new(
68 inner: BoxProcessor,
69 route_id: String,
70 step_index: usize,
71 detail_level: DetailLevel,
72 metrics: Option<Arc<dyn MetricsCollector>>,
73 label: Option<Arc<str>>,
74 kind_hint: SpanKindHint,
75 ) -> Self {
76 let step_id = step_id_for(step_index);
77 let span_name = format!("{route_id}:{}", label.as_deref().unwrap_or(&step_id));
78 let span_kind = match kind_hint {
79 SpanKindHint::Internal => SpanKind::Internal,
80 SpanKindHint::Producer => SpanKind::Producer,
81 SpanKindHint::Consumer => SpanKind::Consumer,
82 SpanKindHint::Client => SpanKind::Client,
83 SpanKindHint::Server => SpanKind::Server,
84 _ => SpanKind::Internal,
87 };
88 Self {
89 inner,
90 route_id,
91 step_id,
92 span_name,
93 step_index,
94 detail_level,
95 metrics,
96 span_kind,
97 spans_enabled: true,
101 metric_levers: MetricsLeversConfig::default(),
102 }
103 }
104
105 pub fn with_spans_enabled(mut self, enabled: bool) -> Self {
107 self.spans_enabled = enabled;
108 self
109 }
110
111 pub fn with_metric_levers(mut self, levers: MetricsLeversConfig) -> Self {
113 self.metric_levers = levers;
114 self
115 }
116
117 fn call_metrics_only(
121 &mut self,
122 exchange: Exchange,
123 start: Instant,
124 ) -> Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>> {
125 let fresh = self.inner.clone();
126 let mut inner = std::mem::replace(&mut self.inner, fresh);
127 let metrics = self.metrics.clone();
128 let route_id = self.route_id.clone();
129 let levers = self.metric_levers.clone();
130 Box::pin(async move {
131 let result = inner.call(exchange).await;
132 record_step_metrics(
133 metrics.as_ref(),
134 &route_id,
135 &levers,
136 start.elapsed(),
137 &result,
138 true,
139 );
140 result
141 })
142 }
143}
144
145fn record_step_metrics(
157 metrics: Option<&Arc<dyn MetricsCollector>>,
158 route_id: &str,
159 levers: &MetricsLeversConfig,
160 duration: std::time::Duration,
161 result: &Result<Exchange, CamelError>,
162 include_duration: bool,
163) {
164 let Some(metrics) = metrics else { return };
165 if include_duration && levers.durations_enabled() {
166 metrics.record_exchange_duration(route_id, duration);
167 }
168 if levers.exchanges_enabled() {
169 metrics.increment_exchanges(route_id);
170 }
171 if let Err(e) = result {
172 let error_class = e.classify();
173 if error_class != CIRCUIT_OPEN {
174 metrics.increment_errors(route_id, error_class);
176 }
177 }
178}
179
180impl Service<Exchange> for TracingProcessor {
181 type Response = Exchange;
182 type Error = CamelError;
183 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
184
185 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
186 let start = Instant::now();
187 match self.inner.poll_ready(cx) {
188 Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
189 Poll::Pending => Poll::Pending,
190 Poll::Ready(Err(e)) => {
191 record_step_metrics(
201 self.metrics.as_ref(),
202 &self.route_id,
203 &self.metric_levers,
204 start.elapsed(),
205 &Err(e.clone()),
206 false,
207 );
208 Poll::Ready(Err(e))
209 }
210 }
211 }
212
213 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
214 let start = Instant::now();
215
216 if !self.spans_enabled {
220 return self.call_metrics_only(exchange, start);
221 }
222
223 let span_name = self.span_name.clone();
224 let span_kind = self.span_kind.clone();
225
226 let tracer = global::tracer_with_scope(
228 InstrumentationScope::builder("camel-core")
229 .with_version(env!("CARGO_PKG_VERSION"))
230 .build(),
231 );
232
233 let parent_cx = exchange.otel_context.clone();
235
236 let mut attributes =
238 step_span_attributes(&self.route_id, self.step_index, exchange.correlation_id());
239
240 if self.detail_level >= DetailLevel::Medium {
241 attributes.push(KeyValue::new(
242 "headers_count",
243 exchange.input.headers.len() as i64,
244 ));
245 attributes.push(KeyValue::new(
246 "body_type",
247 body_type_name(&exchange.input.body),
248 ));
249 attributes.push(KeyValue::new("has_error", exchange.has_error()));
250 }
251
252 let span = tracer
254 .span_builder(span_name)
255 .with_kind(span_kind)
256 .with_attributes(attributes.iter().cloned())
257 .start_with_context(&tracer, &parent_cx);
258
259 let cx = parent_cx.with_span(span);
263
264 exchange.otel_context = cx.clone();
266
267 let tracing_span = tracing::info_span!(
269 target: "camel_tracer",
270 "step",
271 correlation_id = %exchange.correlation_id(),
272 route_id = %self.route_id,
273 step_id = %self.step_id,
274 step_index = self.step_index,
275 duration_ms = tracing::field::Empty,
276 status = tracing::field::Empty,
277 headers_count = tracing::field::Empty,
278 body_type = tracing::field::Empty,
279 has_error = tracing::field::Empty,
280 output_body_type = tracing::field::Empty,
281 header_0 = tracing::field::Empty,
282 header_1 = tracing::field::Empty,
283 header_2 = tracing::field::Empty,
284 error = tracing::field::Empty,
285 error_type = tracing::field::Empty,
286 );
287
288 if self.detail_level >= DetailLevel::Medium {
289 tracing_span.record("headers_count", exchange.input.headers.len() as u64);
290 tracing_span.record("body_type", body_type_name(&exchange.input.body));
291 tracing_span.record("has_error", exchange.has_error());
292 }
293
294 if self.detail_level >= DetailLevel::Full {
295 let headers: Vec<_> = exchange.input.headers.iter().take(3).collect();
296 if let Some((k, v)) = headers.first() {
297 tracing_span.record("header_0", format!("{k}={v:?}"));
298 }
299 if let Some((k, v)) = headers.get(1) {
300 tracing_span.record("header_1", format!("{k}={v:?}"));
301 }
302 if let Some((k, v)) = headers.get(2) {
303 tracing_span.record("header_2", format!("{k}={v:?}"));
304 }
305 }
306
307 let fresh = self.inner.clone();
313 let mut inner = std::mem::replace(&mut self.inner, fresh);
314 let detail_level = self.detail_level.clone();
315 let metrics = self.metrics.clone();
316 let route_id = self.route_id.clone();
317 let levers = self.metric_levers.clone();
318
319 Box::pin(
320 async move {
321 let _guard = SpanEndGuard(cx.clone());
327
328 let result = inner.call(exchange).await;
329
330 let duration = start.elapsed();
331 let duration_ms = duration.as_millis() as u64;
332 tracing::Span::current().record("duration_ms", duration_ms);
333
334 record_step_metrics(
336 metrics.as_ref(),
337 &route_id,
338 &levers,
339 duration,
340 &result,
341 true,
342 );
343
344 match result {
345 Ok(mut ex) => {
346 tracing::Span::current().record("status", "success");
347 cx.span().set_status(Status::Ok);
348
349 if detail_level >= DetailLevel::Medium {
350 tracing::Span::current()
351 .record("output_body_type", body_type_name(&ex.input.body));
352 cx.span().set_attribute(KeyValue::new(
353 "output_body_type",
354 body_type_name(&ex.input.body),
355 ));
356 }
357
358 ex.otel_context = parent_cx.clone();
361 Ok(ex)
362 }
363 Err(e) => {
364 record_exception(&cx.span(), &e);
365 let error_class = e.classify();
366 tracing::Span::current().record("status", "error");
367 tracing::Span::current().record("error", e.to_string());
368 tracing::Span::current().record("error_type", error_class);
369 Err(e)
370 }
371 }
372 }
373 .instrument(tracing_span),
374 )
375 }
376}
377
378impl Clone for TracingProcessor {
379 fn clone(&self) -> Self {
380 Self {
381 inner: self.inner.clone(),
382 route_id: self.route_id.clone(),
383 step_id: self.step_id.clone(),
384 span_name: self.span_name.clone(),
385 step_index: self.step_index,
386 detail_level: self.detail_level.clone(),
387 metrics: self.metrics.clone(),
388 span_kind: self.span_kind.clone(),
389 spans_enabled: self.spans_enabled,
390 metric_levers: self.metric_levers.clone(),
391 }
392 }
393}
394
395pub(crate) fn capped_correlation_id(id: &str) -> &str {
397 const CAP: usize = 128;
398 if id.len() > CAP {
399 "<oversized:correlation_id>"
400 } else {
401 id
402 }
403}
404
405pub(crate) fn step_span_attributes(
412 route_id: &str,
413 step_index: usize,
414 correlation_id: &str,
415) -> Vec<KeyValue> {
416 vec![
417 KeyValue::new("messaging.system", "camel"),
418 KeyValue::new(
419 "correlation_id",
420 capped_correlation_id(correlation_id).to_string(),
421 ),
422 KeyValue::new("route_id", route_id.to_string()),
423 KeyValue::new("step_index", step_index as i64),
424 ]
425}
426
427pub(crate) fn record_exception(span: &SpanRef<'_>, e: &CamelError) {
428 let error_class = e.classify();
429 span.set_status(Status::error(e.to_string()));
430 span.add_event(
431 "exception",
432 vec![
433 KeyValue::new("exception.type", error_class.to_string()),
434 KeyValue::new("exception.message", e.to_string()),
435 ],
436 );
437}
438
439#[cfg(test)]
440#[path = "tracer_tests.rs"]
441mod tests;