From d5e5524c1dc02c0feb5ea367ba7090caf239d7dc Mon Sep 17 00:00:00 2001 From: andylokandy Date: Thu, 18 Dec 2025 16:59:42 +0800 Subject: [PATCH 1/3] feat(fastrace-opentelemetry): add bridge for current local context --- fastrace-opentelemetry/CHANGELOG.md | 2 + fastrace-opentelemetry/Cargo.toml | 1 + fastrace-opentelemetry/README.md | 38 ++++++++--- fastrace-opentelemetry/src/lib.rs | 53 ++++++++++++++- fastrace-opentelemetry/tests/context.rs | 75 ++++++++++++++++++++++ fastrace/src/collector/global_collector.rs | 19 +++--- 6 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 fastrace-opentelemetry/tests/context.rs diff --git a/fastrace-opentelemetry/CHANGELOG.md b/fastrace-opentelemetry/CHANGELOG.md index a11875b6..c706e7a5 100644 --- a/fastrace-opentelemetry/CHANGELOG.md +++ b/fastrace-opentelemetry/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +* Add helpers to extract the current fastrace `SpanContext` and convert it as the current OpenTelemetry `Context`. + ## v0.15.0 * Recognise `SpanContext.is_remote` from the `span.parent_span_is_remote` properties on spans. diff --git a/fastrace-opentelemetry/Cargo.toml b/fastrace-opentelemetry/Cargo.toml index 575cca26..cf4b4030 100644 --- a/fastrace-opentelemetry/Cargo.toml +++ b/fastrace-opentelemetry/Cargo.toml @@ -21,4 +21,5 @@ opentelemetry_sdk = { workspace = true } pollster = { version = "0.4.0" } [dev-dependencies] +fastrace = { workspace = true, features = ["enable"] } opentelemetry-otlp = { workspace = true } diff --git a/fastrace-opentelemetry/README.md b/fastrace-opentelemetry/README.md index 9c30c002..9ad8fe3b 100644 --- a/fastrace-opentelemetry/README.md +++ b/fastrace-opentelemetry/README.md @@ -10,8 +10,8 @@ ```toml [dependencies] -fastrace = "0.7" -fastrace-opentelemetry = "0.13" +fastrace = { version = "0.7", features = ["enable"] } +fastrace-opentelemetry = "0.15" ``` ## Setup OpenTelemetry Collector @@ -36,17 +36,15 @@ Zipkin UI is available on [http://127.0.0.1:9411/](http://127.0.0.1:9411/) ```rust, no_run use std::borrow::Cow; + use fastrace::collector::Config; use fastrace::prelude::*; use fastrace_opentelemetry::OpenTelemetryReporter; -use opentelemetry_otlp::ExportConfig; -use opentelemetry_otlp::Protocol; -use opentelemetry_otlp::SpanExporter; -use opentelemetry_otlp::TonicConfig; -use opentelemetry_sdk::Resource; -use opentelemetry::KeyValue; use opentelemetry::InstrumentationScope; +use opentelemetry::KeyValue; +use opentelemetry_otlp::SpanExporter; use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::Resource; // Initialize reporter let reporter = OpenTelemetryReporter::new( @@ -56,7 +54,7 @@ let reporter = OpenTelemetryReporter::new( .with_protocol(opentelemetry_otlp::Protocol::Grpc) .with_timeout(opentelemetry_otlp::OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT) .build() - .expect("initialize oltp exporter"), + .expect("initialize otlp exporter"), Cow::Owned( Resource::builder() .with_attributes([KeyValue::new("service.name", "asynchronous")]) @@ -71,5 +69,25 @@ fastrace::set_reporter(reporter, Config::default()); let root = Span::root("root", SpanContext::random()); } -fastrace::flush() +fastrace::flush(); +``` + +## Activate OpenTelemetry Trace Context + +If you use fastrace spans but also depend on libraries that expect an OpenTelemetry parent +[`Context`](https://docs.rs/opentelemetry/latest/opentelemetry/struct.Context.html), you can bridge +the current fastrace **local parent** into an OpenTelemetry context. + +This requires a local parent to be set for the current thread (e.g. via +[`Span::set_local_parent`](https://docs.rs/fastrace/latest/fastrace/struct.Span.html#method.set_local_parent)). + +```rust +use fastrace_opentelemetry::current_opentelemetry_context; +use opentelemetry::trace::TraceContextExt; +use opentelemetry::Context; + +let _otel_guard = current_opentelemetry_context() + .map(|sc| Context::current().with_remote_span_context(sc).attach()); + +// Call library code that uses `Context::current()`. ``` diff --git a/fastrace-opentelemetry/src/lib.rs b/fastrace-opentelemetry/src/lib.rs index c2ec703b..622c6883 100644 --- a/fastrace-opentelemetry/src/lib.rs +++ b/fastrace-opentelemetry/src/lib.rs @@ -33,7 +33,7 @@ use fastrace::prelude::*; use opentelemetry::InstrumentationScope; use opentelemetry::KeyValue; use opentelemetry::trace::Event; -use opentelemetry::trace::SpanContext; +use opentelemetry::trace::SpanContext as OtelSpanContext; use opentelemetry::trace::SpanKind; use opentelemetry::trace::Status; use opentelemetry::trace::TraceFlags; @@ -75,6 +75,55 @@ pub struct OpenTelemetryReporter { instrumentation_scope: InstrumentationScope, } +/// Returns the OpenTelemetry [`SpanContext`] of the current fastrace local parent span. +/// +/// This helper bridges fastrace's **thread-local parent stack** (set via +/// [`Span::set_local_parent`]) into an OpenTelemetry +/// `SpanContext` so you can interoperate with OpenTelemetry-based instrumentation. +/// +/// It returns `None` when: +/// - fastrace's `enable` feature is disabled (the local parent stack is inert), or +/// - no local parent is currently set for the thread. +/// +/// The returned span context is **non-recording** (it does not create an OpenTelemetry span on +/// its own). To make it usable as a parent for OpenTelemetry spans, attach it to an +/// OpenTelemetry [`Context`] via [`TraceContextExt::with_remote_span_context`]. +/// +/// # Examples +/// +/// ```rust, no_run +/// use fastrace::prelude::*; +/// use opentelemetry::Context; +/// use opentelemetry::trace::TraceContextExt; +/// +/// let root = Span::root("root", SpanContext::random()); +/// let _g = root.set_local_parent(); +/// +/// // Make the fastrace span the "current" OpenTelemetry parent for this thread. +/// let _otel_guard = fastrace_opentelemetry::current_opentelemetry_context() +/// .map(|sc| Context::current().with_remote_span_context(sc).attach()); +/// +/// // Any OpenTelemetry instrumentation that reads `Context::current()` can now +/// // treat the fastrace span as its parent. +/// ``` +pub fn current_opentelemetry_context() -> Option { + let span_context = fastrace::collector::SpanContext::current_local_parent()?; + + let trace_flags = if span_context.sampled { + TraceFlags::SAMPLED + } else { + TraceFlags::default() + }; + + Some(OtelSpanContext::new( + span_context.trace_id.0.into(), + span_context.span_id.0.into(), + trace_flags, + false, + TraceState::default(), + )) +} + pub const SPAN_KIND: &str = "span.kind"; pub const SPAN_STATUS_CODE: &str = "span.status_code"; pub const SPAN_STATUS_DESCRIPTION: &str = "span.status_description"; @@ -173,7 +222,7 @@ impl OpenTelemetryReporter { let events = map_events(events); SpanData { - span_context: SpanContext::new( + span_context: OtelSpanContext::new( trace_id.0.into(), span_id.0.into(), TraceFlags::default(), diff --git a/fastrace-opentelemetry/tests/context.rs b/fastrace-opentelemetry/tests/context.rs new file mode 100644 index 00000000..fd2da1db --- /dev/null +++ b/fastrace-opentelemetry/tests/context.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; +use std::sync::Mutex; + +use fastrace::prelude::*; +use opentelemetry::trace::Span as _; +use opentelemetry::trace::TraceContextExt; +use opentelemetry::trace::Tracer as _; +use opentelemetry::trace::TracerProvider as _; +use opentelemetry::Context; +use opentelemetry_sdk::error::OTelSdkResult; +use opentelemetry_sdk::trace::SdkTracerProvider; +use opentelemetry_sdk::trace::SpanData; +use opentelemetry_sdk::trace::SpanExporter; + +#[derive(Debug, Clone, Default)] +struct CapturingExporter { + spans: Arc>>, +} + +impl CapturingExporter { + fn new() -> (Self, Arc>>) { + let spans = Arc::new(Mutex::new(Vec::new())); + ( + Self { + spans: Arc::clone(&spans), + }, + spans, + ) + } +} + +impl SpanExporter for CapturingExporter { + fn export( + &self, + batch: Vec, + ) -> impl std::future::Future + Send { + self.spans.lock().unwrap().extend(batch); + std::future::ready(Ok(())) + } +} + +#[test] +fn otel_span_can_be_parented_by_fastrace_local_parent() { + let (exporter, exported_spans) = CapturingExporter::new(); + let provider = SdkTracerProvider::builder() + .with_simple_exporter(exporter) + .build(); + let tracer = provider.tracer("fastrace-opentelemetry-test"); + + let root = Span::root("root", SpanContext::random()); + let root_context = SpanContext::from_span(&root).unwrap(); + let _g = root.set_local_parent(); + + let _otel_guard = fastrace_opentelemetry::current_opentelemetry_context() + .map(|sc| Context::current().with_remote_span_context(sc).attach()); + + let mut span = tracer.start("otel-child"); + span.end(); + + provider.force_flush().unwrap(); + provider.shutdown().unwrap(); + + let spans = exported_spans.lock().unwrap(); + assert_eq!(spans.len(), 1); + let span = &spans[0]; + + assert_eq!( + span.span_context.trace_id().to_string(), + root_context.trace_id.to_string() + ); + assert_eq!( + span.parent_span_id.to_string(), + root_context.span_id.to_string() + ); +} diff --git a/fastrace/src/collector/global_collector.rs b/fastrace/src/collector/global_collector.rs index b998e76a..abe2bf78 100644 --- a/fastrace/src/collector/global_collector.rs +++ b/fastrace/src/collector/global_collector.rs @@ -84,8 +84,8 @@ pub fn flush() { { #[cfg(target_family = "wasm")] { - if let Some(global_collector) = GLOBAL_COLLECTOR.lock().as_mut() { - global_collector.handle_commands(); + if let Some(collector) = GLOBAL_COLLECTOR.lock().as_mut() { + collector.handle_commands(); } } @@ -96,8 +96,8 @@ pub fn flush() { std::thread::Builder::new() .name("fastrace-flush".to_string()) .spawn(move || { - if let Some(global_collector) = GLOBAL_COLLECTOR.lock().as_mut() { - global_collector.handle_commands(); + if let Some(collector) = GLOBAL_COLLECTOR.lock().as_mut() { + collector.handle_commands(); } }) .unwrap() @@ -246,11 +246,12 @@ impl GlobalCollector { .name("fastrace-global-collector".to_string()) .spawn(move || { loop { - let mut global_collector = GLOBAL_COLLECTOR.lock(); - let collector = global_collector.as_mut().unwrap(); - let report_interval = collector.config.report_interval; - collector.handle_commands(); - drop(global_collector); + let report_interval = { + let mut collector = GLOBAL_COLLECTOR.lock(); + let collector = collector.as_mut().unwrap(); + collector.handle_commands(); + collector.config.report_interval + }; COMMAND_BUS.wait_timeout(report_interval); } From 14b10ae267a32e7776e270949d23e443bb0018a0 Mon Sep 17 00:00:00 2001 From: andylokandy Date: Thu, 18 Dec 2025 17:26:53 +0800 Subject: [PATCH 2/3] fix --- fastrace-opentelemetry/tests/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastrace-opentelemetry/tests/context.rs b/fastrace-opentelemetry/tests/context.rs index fd2da1db..3e4e8e07 100644 --- a/fastrace-opentelemetry/tests/context.rs +++ b/fastrace-opentelemetry/tests/context.rs @@ -2,11 +2,11 @@ use std::sync::Arc; use std::sync::Mutex; use fastrace::prelude::*; +use opentelemetry::Context; use opentelemetry::trace::Span as _; use opentelemetry::trace::TraceContextExt; use opentelemetry::trace::Tracer as _; use opentelemetry::trace::TracerProvider as _; -use opentelemetry::Context; use opentelemetry_sdk::error::OTelSdkResult; use opentelemetry_sdk::trace::SdkTracerProvider; use opentelemetry_sdk::trace::SpanData; From b17b72b0036ad87b697263505ea94927c534bff0 Mon Sep 17 00:00:00 2001 From: andylokandy Date: Thu, 18 Dec 2025 18:40:36 +0800 Subject: [PATCH 3/3] fix --- fastrace-opentelemetry/CHANGELOG.md | 2 +- fastrace-opentelemetry/README.md | 12 +++++++++--- fastrace-opentelemetry/src/lib.rs | 5 +++-- fastrace-opentelemetry/tests/context.rs | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/fastrace-opentelemetry/CHANGELOG.md b/fastrace-opentelemetry/CHANGELOG.md index c706e7a5..28610c6f 100644 --- a/fastrace-opentelemetry/CHANGELOG.md +++ b/fastrace-opentelemetry/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -* Add helpers to extract the current fastrace `SpanContext` and convert it as the current OpenTelemetry `Context`. +* Add a bridge to extract the current fastrace `SpanContext` and convert it as the current OpenTelemetry `Context`. ## v0.15.0 diff --git a/fastrace-opentelemetry/README.md b/fastrace-opentelemetry/README.md index 9ad8fe3b..cdee6a30 100644 --- a/fastrace-opentelemetry/README.md +++ b/fastrace-opentelemetry/README.md @@ -82,12 +82,18 @@ This requires a local parent to be set for the current thread (e.g. via [`Span::set_local_parent`](https://docs.rs/fastrace/latest/fastrace/struct.Span.html#method.set_local_parent)). ```rust +use fastrace::prelude::*; use fastrace_opentelemetry::current_opentelemetry_context; use opentelemetry::trace::TraceContextExt; use opentelemetry::Context; -let _otel_guard = current_opentelemetry_context() - .map(|sc| Context::current().with_remote_span_context(sc).attach()); +fn main() { + let span = Span::root("root", SpanContext::random()); + let _guard = span.set_local_parent(); + + let _otel_guard = current_opentelemetry_context() + .map(|cx| Context::current().with_remote_span_context(cx).attach()); -// Call library code that uses `Context::current()`. + // Call library code that uses `Context::current()`. +} ``` diff --git a/fastrace-opentelemetry/src/lib.rs b/fastrace-opentelemetry/src/lib.rs index 622c6883..31eb2e14 100644 --- a/fastrace-opentelemetry/src/lib.rs +++ b/fastrace-opentelemetry/src/lib.rs @@ -87,7 +87,8 @@ pub struct OpenTelemetryReporter { /// /// The returned span context is **non-recording** (it does not create an OpenTelemetry span on /// its own). To make it usable as a parent for OpenTelemetry spans, attach it to an -/// OpenTelemetry [`Context`] via [`TraceContextExt::with_remote_span_context`]. +/// OpenTelemetry [`Context`](opentelemetry::Context) via +/// [`TraceContextExt::with_remote_span_context`](opentelemetry::trace::TraceContextExt::with_remote_span_context). /// /// # Examples /// @@ -101,7 +102,7 @@ pub struct OpenTelemetryReporter { /// /// // Make the fastrace span the "current" OpenTelemetry parent for this thread. /// let _otel_guard = fastrace_opentelemetry::current_opentelemetry_context() -/// .map(|sc| Context::current().with_remote_span_context(sc).attach()); +/// .map(|cx| Context::current().with_remote_span_context(cx).attach()); /// /// // Any OpenTelemetry instrumentation that reads `Context::current()` can now /// // treat the fastrace span as its parent. diff --git a/fastrace-opentelemetry/tests/context.rs b/fastrace-opentelemetry/tests/context.rs index 3e4e8e07..4ddb5666 100644 --- a/fastrace-opentelemetry/tests/context.rs +++ b/fastrace-opentelemetry/tests/context.rs @@ -52,7 +52,7 @@ fn otel_span_can_be_parented_by_fastrace_local_parent() { let _g = root.set_local_parent(); let _otel_guard = fastrace_opentelemetry::current_opentelemetry_context() - .map(|sc| Context::current().with_remote_span_context(sc).attach()); + .map(|cx| Context::current().with_remote_span_context(cx).attach()); let mut span = tracer.start("otel-child"); span.end();