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 sdk/core/azure_core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Added `Tracer::start_span_with_options`, `Tracer::start_span_with_parent_and_options`, and `Span::end_at`, along with a `SpanOptions` struct, to allow reconstructing spans with explicit (backdated) start and end timestamps. These are additive with default implementations, so existing `Tracer`/`Span` implementations continue to work unchanged.

### Breaking Changes

### Bugs Fixed
Expand Down
4 changes: 2 additions & 2 deletions sdk/core/azure_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ pub mod tracing {
pub use crate::http::policies::PublicApiInstrumentationInformation;
pub use azure_core_macros::{client, function, new, subclient};
pub use typespec_client_core::tracing::{
AsAny, Attribute, AttributeArray, AttributeValue, Span, SpanGuard, SpanKind, SpanStatus,
Tracer, TracerProvider,
AsAny, Attribute, AttributeArray, AttributeValue, Span, SpanGuard, SpanKind, SpanOptions,
SpanStatus, Tracer, TracerProvider,
};
}

Expand Down
2 changes: 2 additions & 0 deletions sdk/core/azure_core_opentelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Implemented span backdating (`Tracer::start_span_with_options`, `Tracer::start_span_with_parent_and_options`, and `Span::end_at`) by mapping to the OpenTelemetry `SpanBuilder::with_start_time` and `Span::end_with_timestamp` APIs, so late-bound (tail-sampled) spans can be emitted with their original timestamps.

### Breaking Changes

### Bugs Fixed
Expand Down
101 changes: 99 additions & 2 deletions sdk/core/azure_core_opentelemetry/src/span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
use crate::attributes::AttributeValue as ConversionAttributeValue;
use azure_core::{
http::headers::{HeaderName, HeaderValue},
time::OffsetDateTime,
tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus},
};
use opentelemetry::{
propagation::{Injector, TextMapPropagator},
trace::TraceContextExt,
};
use opentelemetry_sdk::propagation::TraceContextPropagator;
use std::{error::Error as StdError, sync::Arc};
use std::{error::Error as StdError, sync::Arc, time::SystemTime};

/// newtype for Azure Core SpanKind to enable conversion to OpenTelemetry SpanKind
pub(crate) struct OpenTelemetrySpanKind(pub azure_core::tracing::SpanKind);
Expand Down Expand Up @@ -64,6 +65,12 @@ impl Span for OpenTelemetrySpan {
self.context.span().end();
}

fn end_at(&self, end_time: OffsetDateTime) {
self.context
.span()
.end_with_timestamp(SystemTime::from(end_time));
}

fn span_id(&self) -> [u8; 8] {
self.context.span().span_context().span_id().to_bytes()
}
Expand Down Expand Up @@ -150,12 +157,16 @@ impl Drop for OpenTelemetrySpanGuard {
mod tests {
use crate::telemetry::OpenTelemetryTracerProvider;
use azure_core::http::{Context as AzureContext, Url};
use azure_core::tracing::{Attribute, AttributeValue, SpanKind, SpanStatus, TracerProvider};
use azure_core::time::{Duration, OffsetDateTime};
use azure_core::tracing::{
Attribute, AttributeValue, SpanKind, SpanOptions, SpanStatus, TracerProvider,
};
use opentelemetry::trace::TraceContextExt;
use opentelemetry::{Context, Key, KeyValue, Value};
use opentelemetry_sdk::trace::{in_memory_exporter::InMemorySpanExporter, SdkTracerProvider};
use std::io::{Error, ErrorKind};
use std::sync::Arc;
use std::time::SystemTime;
use tracing::trace;

fn create_exportable_tracer_provider() -> (Arc<SdkTracerProvider>, InMemorySpanExporter) {
Expand Down Expand Up @@ -304,6 +315,92 @@ mod tests {
}
}

#[test]
fn test_open_telemetry_span_backdated() {
let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider();
let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider);
let tracer = tracer_provider.get_tracer(Some("Backdated"), "test", Some("0.1.0"));

// Choose timestamps clearly in the past so we can prove the span is *not*
// stamped at "now".
let now = OffsetDateTime::now_utc();
let past_start = now - Duration::seconds(3600);
let past_end = now - Duration::seconds(1800);

let span = tracer.start_span_with_options(
"backdated_span".into(),
SpanKind::Client,
vec![],
SpanOptions {
start_time: Some(past_start),
},
);
span.end_at(past_end);

let spans = otel_exporter.get_finished_spans().unwrap();
assert_eq!(spans.len(), 1);
let span = &spans[0];
assert_eq!(span.name, "backdated_span");
// The injected past timestamps must be preserved exactly, not replaced with "now".
assert_eq!(span.start_time, SystemTime::from(past_start));
assert_eq!(span.end_time, SystemTime::from(past_end));
assert!(span.start_time < span.end_time);
assert!(span.end_time < SystemTime::from(now));
}

#[test]
fn test_open_telemetry_span_backdated_with_parent() {
let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider();
let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider);
let tracer = tracer_provider.get_tracer(Some("Backdated"), "test", Some("0.1.0"));

// Reconstruct a completed operation: a backdated root with a backdated attempt child.
let now = OffsetDateTime::now_utc();
let root_start = now - Duration::seconds(600);
let child_start = now - Duration::seconds(590);
let child_end = now - Duration::seconds(585);
let root_end = now - Duration::seconds(580);

let root = tracer.start_span_with_options(
"operation".into(),
SpanKind::Client,
vec![],
SpanOptions {
start_time: Some(root_start),
},
);
let child = tracer.start_span_with_parent_and_options(
"attempt".into(),
SpanKind::Client,
vec![],
root.clone(),
SpanOptions {
start_time: Some(child_start),
},
);
child.end_at(child_end);
root.end_at(root_end);

let spans = otel_exporter.get_finished_spans().unwrap();
assert_eq!(spans.len(), 2);

let root_span = spans.iter().find(|s| s.name == "operation").unwrap();
let child_span = spans.iter().find(|s| s.name == "attempt").unwrap();

assert_eq!(root_span.start_time, SystemTime::from(root_start));
assert_eq!(root_span.end_time, SystemTime::from(root_end));
assert_eq!(child_span.start_time, SystemTime::from(child_start));
assert_eq!(child_span.end_time, SystemTime::from(child_end));
assert!(root_span.end_time < SystemTime::from(now));

// The child must be parented to the backdated root.
assert_ne!(
child_span.parent_span_id,
opentelemetry::trace::SpanId::INVALID
);
assert_eq!(child_span.parent_span_id.to_bytes(), root.span_id());
}

#[test]
fn test_open_telemetry_span_set_attribute() {
let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider();
Expand Down
105 changes: 71 additions & 34 deletions sdk/core/azure_core_opentelemetry/src/tracer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ use crate::{
span::{OpenTelemetrySpan, OpenTelemetrySpanKind},
};

use azure_core::tracing::{SpanKind, Tracer};
use azure_core::time::OffsetDateTime;
use azure_core::tracing::{SpanKind, SpanOptions, Tracer};
use opentelemetry::{
global::BoxedTracer,
trace::{TraceContextExt, Tracer as OpenTelemetryTracerTrait},
Context, KeyValue,
};
use std::{borrow::Cow, fmt::Debug, sync::Arc};
use std::{borrow::Cow, fmt::Debug, sync::Arc, time::SystemTime};

pub struct OpenTelemetryTracer {
namespace: Option<&'static str>,
Expand All @@ -27,6 +28,45 @@ impl OpenTelemetryTracer {
inner: tracer,
}
}

/// Builds a span within `context`, optionally backdated to `start_time`.
fn build_span(
&self,
name: Cow<'static, str>,
kind: SpanKind,
attributes: Vec<azure_core::tracing::Attribute>,
start_time: Option<OffsetDateTime>,
context: Context,
) -> Arc<dyn azure_core::tracing::Span> {
let mut span_builder = opentelemetry::trace::SpanBuilder::from_name(name)
.with_kind(OpenTelemetrySpanKind(kind).into())
.with_attributes(
attributes
.iter()
.map(|attr| KeyValue::from(OpenTelemetryAttribute(attr.clone()))),
);
if let Some(start_time) = start_time {
span_builder = span_builder.with_start_time(SystemTime::from(start_time));
}
let span = self.inner.build_with_context(span_builder, &context);

OpenTelemetrySpan::new(context.with_span(span))
}

/// Extracts the OpenTelemetry context from a parent span.
fn parent_context(parent: &Arc<dyn azure_core::tracing::Span>) -> Context {
parent
.as_any()
.downcast_ref::<OpenTelemetrySpan>()
.unwrap_or_else(|| {
panic!(
"Could not downcast parent span to OpenTelemetrySpan. Actual type: {}",
std::any::type_name::<dyn azure_core::tracing::Span>()
)
})
.context()
.clone()
}
}

impl Debug for OpenTelemetryTracer {
Expand All @@ -48,17 +88,23 @@ impl Tracer for OpenTelemetryTracer {
kind: SpanKind,
attributes: Vec<azure_core::tracing::Attribute>,
) -> Arc<dyn azure_core::tracing::Span> {
let span_builder = opentelemetry::trace::SpanBuilder::from_name(name)
.with_kind(OpenTelemetrySpanKind(kind).into())
.with_attributes(
attributes
.iter()
.map(|attr| KeyValue::from(OpenTelemetryAttribute(attr.clone()))),
);
let context = Context::current();
let span = self.inner.build_with_context(span_builder, &context);
self.build_span(name, kind, attributes, None, Context::current())
}

OpenTelemetrySpan::new(context.with_span(span))
fn start_span_with_options(
&self,
name: Cow<'static, str>,
kind: SpanKind,
attributes: Vec<azure_core::tracing::Attribute>,
options: SpanOptions,
) -> Arc<dyn azure_core::tracing::Span> {
self.build_span(
name,
kind,
attributes,
options.start_time,
Context::current(),
)
}

fn start_span_with_parent(
Expand All @@ -68,29 +114,20 @@ impl Tracer for OpenTelemetryTracer {
attributes: Vec<azure_core::tracing::Attribute>,
parent: Arc<dyn azure_core::tracing::Span>,
) -> Arc<dyn azure_core::tracing::Span> {
let span_builder = opentelemetry::trace::SpanBuilder::from_name(name)
.with_kind(OpenTelemetrySpanKind(kind).into())
.with_attributes(
attributes
.iter()
.map(|attr| KeyValue::from(OpenTelemetryAttribute(attr.clone()))),
);

// Cast the parent span to Any type
let context = parent
.as_any()
.downcast_ref::<OpenTelemetrySpan>()
.unwrap_or_else(|| {
panic!(
"Could not downcast parent span to OpenTelemetrySpan. Actual type: {}",
std::any::type_name::<dyn azure_core::tracing::Span>()
)
})
.context()
.clone();
let span = self.inner.build_with_context(span_builder, &context);
let context = Self::parent_context(&parent);
self.build_span(name, kind, attributes, None, context)
}

OpenTelemetrySpan::new(context.with_span(span))
fn start_span_with_parent_and_options(
&self,
name: Cow<'static, str>,
kind: SpanKind,
attributes: Vec<azure_core::tracing::Attribute>,
parent: Arc<dyn azure_core::tracing::Span>,
options: SpanOptions,
) -> Arc<dyn azure_core::tracing::Span> {
let context = Self::parent_context(&parent);
self.build_span(name, kind, attributes, options.start_time, context)
}
}

Expand Down
2 changes: 2 additions & 0 deletions sdk/core/typespec_client_core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Added `Tracer::start_span_with_options`, `Tracer::start_span_with_parent_and_options`, and `Span::end_at`, along with a `SpanOptions` struct, to allow reconstructing spans with explicit (backdated) start and end timestamps. These are additive with default implementations, so existing `Tracer`/`Span` implementations continue to work unchanged.

### Breaking Changes

### Bugs Fixed
Expand Down
Loading
Loading