Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions fastrace-opentelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* Add a bridge 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.
Expand Down
1 change: 1 addition & 0 deletions fastrace-opentelemetry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ opentelemetry_sdk = { workspace = true }
pollster = { version = "0.4.0" }

[dev-dependencies]
fastrace = { workspace = true, features = ["enable"] }
opentelemetry-otlp = { workspace = true }
44 changes: 34 additions & 10 deletions fastrace-opentelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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")])
Expand All @@ -71,5 +69,31 @@ 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::prelude::*;
use fastrace_opentelemetry::current_opentelemetry_context;
use opentelemetry::trace::TraceContextExt;
use opentelemetry::Context;

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()`.
}
```
54 changes: 52 additions & 2 deletions fastrace-opentelemetry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,6 +75,56 @@ 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`](opentelemetry::Context) via
/// [`TraceContextExt::with_remote_span_context`](opentelemetry::trace::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(|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.
/// ```
pub fn current_opentelemetry_context() -> Option<OtelSpanContext> {
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";
Expand Down Expand Up @@ -173,7 +223,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(),
Expand Down
75 changes: 75 additions & 0 deletions fastrace-opentelemetry/tests/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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_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<Mutex<Vec<SpanData>>>,
}

impl CapturingExporter {
fn new() -> (Self, Arc<Mutex<Vec<SpanData>>>) {
let spans = Arc::new(Mutex::new(Vec::new()));
(
Self {
spans: Arc::clone(&spans),
},
spans,
)
}
}

impl SpanExporter for CapturingExporter {
fn export(
&self,
batch: Vec<SpanData>,
) -> impl std::future::Future<Output = OTelSdkResult> + 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(|cx| Context::current().with_remote_span_context(cx).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()
);
}
19 changes: 10 additions & 9 deletions fastrace/src/collector/global_collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

Expand All @@ -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()
Expand Down Expand Up @@ -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);
}
Expand Down