diff --git a/Cargo.lock b/Cargo.lock
index 5640f5d7..655d6a95 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2447,6 +2447,45 @@ dependencies = [
"vcpkg",
]
+[[package]]
+name = "opentelemetry"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "js-sys",
+ "pin-project-lite",
+ "thiserror 2.0.12",
+ "tracing",
+]
+
+[[package]]
+name = "opentelemetry-semantic-conventions"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b29a9f89f1a954936d5aa92f19b2feec3c8f3971d3e96206640db7f9706ae3"
+
+[[package]]
+name = "opentelemetry_sdk"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afdefb21d1d47394abc1ba6c57363ab141be19e27cc70d0e422b7f303e4d290b"
+dependencies = [
+ "futures-channel",
+ "futures-executor",
+ "futures-util",
+ "glob",
+ "opentelemetry",
+ "percent-encoding",
+ "rand 0.9.0",
+ "serde_json",
+ "thiserror 2.0.12",
+ "tokio",
+ "tokio-stream",
+]
+
[[package]]
name = "os_info"
version = "3.10.0"
@@ -3141,6 +3180,7 @@ dependencies = [
"sentry-core",
"sentry-debug-images",
"sentry-log",
+ "sentry-opentelemetry",
"sentry-panic",
"sentry-slog",
"sentry-tower",
@@ -3241,6 +3281,17 @@ dependencies = [
"sentry-core",
]
+[[package]]
+name = "sentry-opentelemetry"
+version = "0.37.0"
+dependencies = [
+ "opentelemetry",
+ "opentelemetry-semantic-conventions",
+ "opentelemetry_sdk",
+ "sentry",
+ "sentry-core",
+]
+
[[package]]
name = "sentry-panic"
version = "0.37.0"
diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs
index 740cd0ba..43ccd6a4 100644
--- a/sentry-core/src/performance.rs
+++ b/sentry-core/src/performance.rs
@@ -2,6 +2,9 @@ use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Mutex, MutexGuard};
+use std::time::SystemTime;
+
+use sentry_types::protocol::v7::SpanId;
use crate::{protocol, Hub};
@@ -31,6 +34,23 @@ pub fn start_transaction(ctx: TransactionContext) -> Transaction {
}
}
+/// Start a new Performance Monitoring Transaction with the provided start timestamp.
+///
+/// The transaction needs to be explicitly finished via [`Transaction::finish`],
+/// otherwise it will be discarded.
+/// The transaction itself also represents the root span in the span hierarchy.
+/// Child spans can be started with the [`Transaction::start_child`] method.
+pub fn start_transaction_with_timestamp(
+ ctx: TransactionContext,
+ timestamp: SystemTime,
+) -> Transaction {
+ let transaction = start_transaction(ctx);
+ if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
+ tx.start_timestamp = timestamp;
+ }
+ transaction
+}
+
// Hub API:
impl Hub {
@@ -47,6 +67,21 @@ impl Hub {
Transaction::new_noop(ctx)
}
}
+
+ /// Start a new Performance Monitoring Transaction with the provided start timestamp.
+ ///
+ /// See the global [`start_transaction_with_timestamp`] for more documentation.
+ pub fn start_transaction_with_timestamp(
+ &self,
+ ctx: TransactionContext,
+ timestamp: SystemTime,
+ ) -> Transaction {
+ let transaction = start_transaction(ctx);
+ if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
+ tx.start_timestamp = timestamp;
+ }
+ transaction
+ }
}
// "Context" Types:
@@ -110,6 +145,29 @@ impl TransactionContext {
}
}
+ /// Creates a new Transaction Context with the given `name`, `op`, `trace_id`, and
+ /// possibly the given `span_id` and `parent_span_id`.
+ ///
+ /// See
+ /// for an explanation of a Transaction's `name`, and
+ /// for conventions
+ /// around an `operation`'s value.
+ #[must_use = "this must be used with `start_transaction`"]
+ pub fn new_with_details(
+ name: &str,
+ op: &str,
+ trace_id: protocol::TraceId,
+ span_id: Option,
+ parent_span_id: Option,
+ ) -> Self {
+ let mut slf = Self::new_with_trace_id(name, op, trace_id);
+ if let Some(span_id) = span_id {
+ slf.span_id = span_id;
+ }
+ slf.parent_span_id = parent_span_id;
+ slf
+ }
+
/// Creates a new Transaction Context based on the distributed tracing `headers`.
///
/// The `headers` in particular need to include either the `sentry-trace` or W3C
@@ -121,23 +179,23 @@ impl TransactionContext {
op: &str,
headers: I,
) -> Self {
- let mut trace = None;
- for (k, v) in headers.into_iter() {
- if k.eq_ignore_ascii_case("sentry-trace") {
- trace = parse_sentry_trace(v);
- break;
- }
-
- if k.eq_ignore_ascii_case("traceparent") {
- trace = parse_w3c_traceparent(v);
- }
- }
-
- let (trace_id, parent_span_id, sampled) = match trace {
- Some(trace) => (trace.0, Some(trace.1), trace.2),
- None => (protocol::TraceId::default(), None, None),
- };
-
+ parse_headers(headers)
+ .map(|sentry_trace| Self::continue_from_sentry_trace(name, op, &sentry_trace))
+ .unwrap_or_else(|| Self {
+ name: name.into(),
+ op: op.into(),
+ trace_id: Default::default(),
+ parent_span_id: None,
+ span_id: Default::default(),
+ sampled: None,
+ custom: None,
+ })
+ }
+
+ /// Creates a new Transaction Context based on the provided distributed tracing data.
+ pub fn continue_from_sentry_trace(name: &str, op: &str, sentry_trace: &SentryTrace) -> Self {
+ let (trace_id, parent_span_id, sampled) =
+ (sentry_trace.0, Some(sentry_trace.1), sentry_trace.2);
Self {
name: name.into(),
op: op.into(),
@@ -440,6 +498,28 @@ impl TransactionOrSpan {
}
}
+ /// Starts a new child Span with the given `op`, `description` and `id`.
+ ///
+ /// The span must be explicitly finished via [`Span::finish`], as it will
+ /// otherwise not be sent to Sentry.
+ #[must_use = "a span must be explicitly closed via `finish()`"]
+ pub fn start_child_with_details(
+ &self,
+ op: &str,
+ description: &str,
+ id: SpanId,
+ timestamp: SystemTime,
+ ) -> Span {
+ match self {
+ TransactionOrSpan::Transaction(transaction) => {
+ transaction.start_child_with_details(op, description, id, timestamp)
+ }
+ TransactionOrSpan::Span(span) => {
+ span.start_child_with_details(op, description, id, timestamp)
+ }
+ }
+ }
+
#[cfg(feature = "client")]
pub(crate) fn apply_to_event(&self, event: &mut protocol::Event<'_>) {
if event.contexts.contains_key("trace") {
@@ -462,10 +542,23 @@ impl TransactionOrSpan {
event.contexts.insert("trace".into(), context.into());
}
- /// Finishes the Transaction/Span.
+ /// Finishes the Transaction/Span with the provided end timestamp.
///
/// This records the end timestamp and either sends the inner [`Transaction`]
/// directly to Sentry, or adds the [`Span`] to its transaction.
+ pub fn finish_with_timestamp(self, timestamp: SystemTime) {
+ match self {
+ TransactionOrSpan::Transaction(transaction) => {
+ transaction.finish_with_timestamp(timestamp)
+ }
+ TransactionOrSpan::Span(span) => span.finish_with_timestamp(timestamp),
+ }
+ }
+
+ /// Finishes the Transaction/Span.
+ ///
+ /// This records the current timestamp as the end timestamp and either sends the inner [`Transaction`]
+ /// directly to Sentry, or adds the [`Span`] to its transaction.
pub fn finish(self) {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.finish(),
@@ -693,11 +786,11 @@ impl Transaction {
self.inner.lock().unwrap().sampled
}
- /// Finishes the Transaction.
+ /// Finishes the Transaction with the provided end timestamp.
///
/// This records the end timestamp and sends the transaction together with
/// all finished child spans to Sentry.
- pub fn finish(self) {
+ pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
with_client_impl! {{
let mut inner = self.inner.lock().unwrap();
@@ -708,7 +801,7 @@ impl Transaction {
if let Some(mut transaction) = inner.transaction.take() {
if let Some(client) = inner.client.take() {
- transaction.finish();
+ transaction.finish_with_timestamp(_timestamp);
transaction
.contexts
.insert("trace".into(), inner.context.clone().into());
@@ -731,6 +824,14 @@ impl Transaction {
}}
}
+ /// Finishes the Transaction.
+ ///
+ /// This records the current timestamp as the end timestamp and sends the transaction together with
+ /// all finished child spans to Sentry.
+ pub fn finish(self) {
+ self.finish_with_timestamp(SystemTime::now());
+ }
+
/// Starts a new child Span with the given `op` and `description`.
///
/// The span must be explicitly finished via [`Span::finish`].
@@ -754,6 +855,38 @@ impl Transaction {
span: Arc::new(Mutex::new(span)),
}
}
+
+ /// Starts a new child Span with the given `op` and `description`.
+ ///
+ /// The span must be explicitly finished via [`Span::finish`].
+ #[must_use = "a span must be explicitly closed via `finish()`"]
+ pub fn start_child_with_details(
+ &self,
+ op: &str,
+ description: &str,
+ id: SpanId,
+ timestamp: SystemTime,
+ ) -> Span {
+ let inner = self.inner.lock().unwrap();
+ let span = protocol::Span {
+ trace_id: inner.context.trace_id,
+ parent_span_id: Some(inner.context.span_id),
+ op: Some(op.into()),
+ description: if description.is_empty() {
+ None
+ } else {
+ Some(description.into())
+ },
+ span_id: id,
+ start_timestamp: timestamp,
+ ..Default::default()
+ };
+ Span {
+ transaction: Arc::clone(&self.inner),
+ sampled: inner.sampled,
+ span: Arc::new(Mutex::new(span)),
+ }
+ }
}
/// A smart pointer to a span's [`data` field](protocol::Span::data).
@@ -901,18 +1034,18 @@ impl Span {
self.sampled
}
- /// Finishes the Span.
+ /// Finishes the Span with the provided end timestamp.
///
/// This will record the end timestamp and add the span to the transaction
/// in which it was started.
- pub fn finish(self) {
+ pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
with_client_impl! {{
let mut span = self.span.lock().unwrap();
if span.timestamp.is_some() {
// the span was already finished
return;
}
- span.finish();
+ span.finish_with_timestamp(_timestamp);
let mut inner = self.transaction.lock().unwrap();
if let Some(transaction) = inner.transaction.as_mut() {
if transaction.spans.len() <= MAX_SPANS {
@@ -922,6 +1055,14 @@ impl Span {
}}
}
+ /// Finishes the Span.
+ ///
+ /// This will record the current timestamp as the end timestamp and add the span to the
+ /// transaction in which it was started.
+ pub fn finish(self) {
+ self.finish_with_timestamp(SystemTime::now());
+ }
+
/// Starts a new child Span with the given `op` and `description`.
///
/// The span must be explicitly finished via [`Span::finish`].
@@ -945,6 +1086,38 @@ impl Span {
span: Arc::new(Mutex::new(span)),
}
}
+
+ /// Starts a new child Span with the given `op` and `description`.
+ ///
+ /// The span must be explicitly finished via [`Span::finish`].
+ #[must_use = "a span must be explicitly closed via `finish()`"]
+ fn start_child_with_details(
+ &self,
+ op: &str,
+ description: &str,
+ id: SpanId,
+ timestamp: SystemTime,
+ ) -> Span {
+ let span = self.span.lock().unwrap();
+ let span = protocol::Span {
+ trace_id: span.trace_id,
+ parent_span_id: Some(span.span_id),
+ op: Some(op.into()),
+ description: if description.is_empty() {
+ None
+ } else {
+ Some(description.into())
+ },
+ span_id: id,
+ start_timestamp: timestamp,
+ ..Default::default()
+ };
+ Span {
+ transaction: self.transaction.clone(),
+ sampled: self.sampled,
+ span: Arc::new(Mutex::new(span)),
+ }
+ }
}
/// An Iterator over HTTP header names and values needed for distributed tracing.
@@ -963,8 +1136,21 @@ impl Iterator for TraceHeadersIter {
}
}
+/// A container for distributed tracing metadata that can be extracted from e.g. HTTP headers such as
+/// `sentry-trace` and `traceparent`.
#[derive(Debug, PartialEq)]
-struct SentryTrace(protocol::TraceId, protocol::SpanId, Option);
+pub struct SentryTrace(protocol::TraceId, protocol::SpanId, Option);
+
+impl SentryTrace {
+ /// Creates a new [`SentryTrace`] from the provided parameters
+ pub fn new(
+ trace_id: protocol::TraceId,
+ span_id: protocol::SpanId,
+ sampled: Option,
+ ) -> Self {
+ SentryTrace(trace_id, span_id, sampled)
+ }
+}
fn parse_sentry_trace(header: &str) -> Option {
let header = header.trim();
@@ -998,6 +1184,25 @@ fn parse_w3c_traceparent(header: &str) -> Option {
Some(SentryTrace(trace_id, parent_span_id, parent_sampled))
}
+/// Extracts distributed tracing metadata from headers (or, generally, key-value pairs),
+/// considering the values for both `sentry-trace` (prioritized) and `traceparent`.
+pub fn parse_headers<'a, I: IntoIterator- >(
+ headers: I,
+) -> Option {
+ let mut trace = None;
+ for (k, v) in headers.into_iter() {
+ if k.eq_ignore_ascii_case("sentry-trace") {
+ trace = parse_sentry_trace(v);
+ break;
+ }
+
+ if k.eq_ignore_ascii_case("traceparent") {
+ trace = parse_w3c_traceparent(v);
+ }
+ }
+ trace
+}
+
impl std::fmt::Display for SentryTrace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}-{}", self.0, self.1)?;
diff --git a/sentry-opentelemetry/Cargo.toml b/sentry-opentelemetry/Cargo.toml
new file mode 100644
index 00000000..9f889dc2
--- /dev/null
+++ b/sentry-opentelemetry/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "sentry-opentelemetry"
+version = "0.37.0"
+authors = ["Sentry "]
+license = "MIT"
+readme = "README.md"
+repository = "https://github.com/getsentry/sentry-rust"
+homepage = "https://sentry.io/welcome/"
+description = """
+Sentry integration for OpenTelemetry.
+"""
+edition = "2021"
+rust-version = "1.81"
+
+[package.metadata.docs.rs]
+all-features = true
+
+[dependencies]
+sentry-core = { version = "0.37.0", path = "../sentry-core", features = [
+ "client",
+ "release-health"
+] }
+opentelemetry = { version = "0.29.0", default-features = false }
+opentelemetry_sdk = { version = "0.29.0", default-features = false, features = [
+ "trace",
+] }
+opentelemetry-semantic-conventions = "0.29.0"
+
+[dev-dependencies]
+sentry = { version = "0.37.0", path = "../sentry", features = ["test"] }
+sentry-core = { version = "0.37.0", path = "../sentry-core", features = [
+ "test",
+] }
+opentelemetry_sdk = { version = "0.29.0", default-features = false, features = [
+ "trace",
+ "testing",
+] }
diff --git a/sentry-opentelemetry/src/converters.rs b/sentry-opentelemetry/src/converters.rs
new file mode 100644
index 00000000..0f664514
--- /dev/null
+++ b/sentry-opentelemetry/src/converters.rs
@@ -0,0 +1,56 @@
+use sentry_core::protocol::{value::Number, SpanId, SpanStatus, TraceId, Value};
+
+pub(crate) fn convert_span_id(span_id: &opentelemetry::SpanId) -> SpanId {
+ span_id.to_bytes().into()
+}
+
+pub(crate) fn convert_trace_id(trace_id: &opentelemetry::TraceId) -> TraceId {
+ trace_id.to_bytes().into()
+}
+
+pub(crate) fn convert_span_status(status: &opentelemetry::trace::Status) -> SpanStatus {
+ match status {
+ opentelemetry::trace::Status::Unset | opentelemetry::trace::Status::Ok => SpanStatus::Ok,
+ opentelemetry::trace::Status::Error { description } => {
+ description.parse().unwrap_or(SpanStatus::UnknownError)
+ }
+ }
+}
+
+pub(crate) fn convert_span_kind(kind: opentelemetry::trace::SpanKind) -> Value {
+ format!("{:?}", kind).to_lowercase().into()
+}
+
+pub(crate) fn convert_value(value: opentelemetry::Value) -> Value {
+ match value {
+ opentelemetry::Value::Bool(x) => Value::Bool(x),
+ opentelemetry::Value::I64(x) => Value::Number(x.into()),
+ opentelemetry::Value::F64(x) => Number::from_f64(x)
+ .map(Value::Number)
+ .unwrap_or(Value::Null),
+ opentelemetry::Value::String(x) => Value::String(x.into()),
+ opentelemetry::Value::Array(arr) => match arr {
+ opentelemetry::Array::Bool(items) => {
+ Value::Array(items.iter().map(|x| Value::Bool(*x)).collect())
+ }
+ opentelemetry::Array::I64(items) => Value::Array(
+ items
+ .iter()
+ .map(|x| Value::Number(Number::from(*x)))
+ .collect(),
+ ),
+ opentelemetry::Array::F64(items) => Value::Array(
+ items
+ .iter()
+ .filter_map(|x| Number::from_f64(*x))
+ .map(Value::Number)
+ .collect(),
+ ),
+ opentelemetry::Array::String(items) => {
+ Value::Array(items.iter().map(|x| x.as_str().into()).collect())
+ }
+ _ => Value::Null, // non-exhaustive
+ },
+ _ => Value::Null, // non-exhaustive
+ }
+}
diff --git a/sentry-opentelemetry/src/lib.rs b/sentry-opentelemetry/src/lib.rs
new file mode 100644
index 00000000..d9de713b
--- /dev/null
+++ b/sentry-opentelemetry/src/lib.rs
@@ -0,0 +1,51 @@
+//! OpenTelemetry support for Sentry.
+//!
+//! This integration allows you to capture spans from your existing OpenTelemetry setup and send
+//! them to Sentry, with support for distributed tracing.
+//! It's assumed that only the [OpenTelemetry tracing
+//! API](https://opentelemetry.io/docs/specs/otel/trace/api/) is used to start/end/modify Spans.
+//! Mixing it with the Sentry tracing API (e.g. `sentry_core::start_transaction(ctx)`) will not
+//! work, as the spans created with the two methods will not be nested properly.
+//! Capturing events (either manually with e.g. `sentry::capture_event`, or automatically with e.g. the
+//! `sentry-panic` integration) will send them to Sentry with the correct trace and span
+//! association.
+//!
+//! # Configuration
+//!
+//! Initialize Sentry, then register the [`SentryPropagator`] and the [`SentrySpanProcessor`]:
+//!
+//! ```
+//! use opentelemetry::{global};
+//! use opentelemetry_sdk::{
+//! propagation::TraceContextPropagator, trace::SdkTracerProvider,
+//! };
+//!
+//! // Initialize the Sentry SDK
+//! let _guard = sentry::init(sentry::ClientOptions {
+//! // Enable capturing of traces; set this a to lower value in production.
+//! // For more sophisticated behavior use a custom
+//! // [`sentry::ClientOptions::traces_sampler`] instead.
+//! // That's the equivalent of a tail sampling processor in OpenTelemetry.
+//! // These options will only affect sampling of the spans that are sent to Sentry,
+//! // not of the underlying OpenTelemetry spans.
+//! traces_sample_rate: 1.0,
+//! ..sentry::ClientOptions::default()
+//! });
+//!
+//! // Register the Sentry propagator to enable distributed tracing
+//! global::set_text_map_propagator(sentry_opentelemetry::SentryPropagator::new());
+//!
+//! let tracer_provider = SdkTracerProvider::builder()
+//! // Register the Sentry span processor to send OpenTelemetry spans to Sentry
+//! .with_span_processor(sentry_opentelemetry::SentrySpanProcessor::new())
+//! .build();
+//!
+//! global::set_tracer_provider(tracer_provider);
+//! ```
+
+mod converters;
+mod processor;
+mod propagator;
+
+pub use processor::*;
+pub use propagator::*;
diff --git a/sentry-opentelemetry/src/processor.rs b/sentry-opentelemetry/src/processor.rs
new file mode 100644
index 00000000..d1a27338
--- /dev/null
+++ b/sentry-opentelemetry/src/processor.rs
@@ -0,0 +1,187 @@
+//! An OpenTelemetry [SpanProcessor](https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-processor) for Sentry.
+//!
+//! [`SentrySpanProcessor`] allows the Sentry Rust SDK to integrate with OpenTelemetry.
+//! It transforms OpenTelemetry spans into Sentry transactions/spans and sends them to Sentry.
+//!
+//! # Configuration
+//!
+//! Unless you have no need for distributed tracing, this should be used together with [`crate::propagator::SentryPropagator`]. An example of
+//! setting up both is provided in the [crate-level documentation](../).
+
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+use std::time::SystemTime;
+
+use opentelemetry::global::ObjectSafeSpan;
+use opentelemetry::trace::{get_active_span, SpanId};
+use opentelemetry::Context;
+use opentelemetry_sdk::error::OTelSdkResult;
+use opentelemetry_sdk::trace::{Span, SpanData, SpanProcessor};
+
+use opentelemetry_sdk::Resource;
+use sentry_core::SentryTrace;
+use sentry_core::{TransactionContext, TransactionOrSpan};
+
+use crate::converters::{
+ convert_span_id, convert_span_kind, convert_span_status, convert_trace_id, convert_value,
+};
+
+/// A mapping from Sentry span IDs to Sentry spans/transactions.
+/// Sentry spans are created with the same SpanId as the corresponding OTEL span, so this is used
+/// to track OTEL spans across start/end calls.
+type SpanMap = Arc>>;
+
+/// An OpenTelemetry SpanProcessor that converts OTEL spans to Sentry spans/transactions and sends
+/// them to Sentry.
+#[derive(Debug, Clone)]
+pub struct SentrySpanProcessor {
+ span_map: SpanMap,
+}
+
+impl SentrySpanProcessor {
+ /// Creates a new `SentrySpanProcessor`.
+ pub fn new() -> Self {
+ sentry_core::configure_scope(|scope| {
+ // Associate Sentry events with the correct span and trace.
+ // This works as long as all Sentry spans/transactions are managed exclusively through OTEL APIs.
+ scope.add_event_processor(|mut event| {
+ get_active_span(|otel_span| {
+ let (span_id, trace_id) = (
+ convert_span_id(&otel_span.span_context().span_id()),
+ convert_trace_id(&otel_span.span_context().trace_id()),
+ );
+
+ if let Some(sentry_core::protocol::Context::Trace(trace_context)) =
+ event.contexts.get_mut("trace")
+ {
+ trace_context.trace_id = trace_id;
+ trace_context.span_id = span_id;
+ } else {
+ event.contexts.insert(
+ "trace".into(),
+ sentry_core::protocol::TraceContext {
+ span_id,
+ trace_id,
+ ..Default::default()
+ }
+ .into(),
+ );
+ }
+ });
+ Some(event)
+ });
+ });
+ Self {
+ span_map: Default::default(),
+ }
+ }
+}
+
+impl Default for SentrySpanProcessor {
+ /// Creates a default `SentrySpanProcessor`.
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl SpanProcessor for SentrySpanProcessor {
+ fn on_start(&self, span: &mut Span, ctx: &Context) {
+ let span_id = span.span_context().span_id();
+ let trace_id = span.span_context().trace_id();
+
+ let mut span_map = self.span_map.lock().unwrap();
+
+ let mut span_description = String::new();
+ let mut span_op = String::new();
+ let mut span_start_timestamp = SystemTime::now();
+ let mut parent_sentry_span = None;
+ if let Some(data) = span.exported_data() {
+ span_description = data.name.to_string();
+ span_op = span_description.clone(); // TODO: infer this from OTEL span attributes
+ span_start_timestamp = data.start_time;
+ if data.parent_span_id != SpanId::INVALID {
+ parent_sentry_span = span_map.get(&convert_span_id(&data.parent_span_id));
+ };
+ }
+ let span_description = span_description.as_str();
+ let span_op = span_op.as_str();
+
+ let sentry_span = {
+ if let Some(parent_sentry_span) = parent_sentry_span {
+ // continue local trace
+ TransactionOrSpan::Span(parent_sentry_span.start_child_with_details(
+ span_op,
+ span_description,
+ convert_span_id(&span_id),
+ span_start_timestamp,
+ ))
+ } else {
+ let sentry_ctx = {
+ if let Some(sentry_trace) = ctx.get::() {
+ // continue remote trace
+ TransactionContext::continue_from_sentry_trace(
+ span_description,
+ span_op,
+ sentry_trace,
+ )
+ } else {
+ // start a new trace
+ TransactionContext::new_with_details(
+ span_description,
+ span_op,
+ convert_trace_id(&trace_id),
+ Some(convert_span_id(&span_id)),
+ None,
+ )
+ }
+ };
+ let tx =
+ sentry_core::start_transaction_with_timestamp(sentry_ctx, span_start_timestamp);
+ TransactionOrSpan::Transaction(tx)
+ }
+ };
+ span_map.insert(convert_span_id(&span_id), sentry_span);
+ }
+
+ fn on_end(&self, data: SpanData) {
+ let span_id = data.span_context.span_id();
+
+ let mut span_map = self.span_map.lock().unwrap();
+
+ let Some(sentry_span) = span_map.remove(&convert_span_id(&span_id)) else {
+ return;
+ };
+
+ // TODO: read OTEL span events and convert them to Sentry breadcrumbs/events
+
+ sentry_span.set_data("otel.kind", convert_span_kind(data.span_kind));
+ for attribute in data.attributes {
+ sentry_span.set_data(attribute.key.as_str(), convert_value(attribute.value));
+ }
+ // TODO: read OTEL semantic convention span attributes and map them to the appropriate
+ // Sentry span attributes/context values
+ sentry_span.set_status(convert_span_status(&data.status));
+ sentry_span.finish_with_timestamp(data.end_time);
+ }
+
+ fn force_flush(&self) -> OTelSdkResult {
+ Ok(())
+ }
+
+ fn shutdown(&self) -> OTelSdkResult {
+ Ok(())
+ }
+
+ fn set_resource(&mut self, resource: &Resource) {
+ sentry_core::configure_scope(|scope| {
+ let otel_context = sentry_core::protocol::OtelContext {
+ resource: resource
+ .iter()
+ .map(|(key, value)| (key.as_str().into(), convert_value(value.clone())))
+ .collect(),
+ ..Default::default()
+ };
+ scope.set_context("otel", sentry_core::protocol::Context::from(otel_context));
+ });
+ }
+}
diff --git a/sentry-opentelemetry/src/propagator.rs b/sentry-opentelemetry/src/propagator.rs
new file mode 100644
index 00000000..396f95f7
--- /dev/null
+++ b/sentry-opentelemetry/src/propagator.rs
@@ -0,0 +1,82 @@
+//! An OpenTelemetry [Propagator](https://opentelemetry.io/docs/specs/otel/context/api-propagators/) for Sentry.
+//!
+//! [`SentryPropagator`] serves two purposes:
+//! - extracts incoming Sentry tracing metadata from incoming traces, and stores it in
+//! [`opentelemetry::baggage::Baggage`]. This information can then be used by
+//! [`crate::processor::SentrySpanProcessor`] to achieve distributed tracing.
+//! - injects Sentry tracing metadata in outgoing traces. This information can be used by
+//! downstream Sentry SDKs to achieve distributed tracing.
+//!
+//! # Configuration
+//!
+//! This should be used together with [`crate::processor::SentrySpanProcessor`]. An example of
+//! setting up both is provided in the [crate-level documentation](../).
+
+use std::sync::LazyLock;
+
+use opentelemetry::{
+ propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator},
+ trace::TraceContextExt,
+ Context, SpanId, TraceId,
+};
+use sentry_core::parse_headers;
+use sentry_core::SentryTrace;
+
+use crate::converters::{convert_span_id, convert_trace_id};
+
+const SENTRY_TRACE_KEY: &str = "sentry-trace";
+
+// list of headers used in the inject operation
+static SENTRY_PROPAGATOR_FIELDS: LazyLock<[String; 1]> =
+ LazyLock::new(|| [SENTRY_TRACE_KEY.to_owned()]);
+
+/// An OpenTelemetry Propagator that injects and extracts Sentry's tracing headers to achieve
+/// distributed tracing.
+#[derive(Debug, Copy, Clone)]
+pub struct SentryPropagator {}
+
+impl SentryPropagator {
+ /// Creates a new `SentryPropagator`
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+
+impl Default for SentryPropagator {
+ /// Creates a default `SentryPropagator`.
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl TextMapPropagator for SentryPropagator {
+ fn inject_context(&self, ctx: &Context, injector: &mut dyn Injector) {
+ let trace_id = ctx.span().span_context().trace_id();
+ let span_id = ctx.span().span_context().span_id();
+ let sampled = ctx.span().span_context().is_sampled();
+ if trace_id == TraceId::INVALID || span_id == SpanId::INVALID {
+ return;
+ }
+ let sentry_trace = SentryTrace::new(
+ convert_trace_id(&trace_id),
+ convert_span_id(&span_id),
+ Some(sampled),
+ );
+ injector.set(SENTRY_TRACE_KEY, sentry_trace.to_string());
+ }
+
+ fn extract_with_context(&self, ctx: &Context, extractor: &dyn Extractor) -> Context {
+ let keys = extractor.keys();
+ let pairs = keys
+ .iter()
+ .filter_map(|&key| extractor.get(key).map(|value| (key, value)));
+ if let Some(sentry_trace) = parse_headers(pairs) {
+ return ctx.with_value(sentry_trace);
+ }
+ ctx.clone()
+ }
+
+ fn fields(&self) -> FieldIter<'_> {
+ FieldIter::new(&*SENTRY_PROPAGATOR_FIELDS)
+ }
+}
diff --git a/sentry-opentelemetry/tests/associates_event_with_span.rs b/sentry-opentelemetry/tests/associates_event_with_span.rs
new file mode 100644
index 00000000..2e7b411f
--- /dev/null
+++ b/sentry-opentelemetry/tests/associates_event_with_span.rs
@@ -0,0 +1,83 @@
+mod shared;
+
+use opentelemetry::{
+ global,
+ trace::{Tracer, TracerProvider},
+};
+use opentelemetry_sdk::trace::SdkTracerProvider;
+use sentry_core::protocol::Transaction;
+use sentry_opentelemetry::{SentryPropagator, SentrySpanProcessor};
+
+#[test]
+fn test_associates_event_with_span() {
+ let transport = shared::init_sentry(1.0); // Sample all spans
+
+ // Set up OpenTelemetry
+ global::set_text_map_propagator(SentryPropagator::new());
+ let tracer_provider = SdkTracerProvider::builder()
+ .with_span_processor(SentrySpanProcessor::new())
+ .build();
+ let tracer = tracer_provider.tracer("test".to_string());
+
+ // Create root span and execute test within it
+ tracer.in_span("root_span", |_| {
+ // Create child span and execute within it
+ tracer.in_span("child_span", |_| {
+ // Capture an event while the child span is active
+ sentry::capture_message("Test message", sentry::Level::Error);
+ });
+ });
+
+ // Capture the event and spans
+ let envelopes = transport.fetch_and_clear_envelopes();
+
+ // Find event and transaction
+ let mut transaction: Option = None;
+ let mut span_id: Option = None;
+
+ let mut trace_id_from_event: Option = None;
+ let mut span_id_from_event: Option = None;
+
+ for envelope in &envelopes {
+ for item in envelope.items() {
+ match item {
+ sentry::protocol::EnvelopeItem::Event(event) => {
+ trace_id_from_event = event.contexts.get("trace").and_then(|c| match c {
+ sentry::protocol::Context::Trace(trace) => Some(trace.trace_id.to_string()),
+ _ => unreachable!(),
+ });
+ span_id_from_event = event.contexts.get("trace").and_then(|c| match c {
+ sentry::protocol::Context::Trace(trace) => Some(trace.span_id.to_string()),
+ _ => unreachable!(),
+ });
+ }
+ sentry::protocol::EnvelopeItem::Transaction(tx) => {
+ transaction = Some(tx.clone());
+ tx.spans.iter().for_each(|span| {
+ span_id = Some(span.span_id.to_string());
+ });
+ }
+ _ => (),
+ }
+ }
+ }
+
+ let transaction = transaction.expect("Should have a transaction");
+ let span_id = span_id.expect("Transaction should have a child span");
+
+ let trace_id_from_event = trace_id_from_event.expect("Event should have a trace ID");
+ let span_id_from_event = span_id_from_event.expect("Event should have a span ID");
+
+ // Verify that the transaction ID and span ID in the event match with the transaction and span
+ assert_eq!(
+ {
+ let context = transaction.contexts.get("trace").unwrap().clone();
+ match context {
+ sentry::protocol::Context::Trace(context) => context.trace_id.to_string(),
+ _ => unreachable!(),
+ }
+ },
+ trace_id_from_event
+ );
+ assert_eq!(span_id, span_id_from_event);
+}
diff --git a/sentry-opentelemetry/tests/captures_transaction.rs b/sentry-opentelemetry/tests/captures_transaction.rs
new file mode 100644
index 00000000..c8ca55d0
--- /dev/null
+++ b/sentry-opentelemetry/tests/captures_transaction.rs
@@ -0,0 +1,42 @@
+mod shared;
+
+use opentelemetry::{
+ global,
+ trace::{Tracer, TracerProvider},
+};
+use opentelemetry_sdk::trace::SdkTracerProvider;
+use sentry_opentelemetry::{SentryPropagator, SentrySpanProcessor};
+
+#[test]
+fn test_captures_transaction() {
+ // Initialize Sentry
+ let transport = shared::init_sentry(1.0); // Sample all spans
+
+ // Set up OpenTelemetry
+ global::set_text_map_propagator(SentryPropagator::new());
+ let tracer_provider = SdkTracerProvider::builder()
+ .with_span_processor(SentrySpanProcessor::new())
+ .build();
+ let tracer = tracer_provider.tracer("test".to_string());
+
+ // Create and end a root span
+ tracer.in_span("root_span", |_| {
+ // Span body is empty, just creating the span
+ });
+
+ // Check that data was sent to Sentry
+ let envelopes = transport.fetch_and_clear_envelopes();
+ assert_eq!(
+ envelopes.len(),
+ 1,
+ "Expected one transaction to be sent to Sentry"
+ );
+
+ let transaction = envelopes[0].items().next().unwrap();
+ match transaction {
+ sentry::protocol::EnvelopeItem::Transaction(tx) => {
+ assert_eq!(tx.name.as_deref(), Some("root_span"));
+ }
+ unexpected => panic!("Expected transaction, but got {:#?}", unexpected),
+ }
+}
diff --git a/sentry-opentelemetry/tests/captures_transaction_with_nested_spans.rs b/sentry-opentelemetry/tests/captures_transaction_with_nested_spans.rs
new file mode 100644
index 00000000..4b47efa6
--- /dev/null
+++ b/sentry-opentelemetry/tests/captures_transaction_with_nested_spans.rs
@@ -0,0 +1,72 @@
+mod shared;
+
+use opentelemetry::{
+ global,
+ trace::{Status, TraceContextExt, Tracer, TracerProvider},
+ KeyValue,
+};
+use opentelemetry_sdk::trace::SdkTracerProvider;
+use sentry_opentelemetry::{SentryPropagator, SentrySpanProcessor};
+
+#[test]
+fn test_captures_transaction_with_nested_spans() {
+ // Initialize Sentry
+ let transport = shared::init_sentry(1.0); // Sample all spans
+
+ // Set up OpenTelemetry
+ global::set_text_map_propagator(SentryPropagator::new());
+ let tracer_provider = SdkTracerProvider::builder()
+ .with_span_processor(SentrySpanProcessor::new())
+ .build();
+ let tracer = tracer_provider.tracer("test".to_string());
+
+ // Create nested spans using in_span
+ tracer.in_span("root_span", |_| {
+ tracer.in_span("child_span", |_| {
+ tracer.in_span("grandchild_span", |cx| {
+ // Add some attributes to the grandchild
+ cx.span()
+ .set_attribute(KeyValue::new("test.key", "test.value"));
+ cx.span().set_status(Status::Ok);
+ });
+ });
+ });
+
+ // Check that data was sent to Sentry
+ let envelopes = transport.fetch_and_clear_envelopes();
+ assert_eq!(
+ envelopes.len(),
+ 1,
+ "Expected one transaction to be sent to Sentry"
+ );
+
+ let transaction = envelopes[0].items().next().unwrap();
+ match transaction {
+ sentry::protocol::EnvelopeItem::Transaction(tx) => {
+ assert_eq!(tx.name.as_deref(), Some("root_span"));
+ assert_eq!(tx.spans.len(), 2); // Should have 2 child spans
+
+ let child_span = tx
+ .spans
+ .iter()
+ .find(|s| s.description.as_deref() == Some("child_span"))
+ .expect("Child span should exist");
+ let grandchild_span = tx
+ .spans
+ .iter()
+ .find(|s| s.description.as_deref() == Some("grandchild_span"))
+ .expect("Grandchild span should exist");
+
+ // Get transaction span ID from trace context
+ let tx_span_id = match &tx.contexts.get("trace") {
+ Some(sentry::protocol::Context::Trace(trace)) => trace.span_id,
+ _ => panic!("Missing trace context in transaction"),
+ };
+
+ // Check parent-child relationship
+ assert_eq!(grandchild_span.parent_span_id, Some(child_span.span_id));
+ assert_eq!(child_span.parent_span_id, Some(tx_span_id));
+ }
+ unexpected => panic!("Expected transaction, but got {:#?}", unexpected),
+ }
+}
diff --git a/sentry-opentelemetry/tests/creates_distributed_trace.rs b/sentry-opentelemetry/tests/creates_distributed_trace.rs
new file mode 100644
index 00000000..e57fb5b4
--- /dev/null
+++ b/sentry-opentelemetry/tests/creates_distributed_trace.rs
@@ -0,0 +1,121 @@
+mod shared;
+
+use opentelemetry::{
+ global,
+ propagation::TextMapPropagator,
+ trace::{TraceContextExt, Tracer, TracerProvider},
+ Context,
+};
+use opentelemetry_sdk::trace::SdkTracerProvider;
+use sentry_opentelemetry::{SentryPropagator, SentrySpanProcessor};
+use std::collections::HashMap;
+
+#[test]
+fn test_creates_distributed_trace() {
+ let transport = shared::init_sentry(1.0); // Sample all spans
+
+ // Set up OpenTelemetry
+ global::set_text_map_propagator(SentryPropagator::new());
+ let tracer_provider = SdkTracerProvider::builder()
+ .with_span_processor(SentrySpanProcessor::new())
+ .build();
+ let tracer = tracer_provider.tracer("test".to_string());
+
+ // We need to store the context to pass between services, so we'll use a mutable variable
+ let mut headers = HashMap::new();
+ let propagator = SentryPropagator::new();
+
+ // Create a "first service" span and store context in headers
+ tracer.in_span("first_service", |first_service_ctx| {
+ // Simulate passing the context to another service by extracting and injecting e.g. HTTP headers
+ propagator.inject_context(&first_service_ctx, &mut TestInjector(&mut headers));
+ });
+
+ // Now simulate the second service receiving the headers and continuing the trace
+ let second_service_ctx =
+ propagator.extract_with_context(&Context::current(), &TestExtractor(&headers));
+
+ // Create a second service span that continues the trace
+ // We need to use start_with_context here to connect with the previous context
+ let second_service_span = tracer.start_with_context("second_service", &second_service_ctx);
+ let second_service_ctx = second_service_ctx.with_span(second_service_span);
+
+ // End the second service span
+ second_service_ctx.span().end();
+
+ // Get both transactions at once
+ let envelopes = transport.fetch_and_clear_envelopes();
+ assert_eq!(
+ envelopes.len(),
+ 2,
+ "Expected two transactions to be sent to Sentry"
+ );
+
+ // Find transactions for first and second services
+ let mut first_tx = None;
+ let mut second_tx = None;
+
+ for envelope in &envelopes {
+ let tx = match envelope.items().next().unwrap() {
+ sentry::protocol::EnvelopeItem::Transaction(tx) => tx.clone(),
+ unexpected => panic!("Expected transaction, but got {:#?}", unexpected),
+ };
+
+ // Determine which service this transaction belongs to based on name
+ match tx.name.as_deref() {
+ Some("first_service") => first_tx = Some(tx),
+ Some("second_service") => second_tx = Some(tx),
+ name => panic!("Unexpected transaction name: {:?}", name),
+ }
+ }
+
+ let first_tx = first_tx.expect("Missing first service transaction");
+ let second_tx = second_tx.expect("Missing second service transaction");
+
+ // Get first service trace ID and span ID
+ let (first_trace_id, first_span_id) = match &first_tx.contexts.get("trace") {
+ Some(sentry::protocol::Context::Trace(trace)) => (trace.trace_id, trace.span_id),
+ _ => panic!("Missing trace context in first transaction"),
+ };
+
+ // Get second service trace ID and span ID
+ let (second_trace_id, second_span_id, second_parent_span_id) =
+ match &second_tx.contexts.get("trace") {
+ Some(sentry::protocol::Context::Trace(trace)) => {
+ (trace.trace_id, trace.span_id, trace.parent_span_id)
+ }
+ _ => panic!("Missing trace context in second transaction"),
+ };
+
+ // Verify the distributed trace - same trace ID, different span IDs
+ assert_eq!(first_trace_id, second_trace_id, "Trace IDs should match");
+ assert_ne!(
+ first_span_id, second_span_id,
+ "Span IDs should be different"
+ );
+ assert_eq!(
+ second_parent_span_id,
+ Some(first_span_id),
+ "Second service's parent span ID should match first service's span ID"
+ );
+}
+
+struct TestInjector<'a>(&'a mut HashMap);
+
+impl opentelemetry::propagation::Injector for TestInjector<'_> {
+ fn set(&mut self, key: &str, value: String) {
+ self.0.insert(key.to_string(), value);
+ }
+}
+
+struct TestExtractor<'a>(&'a HashMap);
+
+impl opentelemetry::propagation::Extractor for TestExtractor<'_> {
+ fn get(&self, key: &str) -> Option<&str> {
+ self.0.get(key).map(|s| s.as_str())
+ }
+
+ fn keys(&self) -> Vec<&str> {
+ self.0.keys().map(|k| k.as_str()).collect()
+ }
+}
diff --git a/sentry-opentelemetry/tests/shared.rs b/sentry-opentelemetry/tests/shared.rs
new file mode 100644
index 00000000..5189da05
--- /dev/null
+++ b/sentry-opentelemetry/tests/shared.rs
@@ -0,0 +1,21 @@
+use sentry::{ClientOptions, Hub};
+use sentry_core::test::TestTransport;
+
+use std::sync::Arc;
+
+pub fn init_sentry(traces_sample_rate: f32) -> Arc {
+ let transport = TestTransport::new();
+ let options = ClientOptions {
+ dsn: Some(
+ "https://test@sentry-opentelemetry.com/test"
+ .parse()
+ .unwrap(),
+ ),
+ transport: Some(Arc::new(transport.clone())),
+ sample_rate: 1.0,
+ traces_sample_rate,
+ ..ClientOptions::default()
+ };
+ Hub::current().bind_client(Some(Arc::new(options.into())));
+ transport
+}
diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs
index badeaa0b..bb2008f6 100644
--- a/sentry-types/src/protocol/v7.rs
+++ b/sentry-types/src/protocol/v7.rs
@@ -1096,8 +1096,10 @@ pub enum Context {
Browser(Box),
/// Tracing data.
Trace(Box),
- /// GPU data
+ /// GPU data.
Gpu(Box),
+ /// OpenTelemetry data.
+ Otel(Box),
/// Generic other context data.
#[serde(rename = "unknown")]
Other(Map),
@@ -1114,6 +1116,7 @@ impl Context {
Context::Browser(..) => "browser",
Context::Trace(..) => "trace",
Context::Gpu(..) => "gpu",
+ Context::Otel(..) => "otel",
Context::Other(..) => "unknown",
}
}
@@ -1332,6 +1335,22 @@ pub struct GpuContext {
pub other: Map,
}
+/// OpenTelemetry context
+#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
+pub struct OtelContext {
+ /// OpenTelemetry [general
+ /// attributes](https://opentelemetry.io/docs/specs/semconv/general/attributes/).
+ #[serde(default, skip_serializing_if = "Map::is_empty")]
+ pub attributes: Map,
+ /// OpenTelemetry [resource attributes](https://opentelemetry.io/docs/specs/semconv/resource/),
+ /// describing the entity producing telemetry.
+ #[serde(default, skip_serializing_if = "Map::is_empty")]
+ pub resource: Map,
+ /// Additional arbitrary fields for forwards compatibility.
+ #[serde(flatten)]
+ pub other: Map,
+}
+
/// Holds the identifier for a Span
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[serde(try_from = "String", into = "String")]
@@ -1373,6 +1392,12 @@ impl TryFrom for SpanId {
}
}
+impl From<[u8; 8]> for SpanId {
+ fn from(value: [u8; 8]) -> Self {
+ Self(value)
+ }
+}
+
/// Holds the identifier for a Trace
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[serde(try_from = "String", into = "String")]
@@ -1414,6 +1439,12 @@ impl TryFrom for TraceId {
}
}
+impl From<[u8; 16]> for TraceId {
+ fn from(value: [u8; 16]) -> Self {
+ Self(value)
+ }
+}
+
/// Holds information about a tracing event.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
pub struct TraceContext {
@@ -1457,8 +1488,11 @@ into_context!(Runtime, RuntimeContext);
into_context!(Browser, BrowserContext);
into_context!(Trace, TraceContext);
into_context!(Gpu, GpuContext);
+into_context!(Otel, OtelContext);
-const INFERABLE_CONTEXTS: &[&str] = &["device", "os", "runtime", "app", "browser", "trace", "gpu"];
+const INFERABLE_CONTEXTS: &[&str] = &[
+ "device", "os", "runtime", "app", "browser", "trace", "gpu", "otel",
+];
struct ContextsVisitor;
@@ -1803,6 +1837,11 @@ impl Span {
Default::default()
}
+ /// Finalizes the span with the provided timestamp.
+ pub fn finish_with_timestamp(&mut self, timestamp: SystemTime) {
+ self.timestamp = Some(timestamp);
+ }
+
/// Finalizes the span.
pub fn finish(&mut self) {
self.timestamp = Some(SystemTime::now());
@@ -2050,6 +2089,11 @@ impl<'a> Transaction<'a> {
pub fn finish(&mut self) {
self.timestamp = Some(SystemTime::now());
}
+
+ /// Finalizes the transaction to be dispatched with the given end timestamp.
+ pub fn finish_with_timestamp(&mut self, timestamp: SystemTime) {
+ self.timestamp = Some(timestamp);
+ }
}
impl fmt::Display for Transaction<'_> {
diff --git a/sentry/Cargo.toml b/sentry/Cargo.toml
index 5d77a629..03a8f1bf 100644
--- a/sentry/Cargo.toml
+++ b/sentry/Cargo.toml
@@ -43,6 +43,7 @@ tower = ["sentry-tower"]
tower-http = ["tower", "sentry-tower/http"]
tower-axum-matched-path = ["tower-http", "sentry-tower/axum-matched-path"]
tracing = ["sentry-tracing"]
+opentelemetry = ["sentry-opentelemetry"]
# other features
test = ["sentry-core/test"]
debug-logs = ["dep:log", "sentry-core/debug-logs"]
@@ -70,6 +71,7 @@ sentry-panic = { version = "0.37.0", path = "../sentry-panic", optional = true }
sentry-slog = { version = "0.37.0", path = "../sentry-slog", optional = true }
sentry-tower = { version = "0.37.0", path = "../sentry-tower", optional = true }
sentry-tracing = { version = "0.37.0", path = "../sentry-tracing", optional = true }
+sentry-opentelemetry = { version = "0.37.0", path = "../sentry-opentelemetry", optional = true }
log = { version = "0.4.8", optional = true, features = ["std"] }
reqwest = { version = "0.12", optional = true, features = [
"blocking",
diff --git a/sentry/src/lib.rs b/sentry/src/lib.rs
index be98796f..352094d7 100644
--- a/sentry/src/lib.rs
+++ b/sentry/src/lib.rs
@@ -204,6 +204,10 @@ pub mod integrations {
#[cfg_attr(doc_cfg, doc(cfg(feature = "log")))]
#[doc(inline)]
pub use sentry_log as log;
+ #[cfg(feature = "opentelemetry")]
+ #[cfg_attr(doc_cfg, doc(cfg(feature = "opentelemetry")))]
+ #[doc(inline)]
+ pub use sentry_opentelemetry as opentelemetry;
#[cfg(feature = "panic")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "panic")))]
#[doc(inline)]