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::{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 );
139 result
140 })
141 }
142}
143
144fn record_step_metrics(
150 metrics: Option<&Arc<dyn MetricsCollector>>,
151 route_id: &str,
152 levers: &MetricsLeversConfig,
153 duration: std::time::Duration,
154 result: &Result<Exchange, CamelError>,
155) {
156 let Some(metrics) = metrics else { return };
157 if levers.durations_enabled() {
158 metrics.record_exchange_duration(route_id, duration);
159 }
160 if levers.exchanges_enabled() {
161 metrics.increment_exchanges(route_id);
162 }
163 if let Err(e) = result {
164 let error_class = e.classify();
165 if error_class != CIRCUIT_OPEN {
166 metrics.increment_errors(route_id, error_class);
168 }
169 }
170}
171
172impl Service<Exchange> for TracingProcessor {
173 type Response = Exchange;
174 type Error = CamelError;
175 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
176
177 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
178 self.inner.poll_ready(cx)
179 }
180
181 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
182 let start = Instant::now();
183
184 if !self.spans_enabled {
188 return self.call_metrics_only(exchange, start);
189 }
190
191 let span_name = self.span_name.clone();
192 let span_kind = self.span_kind.clone();
193
194 let tracer = global::tracer_with_scope(
196 InstrumentationScope::builder("camel-core")
197 .with_version(env!("CARGO_PKG_VERSION"))
198 .build(),
199 );
200
201 let parent_cx = exchange.otel_context.clone();
203
204 let mut attributes =
206 step_span_attributes(&self.route_id, self.step_index, exchange.correlation_id());
207
208 if self.detail_level >= DetailLevel::Medium {
209 attributes.push(KeyValue::new(
210 "headers_count",
211 exchange.input.headers.len() as i64,
212 ));
213 attributes.push(KeyValue::new(
214 "body_type",
215 body_type_name(&exchange.input.body),
216 ));
217 attributes.push(KeyValue::new("has_error", exchange.has_error()));
218 }
219
220 let span = tracer
222 .span_builder(span_name)
223 .with_kind(span_kind)
224 .with_attributes(attributes.iter().cloned())
225 .start_with_context(&tracer, &parent_cx);
226
227 let cx = parent_cx.with_span(span);
231
232 exchange.otel_context = cx.clone();
234
235 let tracing_span = tracing::info_span!(
237 target: "camel_tracer",
238 "step",
239 correlation_id = %exchange.correlation_id(),
240 route_id = %self.route_id,
241 step_id = %self.step_id,
242 step_index = self.step_index,
243 duration_ms = tracing::field::Empty,
244 status = tracing::field::Empty,
245 headers_count = tracing::field::Empty,
246 body_type = tracing::field::Empty,
247 has_error = tracing::field::Empty,
248 output_body_type = tracing::field::Empty,
249 header_0 = tracing::field::Empty,
250 header_1 = tracing::field::Empty,
251 header_2 = tracing::field::Empty,
252 error = tracing::field::Empty,
253 error_type = tracing::field::Empty,
254 );
255
256 if self.detail_level >= DetailLevel::Medium {
257 tracing_span.record("headers_count", exchange.input.headers.len() as u64);
258 tracing_span.record("body_type", body_type_name(&exchange.input.body));
259 tracing_span.record("has_error", exchange.has_error());
260 }
261
262 if self.detail_level >= DetailLevel::Full {
263 let headers: Vec<_> = exchange.input.headers.iter().take(3).collect();
264 if let Some((k, v)) = headers.first() {
265 tracing_span.record("header_0", format!("{k}={v:?}"));
266 }
267 if let Some((k, v)) = headers.get(1) {
268 tracing_span.record("header_1", format!("{k}={v:?}"));
269 }
270 if let Some((k, v)) = headers.get(2) {
271 tracing_span.record("header_2", format!("{k}={v:?}"));
272 }
273 }
274
275 let fresh = self.inner.clone();
281 let mut inner = std::mem::replace(&mut self.inner, fresh);
282 let detail_level = self.detail_level.clone();
283 let metrics = self.metrics.clone();
284 let route_id = self.route_id.clone();
285 let levers = self.metric_levers.clone();
286
287 Box::pin(
288 async move {
289 let _guard = SpanEndGuard(cx.clone());
295
296 let result = inner.call(exchange).await;
297
298 let duration = start.elapsed();
299 let duration_ms = duration.as_millis() as u64;
300 tracing::Span::current().record("duration_ms", duration_ms);
301
302 record_step_metrics(metrics.as_ref(), &route_id, &levers, duration, &result);
304
305 match result {
306 Ok(mut ex) => {
307 tracing::Span::current().record("status", "success");
308 cx.span().set_status(Status::Ok);
309
310 if detail_level >= DetailLevel::Medium {
311 tracing::Span::current()
312 .record("output_body_type", body_type_name(&ex.input.body));
313 cx.span().set_attribute(KeyValue::new(
314 "output_body_type",
315 body_type_name(&ex.input.body),
316 ));
317 }
318
319 ex.otel_context = parent_cx.clone();
322 Ok(ex)
323 }
324 Err(e) => {
325 record_exception(&cx.span(), &e);
326 let error_class = e.classify();
327 tracing::Span::current().record("status", "error");
328 tracing::Span::current().record("error", e.to_string());
329 tracing::Span::current().record("error_type", error_class);
330 Err(e)
331 }
332 }
333 }
334 .instrument(tracing_span),
335 )
336 }
337}
338
339impl Clone for TracingProcessor {
340 fn clone(&self) -> Self {
341 Self {
342 inner: self.inner.clone(),
343 route_id: self.route_id.clone(),
344 step_id: self.step_id.clone(),
345 span_name: self.span_name.clone(),
346 step_index: self.step_index,
347 detail_level: self.detail_level.clone(),
348 metrics: self.metrics.clone(),
349 span_kind: self.span_kind.clone(),
350 spans_enabled: self.spans_enabled,
351 metric_levers: self.metric_levers.clone(),
352 }
353 }
354}
355
356pub(crate) fn capped_correlation_id(id: &str) -> &str {
358 const CAP: usize = 128;
359 if id.len() > CAP {
360 "<oversized:correlation_id>"
361 } else {
362 id
363 }
364}
365
366pub(crate) fn step_span_attributes(
373 route_id: &str,
374 step_index: usize,
375 correlation_id: &str,
376) -> Vec<KeyValue> {
377 vec![
378 KeyValue::new("messaging.system", "camel"),
379 KeyValue::new(
380 "correlation_id",
381 capped_correlation_id(correlation_id).to_string(),
382 ),
383 KeyValue::new("route_id", route_id.to_string()),
384 KeyValue::new("step_index", step_index as i64),
385 ]
386}
387
388pub(crate) fn record_exception(span: &SpanRef<'_>, e: &CamelError) {
389 let error_class = e.classify();
390 span.set_status(Status::error(e.to_string()));
391 span.add_event(
392 "exception",
393 vec![
394 KeyValue::new("exception.type", error_class.to_string()),
395 KeyValue::new("exception.message", e.to_string()),
396 ],
397 );
398}
399
400#[cfg(test)]
401#[path = "tracer_tests.rs"]
402mod tests;