camel_core/shared/observability/adapters/
tracer.rs1use 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::{Body, BoxProcessor, CIRCUIT_OPEN, CamelError, Exchange, SpanKindHint};
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
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 span_kind: SpanKind,
59 spans_enabled: bool,
63 metric_levers: MetricsLeversConfig,
65}
66
67pub(crate) fn step_id_for(index: usize) -> String {
70 format!("step-{index}")
71}
72
73impl TracingProcessor {
74 pub fn new(
81 inner: BoxProcessor,
82 route_id: String,
83 step_index: usize,
84 detail_level: DetailLevel,
85 metrics: Option<Arc<dyn MetricsCollector>>,
86 label: Option<Arc<str>>,
87 kind_hint: SpanKindHint,
88 ) -> Self {
89 let step_id = step_id_for(step_index);
90 let span_name = format!("{route_id}:{}", label.as_deref().unwrap_or(&step_id));
91 let span_kind = match kind_hint {
92 SpanKindHint::Internal => SpanKind::Internal,
93 SpanKindHint::Producer => SpanKind::Producer,
94 SpanKindHint::Consumer => SpanKind::Consumer,
95 SpanKindHint::Client => SpanKind::Client,
96 SpanKindHint::Server => SpanKind::Server,
97 _ => SpanKind::Internal,
100 };
101 Self {
102 inner,
103 route_id,
104 step_id,
105 span_name,
106 step_index,
107 detail_level,
108 metrics,
109 span_kind,
110 spans_enabled: true,
114 metric_levers: MetricsLeversConfig::default(),
115 }
116 }
117
118 pub fn with_spans_enabled(mut self, enabled: bool) -> Self {
120 self.spans_enabled = enabled;
121 self
122 }
123
124 pub fn with_metric_levers(mut self, levers: MetricsLeversConfig) -> Self {
126 self.metric_levers = levers;
127 self
128 }
129
130 fn call_metrics_only(
134 &mut self,
135 exchange: Exchange,
136 start: Instant,
137 ) -> Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>> {
138 let fresh = self.inner.clone();
139 let mut inner = std::mem::replace(&mut self.inner, fresh);
140 let metrics = self.metrics.clone();
141 let route_id = self.route_id.clone();
142 let levers = self.metric_levers.clone();
143 Box::pin(async move {
144 let result = inner.call(exchange).await;
145 record_step_metrics(
146 metrics.as_ref(),
147 &route_id,
148 &levers,
149 start.elapsed(),
150 &result,
151 );
152 result
153 })
154 }
155}
156
157fn record_step_metrics(
163 metrics: Option<&Arc<dyn MetricsCollector>>,
164 route_id: &str,
165 levers: &MetricsLeversConfig,
166 duration: std::time::Duration,
167 result: &Result<Exchange, CamelError>,
168) {
169 let Some(metrics) = metrics else { return };
170 if levers.durations_enabled() {
171 metrics.record_exchange_duration(route_id, duration);
172 }
173 if levers.exchanges_enabled() {
174 metrics.increment_exchanges(route_id);
175 }
176 if let Err(e) = result {
177 let error_class = e.classify();
178 if error_class != CIRCUIT_OPEN {
179 metrics.increment_errors(route_id, error_class);
180 }
181 }
182}
183
184impl Service<Exchange> for TracingProcessor {
185 type Response = Exchange;
186 type Error = CamelError;
187 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
188
189 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
190 self.inner.poll_ready(cx)
191 }
192
193 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
194 let start = Instant::now();
195
196 if !self.spans_enabled {
200 return self.call_metrics_only(exchange, start);
201 }
202
203 let span_name = self.span_name.clone();
204 let span_kind = self.span_kind.clone();
205
206 let tracer = global::tracer_with_scope(
208 InstrumentationScope::builder("camel-core")
209 .with_version(env!("CARGO_PKG_VERSION"))
210 .build(),
211 );
212
213 let parent_cx = exchange.otel_context.clone();
215
216 let mut attributes =
218 step_span_attributes(&self.route_id, self.step_index, exchange.correlation_id());
219
220 if self.detail_level >= DetailLevel::Medium {
221 attributes.push(KeyValue::new(
222 "headers_count",
223 exchange.input.headers.len() as i64,
224 ));
225 attributes.push(KeyValue::new(
226 "body_type",
227 body_type_name(&exchange.input.body),
228 ));
229 attributes.push(KeyValue::new("has_error", exchange.has_error()));
230 }
231
232 let span = tracer
234 .span_builder(span_name)
235 .with_kind(span_kind)
236 .with_attributes(attributes.iter().cloned())
237 .start_with_context(&tracer, &parent_cx);
238
239 let cx = parent_cx.with_span(span);
243
244 exchange.otel_context = cx.clone();
246
247 let tracing_span = tracing::info_span!(
249 target: "camel_tracer",
250 "step",
251 correlation_id = %exchange.correlation_id(),
252 route_id = %self.route_id,
253 step_id = %self.step_id,
254 step_index = self.step_index,
255 duration_ms = tracing::field::Empty,
256 status = tracing::field::Empty,
257 headers_count = tracing::field::Empty,
258 body_type = tracing::field::Empty,
259 has_error = tracing::field::Empty,
260 output_body_type = tracing::field::Empty,
261 header_0 = tracing::field::Empty,
262 header_1 = tracing::field::Empty,
263 header_2 = tracing::field::Empty,
264 error = tracing::field::Empty,
265 error_type = tracing::field::Empty,
266 );
267
268 if self.detail_level >= DetailLevel::Medium {
269 tracing_span.record("headers_count", exchange.input.headers.len() as u64);
270 tracing_span.record("body_type", body_type_name(&exchange.input.body));
271 tracing_span.record("has_error", exchange.has_error());
272 }
273
274 if self.detail_level >= DetailLevel::Full {
275 let headers: Vec<_> = exchange.input.headers.iter().take(3).collect();
276 if let Some((k, v)) = headers.first() {
277 tracing_span.record("header_0", format!("{k}={v:?}"));
278 }
279 if let Some((k, v)) = headers.get(1) {
280 tracing_span.record("header_1", format!("{k}={v:?}"));
281 }
282 if let Some((k, v)) = headers.get(2) {
283 tracing_span.record("header_2", format!("{k}={v:?}"));
284 }
285 }
286
287 let fresh = self.inner.clone();
293 let mut inner = std::mem::replace(&mut self.inner, fresh);
294 let detail_level = self.detail_level.clone();
295 let metrics = self.metrics.clone();
296 let route_id = self.route_id.clone();
297 let levers = self.metric_levers.clone();
298
299 Box::pin(
300 async move {
301 let _guard = SpanEndGuard(cx.clone());
307
308 let result = inner.call(exchange).await;
309
310 let duration = start.elapsed();
311 let duration_ms = duration.as_millis() as u64;
312 tracing::Span::current().record("duration_ms", duration_ms);
313
314 record_step_metrics(metrics.as_ref(), &route_id, &levers, duration, &result);
316
317 match result {
318 Ok(mut ex) => {
319 tracing::Span::current().record("status", "success");
320 cx.span().set_status(Status::Ok);
321
322 if detail_level >= DetailLevel::Medium {
323 tracing::Span::current()
324 .record("output_body_type", body_type_name(&ex.input.body));
325 cx.span().set_attribute(KeyValue::new(
326 "output_body_type",
327 body_type_name(&ex.input.body),
328 ));
329 }
330
331 ex.otel_context = parent_cx.clone();
334 Ok(ex)
335 }
336 Err(e) => {
337 record_exception(&cx.span(), &e);
338 let error_class = e.classify();
339 tracing::Span::current().record("status", "error");
340 tracing::Span::current().record("error", e.to_string());
341 tracing::Span::current().record("error_type", error_class);
342 Err(e)
343 }
344 }
345 }
346 .instrument(tracing_span),
347 )
348 }
349}
350
351impl Clone for TracingProcessor {
352 fn clone(&self) -> Self {
353 Self {
354 inner: self.inner.clone(),
355 route_id: self.route_id.clone(),
356 step_id: self.step_id.clone(),
357 span_name: self.span_name.clone(),
358 step_index: self.step_index,
359 detail_level: self.detail_level.clone(),
360 metrics: self.metrics.clone(),
361 span_kind: self.span_kind.clone(),
362 spans_enabled: self.spans_enabled,
363 metric_levers: self.metric_levers.clone(),
364 }
365 }
366}
367
368pub(crate) fn capped_correlation_id(id: &str) -> &str {
370 const CAP: usize = 128;
371 if id.len() > CAP {
372 "<oversized:correlation_id>"
373 } else {
374 id
375 }
376}
377
378pub(crate) fn step_span_attributes(
385 route_id: &str,
386 step_index: usize,
387 correlation_id: &str,
388) -> Vec<KeyValue> {
389 vec![
390 KeyValue::new("messaging.system", "camel"),
391 KeyValue::new(
392 "correlation_id",
393 capped_correlation_id(correlation_id).to_string(),
394 ),
395 KeyValue::new("route_id", route_id.to_string()),
396 KeyValue::new("step_index", step_index as i64),
397 ]
398}
399
400pub(crate) fn record_exception(span: &SpanRef<'_>, e: &CamelError) {
401 let error_class = e.classify();
402 span.set_status(Status::error(e.to_string()));
403 span.add_event(
404 "exception",
405 vec![
406 KeyValue::new("exception.type", error_class.to_string()),
407 KeyValue::new("exception.message", e.to_string()),
408 ],
409 );
410}
411
412#[cfg(test)]
413#[path = "tracer_tests.rs"]
414mod tests;