From c54151454a1bd0a5cbf81a34755c72b39d1b15d1 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 25 Jun 2025 11:29:02 -0700 Subject: [PATCH 01/84] PR feedback from previous PR --- sdk/core/azure_core/src/lib.rs | 4 +- .../src/attributes.rs | 20 ++++------ sdk/core/azure_core_opentelemetry/src/span.rs | 38 ++++++++----------- .../azure_core_opentelemetry/src/tracer.rs | 26 +++++-------- .../tests/integration_test.rs | 8 ++-- .../src/tracing/attributes.rs | 15 +++++++- .../typespec_client_core/src/tracing/mod.rs | 10 ++--- 7 files changed, 56 insertions(+), 65 deletions(-) diff --git a/sdk/core/azure_core/src/lib.rs b/sdk/core/azure_core/src/lib.rs index 97182941cb..edf21fa808 100644 --- a/sdk/core/azure_core/src/lib.rs +++ b/sdk/core/azure_core/src/lib.rs @@ -27,9 +27,7 @@ pub use typespec_client_core::{ fmt, json, sleep, stream, time, Bytes, Uuid, }; -pub mod tracing { - pub use typespec_client_core::tracing::*; -} +pub use typespec_client_core::tracing; #[cfg(feature = "xml")] pub use typespec_client_core::xml; diff --git a/sdk/core/azure_core_opentelemetry/src/attributes.rs b/sdk/core/azure_core_opentelemetry/src/attributes.rs index b15101cc7c..4b389d5420 100644 --- a/sdk/core/azure_core_opentelemetry/src/attributes.rs +++ b/sdk/core/azure_core_opentelemetry/src/attributes.rs @@ -24,9 +24,9 @@ impl From for AttributeValue { } } -impl From for AttributeValue { - fn from(value: u64) -> Self { - AttributeValue(AzureAttributeValue::U64(value)) +impl From for AttributeValue { + fn from(value: f64) -> Self { + AttributeValue(AzureAttributeValue::I64(value as i64)) } } @@ -47,10 +47,9 @@ impl From> for AttributeArray { AttributeArray(AzureAttributeArray::I64(values)) } } - -impl From> for AttributeArray { - fn from(values: Vec) -> Self { - AttributeArray(AzureAttributeArray::U64(values)) +impl From> for AttributeArray { + fn from(values: Vec) -> Self { + AttributeArray(AzureAttributeArray::F64(values)) } } @@ -66,7 +65,7 @@ impl From for opentelemetry::Value { match value.0 { AzureAttributeValue::Bool(b) => opentelemetry::Value::Bool(b), AzureAttributeValue::I64(i) => opentelemetry::Value::I64(i), - AzureAttributeValue::U64(u) => opentelemetry::Value::I64(u as i64), + AzureAttributeValue::F64(u) => opentelemetry::Value::F64(u), AzureAttributeValue::String(s) => opentelemetry::Value::String(s.into()), AzureAttributeValue::Array(arr) => { opentelemetry::Value::Array(opentelemetry::Array::from(AttributeArray(arr))) @@ -81,10 +80,7 @@ impl From for opentelemetry::Array { match array.0 { AzureAttributeArray::Bool(values) => values.into(), AzureAttributeArray::I64(values) => values.into(), - AzureAttributeArray::U64(values) => { - let i64_values: Vec = values.into_iter().map(|v| v as i64).collect(); - i64_values.into() - } + AzureAttributeArray::F64(values) => values.into(), AzureAttributeArray::String(values) => { let string_values: Vec = values.into_iter().map(|s| s.into()).collect(); diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 2981e3c10c..62c861a5b2 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -140,7 +140,7 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Client).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Client); assert!(span.end().is_ok()); let spans = otel_exporter.get_finished_spans().unwrap(); @@ -159,10 +159,9 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let parent_span = tracer.start_span("parent_span", SpanKind::Server).unwrap(); - let child_span = tracer - .start_span_with_parent("child_span", SpanKind::Client, parent_span.clone()) - .unwrap(); + let parent_span = tracer.start_span("parent_span", SpanKind::Server); + let child_span = + tracer.start_span_with_parent("child_span", SpanKind::Client, parent_span.clone()); assert!(child_span.end().is_ok()); assert!(parent_span.end().is_ok()); @@ -188,11 +187,10 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span1 = tracer.start_span("span1", SpanKind::Internal).unwrap(); - let span2 = tracer.start_span("span2", SpanKind::Server).unwrap(); - let child_span = tracer - .start_span_with_parent("child_span", SpanKind::Client, span1.clone()) - .unwrap(); + let span1 = tracer.start_span("span1", SpanKind::Internal); + let span2 = tracer.start_span("span2", SpanKind::Server); + let child_span = + tracer.start_span_with_parent("child_span", SpanKind::Client, span1.clone()); assert!(child_span.end().is_ok()); assert!(span2.end().is_ok()); @@ -219,14 +217,12 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span1 = tracer.start_span("span1", SpanKind::Internal).unwrap(); - let span2 = tracer.start_span("span2", SpanKind::Server).unwrap(); + let span1 = tracer.start_span("span1", SpanKind::Internal); + let span2 = tracer.start_span("span2", SpanKind::Server); let _span_guard = span2 .set_current(&azure_core::http::Context::new()) .unwrap(); - let child_span = tracer - .start_span_with_current("child_span", SpanKind::Client) - .unwrap(); + let child_span = tracer.start_span_with_current("child_span", SpanKind::Client); assert!(child_span.end().is_ok()); assert!(span2.end().is_ok()); @@ -253,7 +249,7 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); assert!(span .set_attribute("test_key", AttributeValue::String("test_value".to_string())) @@ -279,7 +275,7 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Client).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Client); let error = Error::new(ErrorKind::NotFound, "resource not found"); assert!(span.record_error(&error).is_ok()); @@ -308,14 +304,12 @@ mod tests { let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); // Test Ok status - let span = tracer.start_span("test_span_ok", SpanKind::Server).unwrap(); + let span = tracer.start_span("test_span_ok", SpanKind::Server); assert!(span.set_status(SpanStatus::Ok).is_ok()); assert!(span.end().is_ok()); // Test Error status - let span = tracer - .start_span("test_span_error", SpanKind::Server) - .unwrap(); + let span = tracer.start_span("test_span_error", SpanKind::Server); assert!(span .set_status(SpanStatus::Error { description: "test error".to_string() @@ -355,7 +349,7 @@ mod tests { 42 }; - let span = tracer.start_span("test_span", SpanKind::Client).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Client); let azure_context = AzureContext::new(); let azure_context = azure_context.with_value(span.clone()); diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 9eb9e0fe7f..d32ff66241 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -2,10 +2,7 @@ // Licensed under the MIT License. use crate::span::{OpenTelemetrySpan, OpenTelemetrySpanKind}; -use azure_core::{ - tracing::{SpanKind, Tracer}, - Result, -}; +use azure_core::tracing::{SpanKind, Tracer}; use opentelemetry::{ global::BoxedTracer, trace::{TraceContextExt, Tracer as OpenTelemetryTracerTrait}, @@ -29,26 +26,26 @@ impl Tracer for OpenTelemetryTracer { &self, name: &'static str, kind: SpanKind, - ) -> Result> { + ) -> Arc { let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()); let context = Context::new(); let span = self.inner.build_with_context(span_builder, &context); - Ok(OpenTelemetrySpan::new(context.with_span(span))) + OpenTelemetrySpan::new(context.with_span(span)) } fn start_span_with_current( &self, name: &'static str, kind: SpanKind, - ) -> Result> { + ) -> Arc { let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()); let context = Context::current(); let span = self.inner.build_with_context(span_builder, &context); - Ok(OpenTelemetrySpan::new(context.with_span(span))) + OpenTelemetrySpan::new(context.with_span(span)) } fn start_span_with_parent( @@ -56,7 +53,7 @@ impl Tracer for OpenTelemetryTracer { name: &'static str, kind: SpanKind, parent: Arc, - ) -> Result> { + ) -> Arc { let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()); @@ -64,17 +61,12 @@ impl Tracer for OpenTelemetryTracer { let context = parent .as_any() .downcast_ref::() - .ok_or_else(|| { - azure_core::Error::message( - azure_core::error::ErrorKind::DataConversion, - "Could not downcast parent span to OpenTelemetrySpan", - ) - })? + .expect("Could not downcast parent span to OpenTelemetrySpan") .context() .clone(); let span = self.inner.build_with_context(span_builder, &context); - Ok(OpenTelemetrySpan::new(context.with_span(span))) + OpenTelemetrySpan::new(context.with_span(span)) } } @@ -91,7 +83,7 @@ mod tests { let noop_tracer = NoopTracerProvider::new(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); assert!(span.end().is_ok()); } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index 65c17b31af..584e48b8ee 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use azure_core::tracing::{SpanKind, TracerProvider}; +use azure_core::tracing::{SpanKind, TracerProvider as _}; use azure_core_opentelemetry::OpenTelemetryTracerProvider; use opentelemetry_sdk::trace::SdkTracerProvider; use std::error::Error; @@ -17,7 +17,7 @@ async fn test_span_creation() -> Result<(), Box> { let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); // Create a span using the Azure tracer - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); // Add attributes to the span using individual set_attribute calls span.set_attribute( @@ -43,7 +43,7 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { // Get a tracer and verify it works let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); span.end()?; Ok(()) @@ -59,7 +59,7 @@ async fn test_span_attributes() -> Result<(), Box> { let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); // Create span with multiple attributes - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); // Add attributes using individual set_attribute calls span.set_attribute( diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 66401bdd06..32eb53c593 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,17 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/// An array of homogeneous attribute values. pub enum AttributeArray { + /// An array of boolean values. Bool(Vec), + /// An array of 64-bit signed integers. I64(Vec), - U64(Vec), + /// An array of 64bit floating point values. + F64(Vec), + /// An array of strings. String(Vec), } +/// Represents a single attribute value, which can be of various types. pub enum AttributeValue { + /// A boolean attribute value. Bool(bool), + /// A signed 64-bit integer attribute value. I64(i64), - U64(u64), + /// A 64-bit floating point attribute value + F64(f64), + /// A string attribute value. String(String), + /// An array of attribute values. Array(AttributeArray), } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index bc2c25c59f..498a26d86c 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -4,7 +4,6 @@ //! Distributed tracing trait definitions //! use crate::http::Context; -use crate::Result; use std::sync::Arc; /// Overall architecture for distributed tracing in the SDK. @@ -48,8 +47,7 @@ pub trait Tracer { /// # Returns /// An `Arc` representing the started span. /// - fn start_span(&self, name: &'static str, kind: SpanKind) - -> Result>; + fn start_span(&self, name: &'static str, kind: SpanKind) -> Arc; /// Starts a new span with the given type, using the current span as the parent span. /// @@ -64,7 +62,7 @@ pub trait Tracer { &self, name: &'static str, kind: SpanKind, - ) -> Result>; + ) -> Arc; /// Starts a new child with the given name, type, and parent span. /// @@ -76,12 +74,14 @@ pub trait Tracer { /// # Returns /// An `Arc` representing the started span /// + /// Note: This method may panic if the parent span cannot be downcasted to the expected type. + /// fn start_span_with_parent( &self, name: &'static str, kind: SpanKind, parent: Arc, - ) -> Result>; + ) -> Arc; } pub enum SpanStatus { Unset, From d64c7145d50ce9432b27f7c6eca62bc57ec248d1 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 25 Jun 2025 13:52:42 -0700 Subject: [PATCH 02/84] Start of a dummy service client upon which to hang tracing stuff --- Cargo.lock | 4 ++++ sdk/core/azure_core_opentelemetry/Cargo.toml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index da7e2d083d..2c43faa147 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,7 +205,10 @@ dependencies = [ name = "azure_core_opentelemetry" version = "0.1.0" dependencies = [ + "async-trait", "azure_core", + "azure_core_test", + "azure_core_test_macros", "log", "opentelemetry 0.30.0", "opentelemetry_sdk 0.30.0", @@ -214,6 +217,7 @@ dependencies = [ "tracing-opentelemetry", "tracing-subscriber", "typespec_client_core", + "url", ] [[package]] diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index df4babf063..b81e4e5f2d 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -22,10 +22,14 @@ tracing.workspace = true typespec_client_core.workspace = true [dev-dependencies] +async-trait.workspace = true +azure_core_test = { workspace = true, features = ["tracing"] } +azure_core_test_macros.workspace = true opentelemetry_sdk = { version = "0.30", features = ["testing"] } tokio.workspace = true tracing-opentelemetry = "0.26" tracing-subscriber.workspace = true +url.workspace = true [lints] workspace = true From a5d6b2d0663b249532aa7033a08cb15e2a4e259f Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Thu, 26 Jun 2025 15:25:30 -0700 Subject: [PATCH 03/84] Create RequestInstrumentationPolicy to add spans for HTTP operations --- sdk/core/azure_core/src/http/options/mod.rs | 4 + .../http/options/request_instrumentation.rs | 11 + sdk/core/azure_core/src/http/policies/mod.rs | 2 + .../http/policies/request_instrumentation.rs | 535 ++++++++++++++++++ .../src/attributes.rs | 17 +- sdk/core/azure_core_opentelemetry/src/span.rs | 103 ++-- .../azure_core_opentelemetry/src/telemetry.rs | 12 +- .../azure_core_opentelemetry/src/tracer.rs | 60 +- .../tests/integration_test.rs | 22 +- .../tests/telemetry_service_implementation.rs | 199 +++++++ .../src/http/policies/retry/mod.rs | 8 +- .../src/tracing/attributes.rs | 102 +++- .../typespec_client_core/src/tracing/mod.rs | 57 +- 13 files changed, 1021 insertions(+), 111 deletions(-) create mode 100644 sdk/core/azure_core/src/http/options/request_instrumentation.rs create mode 100644 sdk/core/azure_core/src/http/policies/request_instrumentation.rs create mode 100644 sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs diff --git a/sdk/core/azure_core/src/http/options/mod.rs b/sdk/core/azure_core/src/http/options/mod.rs index a9593c3563..70d8b6eded 100644 --- a/sdk/core/azure_core/src/http/options/mod.rs +++ b/sdk/core/azure_core/src/http/options/mod.rs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +mod request_instrumentation; mod user_agent; +pub use request_instrumentation::*; use std::sync::Arc; use typespec_client_core::http::policies::Policy; pub use typespec_client_core::http::{ @@ -27,6 +29,8 @@ pub struct ClientOptions { /// User-Agent telemetry options. pub user_agent: Option, + + pub request_instrumentation: Option, } impl ClientOptions { diff --git a/sdk/core/azure_core/src/http/options/request_instrumentation.rs b/sdk/core/azure_core/src/http/options/request_instrumentation.rs new file mode 100644 index 0000000000..d15dd01584 --- /dev/null +++ b/sdk/core/azure_core/src/http/options/request_instrumentation.rs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use std::sync::Arc; + +/// Policy options to enable distributed tracing. +#[derive(Clone, Debug, Default)] +pub struct RequestInstrumentationOptions { + /// Set the tracing provider for distributed tracing. + pub tracing_provider: Option>, +} diff --git a/sdk/core/azure_core/src/http/policies/mod.rs b/sdk/core/azure_core/src/http/policies/mod.rs index 81c8e3769a..2d07bf36f7 100644 --- a/sdk/core/azure_core/src/http/policies/mod.rs +++ b/sdk/core/azure_core/src/http/policies/mod.rs @@ -5,9 +5,11 @@ mod bearer_token_policy; mod client_request_id; +mod request_instrumentation; mod user_agent; pub use bearer_token_policy::BearerTokenCredentialPolicy; pub use client_request_id::*; +pub use request_instrumentation::*; pub use typespec_client_core::http::policies::*; pub use user_agent::*; diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs new file mode 100644 index 0000000000..9785269650 --- /dev/null +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -0,0 +1,535 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use crate::{ + http::{headers, options::RequestInstrumentationOptions, Context, Request}, + tracing::{Span, SpanKind}, +}; +use std::sync::Arc; +use typespec_client_core::{ + http::policies::{Policy, PolicyResult, RetryPolicyCount}, + tracing::Attribute, +}; + +#[allow(dead_code)] +const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; + +const AZ_SCHEMA_URL_ATTRIBUTE: &str = "az.schema.url"; +const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client.request.id"; +const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; +const AZ_SERVICE_REQUEST_ID_ATTRIBUTE: &str = "az.service.request.id"; +const HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE: &str = "http.request.resend.count"; +const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE: &str = "http.response.status_code"; +const HTTP_REQUEST_METHOD_ATTRIBUTE: &str = "http.request.method"; +const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; +const SERVER_PORT_ATTRIBUTE: &str = "server.port"; +const URL_FULL_ATTRIBUTE: &str = "url.full"; + +/// Sets the User-Agent header with useful information in a typical format for Azure SDKs. +#[derive(Clone, Debug)] +pub struct RequestInstrumentationPolicy { + tracer: Option>, +} + +impl<'a> RequestInstrumentationPolicy { + pub fn new( + crate_name: Option<&'a str>, + crate_version: Option<&'a str>, + options: Option<&RequestInstrumentationOptions>, + ) -> Self { + if let Some(tracing_provider) = options.and_then(|o| o.tracing_provider.clone()) { + Self { + tracer: Some(tracing_provider.get_tracer( + crate_name.unwrap_or("unknown"), + crate_version.unwrap_or("unknown"), + )), + } + } else { + // If no tracing provider is set, we return a policy with no tracer. + Self { tracer: None } + } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Policy for RequestInstrumentationPolicy { + async fn send( + &self, + ctx: &Context, + request: &mut Request, + next: &[Arc], + ) -> PolicyResult { + if let Some(tracer) = &self.tracer { + let mut span_attributes = vec![ + Attribute { + key: HTTP_REQUEST_METHOD_ATTRIBUTE, + value: request.method().to_string().into(), + }, + Attribute { + key: AZ_SCHEMA_URL_ATTRIBUTE, + value: request.url().scheme().into(), + }, + ]; + + if !request.url().username().is_empty() || request.url().password().is_some() { + // If the URL contains a password, we do not log it for security reasons. + let full_url = format!( + "{}://{}{}{}{}{}", + request.url().scheme(), + request + .url() + .host() + .map_or_else(|| "unknown_host".to_string(), |h| h.to_string()), + request + .url() + .port() + .map_or_else(String::new, |p| format!(":{}", p)), + request.url().path(), + request + .url() + .query() + .map_or_else(String::new, |q| format!("?{}", q)), + request + .url() + .fragment() + .map_or_else(String::new, |f| format!("#{}", f)), + ); + span_attributes.push(Attribute { + key: URL_FULL_ATTRIBUTE, + value: full_url.into(), + }); + } else { + // If no password is present, we log the full URL. + span_attributes.push(Attribute { + key: URL_FULL_ATTRIBUTE, + value: request.url().to_string().into(), + }); + } + + if let Some(host) = request.url().host() { + span_attributes.push(Attribute { + key: SERVER_ADDRESS_ATTRIBUTE, + value: host.to_string().into(), + }); + } + if let Some(port) = request.url().port_or_known_default() { + span_attributes.push(Attribute { + key: SERVER_PORT_ATTRIBUTE, + value: port.into(), + }); + } + let span = if let Some(parent_span) = ctx.value::>() { + // If a parent span exists, start a new span with the parent. + tracer.start_span_with_parent( + request.method().to_string().as_str(), + SpanKind::Client, + span_attributes, + parent_span.clone(), + ) + } else { + // If no parent span exists, start a new span without a parent. + tracer.start_span_with_current( + request.method().to_string().as_str(), + SpanKind::Client, + span_attributes, + ) + }; + + if let Some(client_request_id) = request + .headers() + .get_optional_str(&headers::CLIENT_REQUEST_ID) + { + span.set_attribute(AZ_CLIENT_REQUEST_ID_ATTRIBUTE, client_request_id.into()); + } + + if let Some(service_request_id) = + request.headers().get_optional_str(&headers::REQUEST_ID) + { + span.set_attribute(AZ_SERVICE_REQUEST_ID_ATTRIBUTE, service_request_id.into()); + } + + if let Some(retry_count) = ctx.value::() { + span.set_attribute(HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, retry_count.0.into()); + } + + let result = next[0].send(ctx, request, &next[1..]).await; + + if result.is_err() { + // If the request failed, set an error type attribute. + span.set_attribute( + ERROR_TYPE_ATTRIBUTE, + result.as_ref().err().unwrap().to_string().into(), + ); + } + if let Ok(response) = result.as_ref() { + // If the request was successful, set the HTTP response status code. + span.set_attribute( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + u16::from(response.status()).into(), + ); + + if response.status().is_server_error() || response.status().is_client_error() { + // If the response status indicates an error, set the span status to error. + span.set_status(crate::tracing::SpanStatus::Error { + description: format!( + "HTTP request failed with status code {}: {}", + response.status(), + response.status().canonical_reason() + ), + }); + } + } + + span.end(); + return result; + } else { + // If no tracer is set, we simply forward the request without instrumentation. + next[0].send(ctx, request, &next[1..]).await + } + } +} +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + use typespec_client_core::{ + http::{ + headers::Headers, policies::TransportPolicy, Method, RawResponse, StatusCode, + TransportOptions, + }, + tracing::{AsAny, AttributeValue, Span, SpanStatus, Tracer, TracerProvider}, + }; + + #[derive(Debug)] + struct MockTransport; + + #[async_trait::async_trait] + impl Policy for MockTransport { + async fn send( + &self, + _ctx: &Context, + _request: &mut Request, + _next: &[Arc], + ) -> PolicyResult { + PolicyResult::Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + Vec::new(), + )) + } + } + + #[derive(Debug)] + struct MockTracingProvider { + tracers: Mutex>>, + } + + impl MockTracingProvider { + fn new() -> Self { + Self { + tracers: Mutex::new(Vec::new()), + } + } + } + impl TracerProvider for MockTracingProvider { + fn get_tracer( + &self, + crate_name: &str, + crate_version: &str, + ) -> Arc { + let mut tracers = self.tracers.lock().unwrap(); + let tracer = Arc::new(MockTracer { + name: crate_name.to_string(), + version: crate_version.to_string(), + spans: Mutex::new(Vec::new()), + }); + + tracers.push(tracer.clone()); + tracer + } + } + + #[derive(Debug)] + struct MockTracer { + name: String, + version: String, + spans: Mutex>>, + } + + impl Tracer for MockTracer { + fn start_span_with_current( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + + fn start_span_with_parent( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + _parent: Arc, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + + fn start_span( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + } + + #[derive(Debug)] + struct MockSpan { + name: String, + #[allow(dead_code)] + kind: SpanKind, + #[allow(dead_code)] + attributes: Mutex>, + state: Mutex, + is_open: Mutex, + } + impl MockSpan { + fn new(name: &str, kind: SpanKind, attributes: Vec) -> Self { + println!("Creating MockSpan: {}", name); + println!("Attributes: {:?}", attributes); + Self { + name: name.to_string(), + kind, + attributes: Mutex::new(attributes), + state: Mutex::new(SpanStatus::Unset), + is_open: Mutex::new(true), + } + } + } + + impl Span for MockSpan { + fn set_attribute(&self, key: &'static str, value: AttributeValue) { + println!("{}: Setting attribute {}: {:?}", self.name, key, value); + let mut attributes = self.attributes.lock().unwrap(); + attributes.push(Attribute { key, value }); + } + + fn set_status(&self, status: crate::tracing::SpanStatus) { + println!("{}: Setting span status: {:?}", self.name, status); + let mut state = self.state.lock().unwrap(); + *state = status; + } + + fn end(&self) { + println!("Ending span: {}", self.name); + let mut is_open = self.is_open.lock().unwrap(); + *is_open = false; + } + + fn is_recording(&self) -> bool { + true + } + + fn span_id(&self) -> [u8; 8] { + [0; 8] // Mock span ID + } + + fn record_error(&self, _error: &dyn std::error::Error) { + todo!() + } + + fn set_current( + &self, + _context: &Context, + ) -> typespec_client_core::Result> + { + todo!() + } + } + + impl AsAny for MockSpan { + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + async fn run_instrumentation_test( + crate_name: Option<&str>, + version: Option<&str>, + request: &mut Request, + ) -> Arc { + let mock_tracer = Arc::new(MockTracingProvider::new()); + let options = RequestInstrumentationOptions { + tracing_provider: Some(mock_tracer.clone()), + }; + let policy = Arc::new(RequestInstrumentationPolicy::new( + crate_name, + version, + Some(&options), + )); + + let transport = + TransportPolicy::new(TransportOptions::new_custom_policy(Arc::new(MockTransport))); + + let ctx = Context::default(); + let next: Vec> = vec![Arc::new(transport)]; + let _result = policy.send(&ctx, request, &next).await; + + mock_tracer + } + fn check_instrumentation_result( + mock_tracer: Arc, + expected_name: &str, + expected_version: &str, + expected_method: &str, + expected_attributes: Vec<(&str, AttributeValue)>, + ) { + assert_eq!( + mock_tracer.tracers.lock().unwrap().len(), + 1, + "Expected one tracer to be created", + ); + let tracers = mock_tracer.tracers.lock().unwrap(); + let tracer = tracers.first().unwrap(); + assert_eq!(tracer.name, expected_name); + assert_eq!(tracer.version, expected_version); + let spans = tracer.spans.lock().unwrap(); + assert_eq!(spans.len(), 1, "Expected one span to be created"); + println!("Spans: {:?}", spans); + let span = spans.first().unwrap(); + assert_eq!(span.name, expected_method); + let attributes = span.attributes.lock().unwrap(); + for attr in attributes.iter() { + println!("Attribute: {} = {:?}", attr.key, attr.value); + let mut found = false; + for (key, value) in &expected_attributes { + if attr.key == *key { + assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); + found = true; + break; + } + } + if !found { + panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); + } + } + for (key, value) in &expected_attributes { + if !attributes + .iter() + .any(|attr| attr.key == *key && attr.value == *value) + { + panic!("Expected attribute not found: {} = {:?}", key, value); + } + } + } + + #[tokio::test] + async fn simple_instrumentation_policy() { + let url = "http://example.com/path"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer = + run_instrumentation_test(Some("test_crate"), Some("1.0.0"), &mut request).await; + + check_instrumentation_result( + mock_tracer, + "test_crate", + "1.0.0", + "GET", + vec![ + (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("http")), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(80)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("http://example.com/path"), + ), + ], + ); + } + + #[tokio::test] + async fn client_request_id() { + let url = "https://example.com/client_request_id"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + request.insert_header(headers::CLIENT_REQUEST_ID, "test-client-request-id"); + + let mock_tracer = + run_instrumentation_test(Some("test_crate"), Some("1.0.0"), &mut request).await; + + check_instrumentation_result( + mock_tracer.clone(), + "test_crate", + "1.0.0", + "GET", + vec![ + (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + ( + AZ_CLIENT_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-client-request-id"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://example.com/client_request_id"), + ), + ], + ); + } + + #[tokio::test] + async fn test_url_with_password() { + let url = "https://user:password@host:8080/path?query=value#fragment"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer_provider = run_instrumentation_test(None, None, &mut request).await; + + check_instrumentation_result( + mock_tracer_provider, + "unknown", + "unknown", + "GET", + vec![ + (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + (SERVER_ADDRESS_ATTRIBUTE, AttributeValue::from("host")), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(8080)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://host:8080/path?query=value#fragment"), + ), + ], + ); + } +} diff --git a/sdk/core/azure_core_opentelemetry/src/attributes.rs b/sdk/core/azure_core_opentelemetry/src/attributes.rs index 4b389d5420..2a87b25199 100644 --- a/sdk/core/azure_core_opentelemetry/src/attributes.rs +++ b/sdk/core/azure_core_opentelemetry/src/attributes.rs @@ -5,13 +5,17 @@ // Re-export typespec_client_core tracing attributes for convenience use azure_core::tracing::{ - AttributeArray as AzureAttributeArray, AttributeValue as AzureAttributeValue, + Attribute as AzureAttribute, AttributeArray as AzureAttributeArray, + AttributeValue as AzureAttributeValue, }; +use opentelemetry::KeyValue; pub(super) struct AttributeArray(AzureAttributeArray); pub(super) struct AttributeValue(pub AzureAttributeValue); +pub(super) struct OpenTelemetryAttribute(pub AzureAttribute); + impl From for AttributeValue { fn from(value: bool) -> Self { AttributeValue(AzureAttributeValue::Bool(value)) @@ -59,13 +63,22 @@ impl From> for AttributeArray { } } +impl From for KeyValue { + fn from(attr: OpenTelemetryAttribute) -> Self { + KeyValue::new( + attr.0.key, + opentelemetry::Value::from(AttributeValue(attr.0.value)), + ) + } +} + /// Conversion from typespec_client_core AttributeValue to OpenTelemetry Value impl From for opentelemetry::Value { fn from(value: AttributeValue) -> Self { match value.0 { AzureAttributeValue::Bool(b) => opentelemetry::Value::Bool(b), AzureAttributeValue::I64(i) => opentelemetry::Value::I64(i), - AzureAttributeValue::F64(u) => opentelemetry::Value::F64(u), + AzureAttributeValue::F64(f) => opentelemetry::Value::F64(f), AzureAttributeValue::String(s) => opentelemetry::Value::String(s.into()), AzureAttributeValue::Array(arr) => { opentelemetry::Value::Array(opentelemetry::Array::from(AttributeArray(arr))) diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 62c861a5b2..914e1a599e 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -41,40 +41,40 @@ impl OpenTelemetrySpan { } impl Span for OpenTelemetrySpan { - fn end(&self) -> Result<()> { + fn is_recording(&self) -> bool { + self.context.span().is_recording() + } + + fn end(&self) { self.context.span().end(); - Ok(()) } fn span_id(&self) -> [u8; 8] { self.context.span().span_context().span_id().to_bytes() } - fn set_attribute(&self, key: &'static str, value: AttributeValue) -> Result<()> { + fn set_attribute(&self, key: &'static str, value: AttributeValue) { let otel_value = opentelemetry::Value::from(ConversionAttributeValue(value)); self.context .span() .set_attribute(opentelemetry::KeyValue::new(key, otel_value)); - Ok(()) } - fn record_error(&self, error: &dyn StdError) -> Result<()> { + fn record_error(&self, error: &dyn StdError) { self.context.span().record_error(error); self.context .span() .set_status(opentelemetry::trace::Status::error(error.to_string())); - Ok(()) } - fn set_status(&self, status: SpanStatus) -> Result<()> { + fn set_status(&self, status: SpanStatus) { let otel_status = match status { SpanStatus::Unset => opentelemetry::trace::Status::Unset, SpanStatus::Ok => opentelemetry::trace::Status::Ok, SpanStatus::Error { description } => opentelemetry::trace::Status::error(description), }; self.context.span().set_status(otel_status); - Ok(()) } fn set_current( @@ -117,7 +117,7 @@ impl Drop for OpenTelemetrySpanGuard { mod tests { use crate::telemetry::OpenTelemetryTracerProvider; use azure_core::http::Context as AzureContext; - use azure_core::tracing::{AttributeValue, SpanKind, SpanStatus, TracerProvider}; + use azure_core::tracing::{Attribute, AttributeValue, SpanKind, SpanStatus, TracerProvider}; use opentelemetry::trace::TraceContextExt; use opentelemetry::{Context, Key, KeyValue, Value}; use opentelemetry_sdk::trace::{in_memory_exporter::InMemorySpanExporter, SdkTracerProvider}; @@ -140,8 +140,8 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Client); - assert!(span.end().is_ok()); + let span = tracer.start_span("test_span", SpanKind::Client, vec![]); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); @@ -159,12 +159,16 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let parent_span = tracer.start_span("parent_span", SpanKind::Server); - let child_span = - tracer.start_span_with_parent("child_span", SpanKind::Client, parent_span.clone()); + let parent_span = tracer.start_span("parent_span", SpanKind::Server, vec![]); + let child_span = tracer.start_span_with_parent( + "child_span", + SpanKind::Client, + vec![], + parent_span.clone(), + ); - assert!(child_span.end().is_ok()); - assert!(parent_span.end().is_ok()); + child_span.end(); + parent_span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 2); @@ -187,14 +191,14 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span1 = tracer.start_span("span1", SpanKind::Internal); - let span2 = tracer.start_span("span2", SpanKind::Server); + let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); + let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let child_span = - tracer.start_span_with_parent("child_span", SpanKind::Client, span1.clone()); + tracer.start_span_with_parent("child_span", SpanKind::Client, vec![], span1.clone()); - assert!(child_span.end().is_ok()); - assert!(span2.end().is_ok()); - assert!(span1.end().is_ok()); + child_span.end(); + span2.end(); + span1.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 3); @@ -217,16 +221,16 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span1 = tracer.start_span("span1", SpanKind::Internal); - let span2 = tracer.start_span("span2", SpanKind::Server); + let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); + let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let _span_guard = span2 .set_current(&azure_core::http::Context::new()) .unwrap(); - let child_span = tracer.start_span_with_current("child_span", SpanKind::Client); + let child_span = tracer.start_span_with_current("child_span", SpanKind::Client, vec![]); - assert!(child_span.end().is_ok()); - assert!(span2.end().is_ok()); - assert!(span1.end().is_ok()); + child_span.end(); + span2.end(); + span1.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 3); @@ -249,12 +253,10 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Internal); + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); - assert!(span - .set_attribute("test_key", AttributeValue::String("test_value".to_string())) - .is_ok()); - assert!(span.end().is_ok()); + span.set_attribute("test_key", AttributeValue::String("test_value".to_string())); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); @@ -275,11 +277,11 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Client); + let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let error = Error::new(ErrorKind::NotFound, "resource not found"); - assert!(span.record_error(&error).is_ok()); - assert!(span.end().is_ok()); + span.record_error(&error); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); @@ -304,18 +306,16 @@ mod tests { let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); // Test Ok status - let span = tracer.start_span("test_span_ok", SpanKind::Server); - assert!(span.set_status(SpanStatus::Ok).is_ok()); - assert!(span.end().is_ok()); + let span = tracer.start_span("test_span_ok", SpanKind::Server, vec![]); + span.set_status(SpanStatus::Ok); + span.end(); // Test Error status - let span = tracer.start_span("test_span_error", SpanKind::Server); - assert!(span - .set_status(SpanStatus::Error { - description: "test error".to_string() - }) - .is_ok()); - assert!(span.end().is_ok()); + let span = tracer.start_span("test_span_error", SpanKind::Server, vec![]); + span.set_status(SpanStatus::Error { + description: "test error".to_string(), + }); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 2); @@ -349,7 +349,14 @@ mod tests { 42 }; - let span = tracer.start_span("test_span", SpanKind::Client); + let span = tracer.start_span( + "test_span", + SpanKind::Client, + vec![Attribute { + key: "test_key", + value: "test_value".into(), + }], + ); let azure_context = AzureContext::new(); let azure_context = azure_context.with_value(span.clone()); @@ -359,7 +366,7 @@ mod tests { let result = future.await; assert_eq!(result, 42); - span.end().unwrap(); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index ea7f1ebf57..dbe9cbddba 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -26,13 +26,13 @@ impl OpenTelemetryTracerProvider { impl TracerProvider for OpenTelemetryTracerProvider { fn get_tracer( &self, - name: &'static str, - package_version: &'static str, - ) -> Box { - let scope = InstrumentationScope::builder(name) - .with_version(package_version) + name: &str, + package_version: &str, + ) -> Arc { + let scope = InstrumentationScope::builder(name.to_owned()) + .with_version(package_version.to_owned()) .build(); - Box::new(OpenTelemetryTracer::new(BoxedTracer::new( + Arc::new(OpenTelemetryTracer::new(BoxedTracer::new( self.inner.boxed_tracer(scope), ))) } diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index d32ff66241..8cc8bfc4a9 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -1,12 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::span::{OpenTelemetrySpan, OpenTelemetrySpanKind}; -use azure_core::tracing::{SpanKind, Tracer}; +use crate::{ + attributes::OpenTelemetryAttribute, + span::{OpenTelemetrySpan, OpenTelemetrySpanKind}, +}; + +use azure_core::tracing::{Attribute, SpanKind, Tracer}; use opentelemetry::{ global::BoxedTracer, trace::{TraceContextExt, Tracer as OpenTelemetryTracerTrait}, - Context, + Context, KeyValue, }; use std::sync::Arc; @@ -24,11 +28,17 @@ impl OpenTelemetryTracer { impl Tracer for OpenTelemetryTracer { fn start_span( &self, - name: &'static str, + name: &str, kind: SpanKind, - ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) - .with_kind(OpenTelemetrySpanKind(kind).into()); + attributes: Vec, + ) -> Arc { + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + .with_kind(OpenTelemetrySpanKind(kind).into()) + .with_attributes( + attributes + .into_iter() + .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr))), + ); let context = Context::new(); let span = self.inner.build_with_context(span_builder, &context); @@ -37,11 +47,17 @@ impl Tracer for OpenTelemetryTracer { fn start_span_with_current( &self, - name: &'static str, + name: &str, kind: SpanKind, - ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) - .with_kind(OpenTelemetrySpanKind(kind).into()); + attributes: Vec, + ) -> Arc { + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + .with_kind(OpenTelemetrySpanKind(kind).into()) + .with_attributes( + attributes + .into_iter() + .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr))), + ); let context = Context::current(); let span = self.inner.build_with_context(span_builder, &context); @@ -50,12 +66,18 @@ impl Tracer for OpenTelemetryTracer { fn start_span_with_parent( &self, - name: &'static str, + name: &str, kind: SpanKind, - parent: Arc, - ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) - .with_kind(OpenTelemetrySpanKind(kind).into()); + attributes: Vec, + parent: Arc, + ) -> Arc { + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + .with_kind(OpenTelemetrySpanKind(kind).into()) + .with_attributes( + attributes + .into_iter() + .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr))), + ); // Cast the parent span to Any type let context = parent @@ -83,8 +105,8 @@ mod tests { let noop_tracer = NoopTracerProvider::new(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); - let span = tracer.start_span("test_span", SpanKind::Internal); - assert!(span.end().is_ok()); + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); + span.end(); } #[test] @@ -99,6 +121,6 @@ mod tests { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); - let _span = tracer.start_span("test_span", SpanKind::Internal); + let _span = tracer.start_span("test_span", SpanKind::Internal, vec![]); } } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index 584e48b8ee..b9cc00973b 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -17,20 +17,20 @@ async fn test_span_creation() -> Result<(), Box> { let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); // Create a span using the Azure tracer - let span = tracer.start_span("test_span", SpanKind::Internal); + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); // Add attributes to the span using individual set_attribute calls span.set_attribute( "test_key", azure_core::tracing::AttributeValue::String("test_value".to_string()), - )?; + ); span.set_attribute( "service.name", azure_core::tracing::AttributeValue::String("azure-test".to_string()), - )?; + ); // End the span - span.end()?; + span.end(); Ok(()) } @@ -43,8 +43,8 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { // Get a tracer and verify it works let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); - let span = tracer.start_span("test_span", SpanKind::Internal); - span.end()?; + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); + span.end(); Ok(()) } @@ -59,24 +59,24 @@ async fn test_span_attributes() -> Result<(), Box> { let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); // Create span with multiple attributes - let span = tracer.start_span("test_span", SpanKind::Internal); + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); // Add attributes using individual set_attribute calls span.set_attribute( "service.name", azure_core::tracing::AttributeValue::String("test-service".to_string()), - )?; + ); span.set_attribute( "operation.name", azure_core::tracing::AttributeValue::String("test-operation".to_string()), - )?; + ); span.set_attribute( "request.id", azure_core::tracing::AttributeValue::String("req-123".to_string()), - )?; + ); // End the span - span.end()?; + span.end(); Ok(()) } diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs new file mode 100644 index 0000000000..911a50919e --- /dev/null +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -0,0 +1,199 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//! This file contains an Azure SDK for Rust fake service client API. +//! +use azure_core::{ + credentials::TokenCredential, + fmt::SafeDebug, + http::{ + policies::{BearerTokenCredentialPolicy, Policy}, + ClientMethodOptions, ClientOptions, Pipeline, RawResponse, Request, + RequestInstrumentationOptions, Url, + }, + Result, +}; +use azure_core_opentelemetry::OpenTelemetryTracerProvider; +use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider}; +use std::sync::Arc; + +#[derive(Default, Clone, SafeDebug)] +pub struct TestServiceClientOptions { + pub azure_client_options: ClientOptions, + pub api_version: String, +} + +pub struct TestServiceClient { + endpoint: Url, + api_version: String, + pipeline: Pipeline, +} + +#[derive(Default, SafeDebug)] +pub struct TestServiceClientGetMethodOptions<'a> { + pub method_options: ClientMethodOptions<'a>, +} + +impl TestServiceClient { + pub fn new( + endpoint: &str, + credential: Arc, + options: Option, + ) -> Result { + let options = options.unwrap_or_default(); + let mut endpoint = Url::parse(endpoint)?; + if !endpoint.scheme().starts_with("http") { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("{endpoint} must use http(s)"), + )); + } + endpoint.set_query(None); + let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( + credential, + vec!["https://vault.azure.net/.default"], + )); + let request_instrumentation_policy = Arc::new( + azure_core::http::policies::RequestInstrumentationPolicy::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options + .azure_client_options + .request_instrumentation + .as_ref(), + ), + ); + Ok(Self { + endpoint, + api_version: options.api_version, + pipeline: Pipeline::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options.azure_client_options, + Vec::default(), + vec![auth_policy, request_instrumentation_policy], + ), + }) + } + + /// Returns the Url associated with this client. + pub fn endpoint(&self) -> &Url { + &self.endpoint + } + + pub async fn get( + &self, + path: &str, + options: Option>, + ) -> Result { + let options = options.unwrap_or_default(); + let mut url = self.endpoint.clone(); + url.set_path(path); + url.query_pairs_mut() + .append_pair("api-version", &self.api_version); + + let mut request = Request::new(url, azure_core::http::Method::Get); + + let response = self + .pipeline + .send(&options.method_options.context, &mut request) + .await?; + if !response.status().is_success() { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::HttpResponse { + status: response.status(), + error_code: None, + }, + format!("Failed to GET {}: {}", request.url(), response.status()), + )); + } + Ok(response) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use azure_core::Result; + use azure_core_test::{recorded, TestContext}; + use tracing::{info, trace}; + + fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { + let otel_exporter = InMemorySpanExporter::default(); + let otel_tracer_provider = SdkTracerProvider::builder() + .with_simple_exporter(otel_exporter.clone()) + .build(); + let otel_tracer_provider = Arc::new(otel_tracer_provider); + (otel_tracer_provider, otel_exporter) + } + + #[recorded::test()] + async fn test_service_client_new(ctx: TestContext) -> Result<()> { + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + assert_eq!(client.endpoint().as_str(), "https://example.com/"); + assert_eq!(client.api_version, "2023-10-01"); + + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get(ctx: TestContext) -> Result<()> { + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("index.html", None).await; + info!("Response: {:?}", response); + assert!(response.is_ok()); + let response = response.unwrap(); + assert_eq!(response.status(), azure_core::http::StatusCode::Ok); + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("index.html", None).await; + info!("Response: {:?}", response); + assert!(response.is_ok()); + let response = response.unwrap(); + assert_eq!(response.status(), azure_core::http::StatusCode::Ok); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + for span in &spans { + trace!("Span: {:?}", span); + } + + Ok(()) + } +} diff --git a/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs b/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs index b4ea0e5098..818efe34c7 100644 --- a/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs @@ -68,6 +68,11 @@ pub fn get_retry_after(headers: &Headers, now: DateTimeFn) -> Option { }) } +/// A wrapper around a retry count to be used in the context of a retry policy. +/// +/// This allows a post-retry policy to access the retry count +pub struct RetryPolicyCount(pub u32); + /// A retry policy. /// /// In the simple form, the policies need only differ in how @@ -131,7 +136,8 @@ where "failed to reset body stream before retrying request", )?; } - let result = next[0].send(ctx, request, &next[1..]).await; + let try_context = ctx.clone().with_value(RetryPolicyCount(retry_count)); + let result = next[0].send(&try_context, request, &next[1..]).await; // only start keeping track of time after the first request is made let start = start.get_or_insert_with(OffsetDateTime::now_utc); let (last_error, retry_after) = match result { diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 32eb53c593..f0321a941c 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use crate::fmt::SafeDebug; + /// An array of homogeneous attribute values. +#[derive(SafeDebug, PartialEq)] pub enum AttributeArray { /// An array of boolean values. Bool(Vec), @@ -13,7 +16,8 @@ pub enum AttributeArray { String(Vec), } -/// Represents a single attribute value, which can be of various types. +/// Represents a single attribute value, which can be of various types +#[derive(SafeDebug, PartialEq)] pub enum AttributeValue { /// A boolean attribute value. Bool(bool), @@ -26,3 +30,99 @@ pub enum AttributeValue { /// An array of attribute values. Array(AttributeArray), } +#[derive(SafeDebug)] +pub struct Attribute { + /// A key-value pair attribute. + pub key: &'static str, + pub value: AttributeValue, +} + +impl PartialEq<&str> for AttributeValue { + fn eq(&self, other: &&str) -> bool { + match self { + AttributeValue::String(s) => s == *other, + _ => false, + } + } +} + +impl PartialEq for AttributeValue { + fn eq(&self, other: &i64) -> bool { + match self { + AttributeValue::I64(i) => i == other, + _ => false, + } + } +} + +impl From for AttributeValue { + fn from(value: String) -> Self { + AttributeValue::String(value) + } +} + +impl From<&str> for AttributeValue { + fn from(value: &str) -> Self { + AttributeValue::String(value.to_string()) + } +} + +impl From for AttributeValue { + fn from(value: bool) -> Self { + AttributeValue::Bool(value) + } +} + +impl From for AttributeValue { + fn from(value: i32) -> Self { + AttributeValue::I64(value as i64) + } +} + +impl From for AttributeValue { + fn from(value: u16) -> Self { + AttributeValue::I64(value as i64) + } +} + +impl From for AttributeValue { + fn from(value: u32) -> Self { + AttributeValue::I64(value as i64) + } +} + +impl From for AttributeValue { + fn from(value: i64) -> Self { + AttributeValue::I64(value) + } +} + +impl From for AttributeValue { + fn from(value: f64) -> Self { + AttributeValue::F64(value) + } +} + +impl From> for AttributeValue { + fn from(value: Vec) -> Self { + AttributeValue::Array(AttributeArray::Bool(value)) + } +} + +impl From> for AttributeValue { + fn from(value: Vec) -> Self { + AttributeValue::Array(AttributeArray::I64(value)) + } +} + +impl From> for AttributeValue { + fn from(value: Vec) -> Self { + AttributeValue::Array(AttributeArray::F64(value)) + } +} + +impl From> for AttributeValue { + fn from(value: Vec) -> Self { + AttributeValue::Array(AttributeArray::String(value)) + } +} diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index 498a26d86c..f901ea4e27 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -4,6 +4,7 @@ //! Distributed tracing trait definitions //! use crate::http::Context; +use std::fmt::Debug; use std::sync::Arc; /// Overall architecture for distributed tracing in the SDK. @@ -18,26 +19,28 @@ use std::sync::Arc; mod attributes; mod with_context; -pub use attributes::{AttributeArray, AttributeValue}; +pub use attributes::{Attribute, AttributeArray, AttributeValue}; pub use with_context::{FutureExt, WithContext}; /// The TracerProvider trait is the entrypoint for distributed tracing in the SDK. /// /// It provides a method to get a tracer for a specific name and package version. -pub trait TracerProvider { +pub trait TracerProvider: Send + Sync { /// Returns a tracer for the given name. /// /// Arguments: /// - `package_name`: The name of the package for which the tracer is requested. /// - `package_version`: The version of the package for which the tracer is requested. - fn get_tracer( - &self, - package_name: &'static str, - package_version: &'static str, - ) -> Box; + fn get_tracer(&self, package_name: &str, package_version: &str) -> Arc; +} + +impl Debug for dyn TracerProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TracerProvider").finish_non_exhaustive() + } } -pub trait Tracer { +pub trait Tracer: Send + Sync { /// Starts a new span with the given name and type. /// /// # Arguments @@ -47,7 +50,7 @@ pub trait Tracer { /// # Returns /// An `Arc` representing the started span. /// - fn start_span(&self, name: &'static str, kind: SpanKind) -> Arc; + fn start_span(&self, name: &str, kind: SpanKind, attributes: Vec) -> Arc; /// Starts a new span with the given type, using the current span as the parent span. /// @@ -60,9 +63,10 @@ pub trait Tracer { /// fn start_span_with_current( &self, - name: &'static str, + name: &str, kind: SpanKind, - ) -> Arc; + attributes: Vec, + ) -> Arc; /// Starts a new child with the given name, type, and parent span. /// @@ -78,11 +82,20 @@ pub trait Tracer { /// fn start_span_with_parent( &self, - name: &'static str, + name: &str, kind: SpanKind, - parent: Arc, - ) -> Arc; + attributes: Vec, + parent: Arc, + ) -> Arc; +} + +impl Debug for dyn Tracer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tracer").finish_non_exhaustive() + } } + +#[derive(Debug)] pub enum SpanStatus { Unset, Ok, @@ -104,12 +117,14 @@ pub trait SpanGuard { fn end(self) -> crate::Result<()>; } -pub trait Span: AsAny { +pub trait Span: AsAny + Send + Sync { + fn is_recording(&self) -> bool; + /// The 8 byte value which identifies the span. fn span_id(&self) -> [u8; 8]; /// Ends the current span. - fn end(&self) -> crate::Result<()>; + fn end(&self); /// Sets the status of the current span. /// # Arguments @@ -118,14 +133,10 @@ pub trait Span: AsAny { /// # Returns /// A `Result` indicating success or failure of the operation. /// - fn set_status(&self, status: SpanStatus) -> crate::Result<()>; + fn set_status(&self, status: SpanStatus); /// Sets an attribute on the current span. - fn set_attribute( - &self, - key: &'static str, - value: attributes::AttributeValue, - ) -> crate::Result<()>; + fn set_attribute(&self, key: &'static str, value: attributes::AttributeValue); /// Records a Rust standard error on the current span. /// @@ -135,7 +146,7 @@ pub trait Span: AsAny { /// # Returns /// A `Result` indicating success or failure of the operation. /// - fn record_error(&self, error: &dyn std::error::Error) -> crate::Result<()>; + fn record_error(&self, error: &dyn std::error::Error); /// Temporarily sets the span as the current active span in the context. /// # Arguments From c72cc32f9c912dcfb98b88c7ea2b9903a5cf0578 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Thu, 26 Jun 2025 16:24:50 -0700 Subject: [PATCH 04/84] Telemetry static strings again --- .../http/policies/request_instrumentation.rs | 20 +++++++++---------- .../azure_core_opentelemetry/src/telemetry.rs | 8 ++++---- .../azure_core_opentelemetry/src/tracer.rs | 10 +++++----- .../typespec_client_core/src/http/method.rs | 2 +- .../src/http/request/mod.rs | 19 ++++++++++++++++-- .../typespec_client_core/src/tracing/mod.rs | 17 ++++++++++++---- 6 files changed, 49 insertions(+), 27 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 9785269650..16ae755205 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -31,10 +31,10 @@ pub struct RequestInstrumentationPolicy { tracer: Option>, } -impl<'a> RequestInstrumentationPolicy { +impl RequestInstrumentationPolicy { pub fn new( - crate_name: Option<&'a str>, - crate_version: Option<&'a str>, + crate_name: Option<&'static str>, + crate_version: Option<&'static str>, options: Option<&RequestInstrumentationOptions>, ) -> Self { if let Some(tracing_provider) = options.and_then(|o| o.tracing_provider.clone()) { @@ -119,21 +119,19 @@ impl Policy for RequestInstrumentationPolicy { value: port.into(), }); } + // Get the method as a string to avoid lifetime issues + let method_str = request.method_as_str(); let span = if let Some(parent_span) = ctx.value::>() { // If a parent span exists, start a new span with the parent. tracer.start_span_with_parent( - request.method().to_string().as_str(), + method_str, SpanKind::Client, span_attributes, parent_span.clone(), ) } else { // If no parent span exists, start a new span without a parent. - tracer.start_span_with_current( - request.method().to_string().as_str(), - SpanKind::Client, - span_attributes, - ) + tracer.start_span_with_current(method_str, SpanKind::Client, span_attributes) }; if let Some(client_request_id) = request @@ -364,8 +362,8 @@ mod tests { } async fn run_instrumentation_test( - crate_name: Option<&str>, - version: Option<&str>, + crate_name: Option<&'static str>, + version: Option<&'static str>, request: &mut Request, ) -> Arc { let mock_tracer = Arc::new(MockTracingProvider::new()); diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index dbe9cbddba..8231bf7739 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -26,11 +26,11 @@ impl OpenTelemetryTracerProvider { impl TracerProvider for OpenTelemetryTracerProvider { fn get_tracer( &self, - name: &str, - package_version: &str, + name: &'static str, + package_version: &'static str, ) -> Arc { - let scope = InstrumentationScope::builder(name.to_owned()) - .with_version(package_version.to_owned()) + let scope = InstrumentationScope::builder(name) + .with_version(package_version) .build(); Arc::new(OpenTelemetryTracer::new(BoxedTracer::new( self.inner.boxed_tracer(scope), diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 8cc8bfc4a9..d4aa7afc69 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -28,11 +28,11 @@ impl OpenTelemetryTracer { impl Tracer for OpenTelemetryTracer { fn start_span( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()) .with_attributes( attributes @@ -47,11 +47,11 @@ impl Tracer for OpenTelemetryTracer { fn start_span_with_current( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()) .with_attributes( attributes @@ -66,7 +66,7 @@ impl Tracer for OpenTelemetryTracer { fn start_span_with_parent( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, parent: Arc, diff --git a/sdk/typespec/typespec_client_core/src/http/method.rs b/sdk/typespec/typespec_client_core/src/http/method.rs index 3c185d66af..2803fb9952 100644 --- a/sdk/typespec/typespec_client_core/src/http/method.rs +++ b/sdk/typespec/typespec_client_core/src/http/method.rs @@ -194,7 +194,7 @@ impl<'a> std::convert::TryFrom<&'a str> for Method { } impl AsRef for Method { - fn as_ref(&self) -> &str { + fn as_ref(&self) -> &'static str { match self { Self::Delete => "DELETE", Self::Get => "GET", diff --git a/sdk/typespec/typespec_client_core/src/http/request/mod.rs b/sdk/typespec/typespec_client_core/src/http/request/mod.rs index dc38355600..ab2549d0e0 100644 --- a/sdk/typespec/typespec_client_core/src/http/request/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/request/mod.rs @@ -145,8 +145,23 @@ impl Request { &self.method } - /// Inserts zero or more headers from a type that implements [`AsHeaders`]. - pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { + /// Returns the HTTP method as a static string. + /// + /// This is not generally useful and should be avoided in favor of using the ['Request::method()'] method. + #[doc(hidden)] + pub fn method_as_str(&self) -> &'static str { + match self.method { + Method::Delete => "DELETE", + Method::Get => "GET", + Method::Head => "HEAD", + Method::Patch => "PATCH", + Method::Post => "POST", + Method::Put => "PUT", + } + } + + /// Inserts zero or more headers from a type that implements [`AsHeaders`]. + pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { for (name, value) in headers.as_headers()? { self.insert_header(name, value); } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index f901ea4e27..23349e8a24 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -31,7 +31,11 @@ pub trait TracerProvider: Send + Sync { /// Arguments: /// - `package_name`: The name of the package for which the tracer is requested. /// - `package_version`: The version of the package for which the tracer is requested. - fn get_tracer(&self, package_name: &str, package_version: &str) -> Arc; + fn get_tracer( + &self, + package_name: &'static str, + package_version: &'static str, + ) -> Arc; } impl Debug for dyn TracerProvider { @@ -50,7 +54,12 @@ pub trait Tracer: Send + Sync { /// # Returns /// An `Arc` representing the started span. /// - fn start_span(&self, name: &str, kind: SpanKind, attributes: Vec) -> Arc; + fn start_span( + &self, + name: &'static str, + kind: SpanKind, + attributes: Vec, + ) -> Arc; /// Starts a new span with the given type, using the current span as the parent span. /// @@ -63,7 +72,7 @@ pub trait Tracer: Send + Sync { /// fn start_span_with_current( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, ) -> Arc; @@ -82,7 +91,7 @@ pub trait Tracer: Send + Sync { /// fn start_span_with_parent( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, parent: Arc, From db6c3523b71719fb7020775826bca9243c71bda0 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Thu, 26 Jun 2025 16:35:48 -0700 Subject: [PATCH 05/84] http::Method as_str() --- .../src/http/policies/request_instrumentation.rs | 3 ++- sdk/typespec/typespec_client_core/src/http/method.rs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 16ae755205..1911b30d53 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -120,7 +120,8 @@ impl Policy for RequestInstrumentationPolicy { }); } // Get the method as a string to avoid lifetime issues - let method_str = request.method_as_str(); + // let method_str = request.method_as_str(); + let method_str = request.method().as_str(); let span = if let Some(parent_span) = ctx.value::>() { // If a parent span exists, start a new span with the parent. tracer.start_span_with_parent( diff --git a/sdk/typespec/typespec_client_core/src/http/method.rs b/sdk/typespec/typespec_client_core/src/http/method.rs index 2803fb9952..40b559b691 100644 --- a/sdk/typespec/typespec_client_core/src/http/method.rs +++ b/sdk/typespec/typespec_client_core/src/http/method.rs @@ -110,6 +110,18 @@ impl Method { pub fn is_safe(&self) -> bool { matches!(self, Method::Get | Method::Head) } + + /// Returns the HTTP method as a static string slice. + pub fn as_str(&self) -> &'static str { + match self { + Method::Delete => "DELETE", + Method::Get => "GET", + Method::Head => "HEAD", + Method::Patch => "PATCH", + Method::Post => "POST", + Method::Put => "PUT", + } + } } #[cfg(any(feature = "json", feature = "xml"))] From 16b26b8a33bb4d2dcfcd46379b7c4769958f8bdf Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Sat, 28 Jun 2025 09:30:14 -0700 Subject: [PATCH 06/84] Added service tracing spans; populate az.namespace --- sdk/core/azure_core/src/http/options/mod.rs | 15 +- sdk/core/azure_core/src/http/pipeline.rs | 37 +- .../http/policies/request_instrumentation.rs | 243 ++++++++++--- sdk/core/azure_core_opentelemetry/README.md | 14 - sdk/core/azure_core_opentelemetry/src/span.rs | 38 ++- .../azure_core_opentelemetry/src/telemetry.rs | 13 +- .../azure_core_opentelemetry/src/tracer.rs | 18 +- .../tests/integration_test.rs | 6 +- .../tests/telemetry_service_implementation.rs | 320 ++++++++++++++++-- .../typespec_client_core/src/tracing/mod.rs | 9 +- 10 files changed, 603 insertions(+), 110 deletions(-) diff --git a/sdk/core/azure_core/src/http/options/mod.rs b/sdk/core/azure_core/src/http/options/mod.rs index 70d8b6eded..fd2e0e835e 100644 --- a/sdk/core/azure_core/src/http/options/mod.rs +++ b/sdk/core/azure_core/src/http/options/mod.rs @@ -33,13 +33,18 @@ pub struct ClientOptions { pub request_instrumentation: Option, } +pub(crate) struct CoreClientOptions { + pub(crate) user_agent: UserAgentOptions, + pub(crate) request_instrumentation: RequestInstrumentationOptions, +} + impl ClientOptions { /// Efficiently deconstructs into owned [`typespec_client_core::http::ClientOptions`] as well as unwrapped or default Azure-specific options. /// /// If instead we implemented [`Into`], we'd have to clone Azure-specific options instead of moving memory of [`Some`] values. pub(in crate::http) fn deconstruct( self, - ) -> (UserAgentOptions, typespec_client_core::http::ClientOptions) { + ) -> (CoreClientOptions, typespec_client_core::http::ClientOptions) { let options = typespec_client_core::http::ClientOptions { per_call_policies: self.per_call_policies, per_try_policies: self.per_try_policies, @@ -47,6 +52,12 @@ impl ClientOptions { transport: self.transport, }; - (self.user_agent.unwrap_or_default(), options) + ( + CoreClientOptions { + user_agent: self.user_agent.unwrap_or_default(), + request_instrumentation: self.request_instrumentation.unwrap_or_default(), + }, + options, + ) } } diff --git a/sdk/core/azure_core/src/http/pipeline.rs b/sdk/core/azure_core/src/http/pipeline.rs index ce69465631..7358e5d65b 100644 --- a/sdk/core/azure_core/src/http/pipeline.rs +++ b/sdk/core/azure_core/src/http/pipeline.rs @@ -3,7 +3,7 @@ use super::policies::ClientRequestIdPolicy; use crate::http::{ - policies::{Policy, UserAgentPolicy}, + policies::{Policy, RequestInstrumentationPolicy, UserAgentPolicy}, ClientOptions, }; use std::{ @@ -51,9 +51,38 @@ impl Pipeline { let mut per_call_policies = per_call_policies.clone(); push_unique(&mut per_call_policies, ClientRequestIdPolicy::default()); - let (user_agent, options) = options.deconstruct(); - let telemetry_policy = UserAgentPolicy::new(crate_name, crate_version, &user_agent); - push_unique(&mut per_call_policies, telemetry_policy); + let (core_client_options, options) = options.deconstruct(); + let user_agent_policy = + UserAgentPolicy::new(crate_name, crate_version, &core_client_options.user_agent); + push_unique(&mut per_call_policies, user_agent_policy); + + + let mut per_try_policies = per_try_policies.clone(); + if core_client_options + .request_instrumentation + .tracing_provider + .is_some() + { + // Note that the choice to use "None" as the namespace here + // is intentional. + // The `azure_namespace` parameter is used to populate the `az.namespace` + // span attribute, however that information is only known by the author of the + // client library, not the core library. + // It is also *not* a constant that can be derived from the crate information - + // it is a value that is determined from the list of resource providers + // listed [here](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers). + // + // This information can only come from the package owner. It doesn't make sense + // to burden all users of the azure_core pipeline with determining this + // information, so we use `None` here. + let request_instrumentation_policy = RequestInstrumentationPolicy::new( + None, + crate_name, + crate_version, + &core_client_options.request_instrumentation, + ); + push_unique(&mut per_try_policies, request_instrumentation_policy); + } Self(http::Pipeline::new( options, diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 1911b30d53..81a84dd6d8 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -11,9 +11,7 @@ use typespec_client_core::{ tracing::Attribute, }; -#[allow(dead_code)] const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; - const AZ_SCHEMA_URL_ATTRIBUTE: &str = "az.schema.url"; const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client.request.id"; const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; @@ -25,27 +23,47 @@ const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; const SERVER_PORT_ATTRIBUTE: &str = "server.port"; const URL_FULL_ATTRIBUTE: &str = "url.full"; -/// Sets the User-Agent header with useful information in a typical format for Azure SDKs. +/// Sets distributed tracing information for HTTP requests. #[derive(Clone, Debug)] pub struct RequestInstrumentationPolicy { tracer: Option>, } impl RequestInstrumentationPolicy { + /// Creates a new `RequestInstrumentationPolicy`. + /// + /// # Arguments + /// - `azure_namespace`: The Azure namespace for the tracer. + /// - `crate_name`: The name of the crate for which the tracer is created. + /// - `crate_version`: The version of the crate for which the tracer is created. + /// - `options`: Options for request instrumentation, including the tracing provider. + /// + /// # Returns + /// A new instance of `RequestInstrumentationPolicy`. + /// + /// # Note + /// This policy will only create a tracer if a tracing provider is provided in the options. + /// + /// This policy will create a tracer that can be used to instrument HTTP requests. + /// However this tracer is only used when the client method is NOT instrumented. + /// A part of the client method instrumentation sets a client-specific tracer into the + /// request `[Context]` which will be used instead of the tracer from this policy. + /// pub fn new( + azure_namespace: Option<&'static str>, crate_name: Option<&'static str>, crate_version: Option<&'static str>, - options: Option<&RequestInstrumentationOptions>, + options: &RequestInstrumentationOptions, ) -> Self { - if let Some(tracing_provider) = options.and_then(|o| o.tracing_provider.clone()) { + if let Some(tracing_provider) = &options.tracing_provider { Self { tracer: Some(tracing_provider.get_tracer( + azure_namespace.unwrap_or("Unknown"), crate_name.unwrap_or("unknown"), crate_version.unwrap_or("unknown"), )), } } else { - // If no tracing provider is set, we return a policy with no tracer. Self { tracer: None } } } @@ -60,12 +78,26 @@ impl Policy for RequestInstrumentationPolicy { request: &mut Request, next: &[Arc], ) -> PolicyResult { - if let Some(tracer) = &self.tracer { + // If the context has a tracer (which happens when called from an instrumented method), + // we prefer the tracer from the context. + // Otherwise, we use the tracer from the policy itself. + // This allows for flexibility in using different tracers in different contexts. + let tracer = if ctx.value::>().is_some() { + ctx.value::>() + } else { + self.tracer.as_ref() + }; + + if let Some(tracer) = tracer { let mut span_attributes = vec![ Attribute { key: HTTP_REQUEST_METHOD_ATTRIBUTE, value: request.method().to_string().into(), }, + Attribute { + key: AZ_NAMESPACE_ATTRIBUTE, + value: tracer.namespace().into(), + }, Attribute { key: AZ_SCHEMA_URL_ATTRIBUTE, value: request.url().scheme().into(), @@ -191,33 +223,17 @@ impl Policy for RequestInstrumentationPolicy { #[cfg(test)] mod tests { use super::*; - use std::sync::{Arc, Mutex}; - use typespec_client_core::{ + use crate::{ http::{ headers::Headers, policies::TransportPolicy, Method, RawResponse, StatusCode, TransportOptions, }, tracing::{AsAny, AttributeValue, Span, SpanStatus, Tracer, TracerProvider}, + Result, }; - - #[derive(Debug)] - struct MockTransport; - - #[async_trait::async_trait] - impl Policy for MockTransport { - async fn send( - &self, - _ctx: &Context, - _request: &mut Request, - _next: &[Arc], - ) -> PolicyResult { - PolicyResult::Ok(RawResponse::from_bytes( - StatusCode::Ok, - Headers::new(), - Vec::new(), - )) - } - } + use azure_core_test::http::MockHttpClient; + use futures::future::BoxFuture; + use std::sync::{Arc, Mutex}; #[derive(Debug)] struct MockTracingProvider { @@ -234,13 +250,15 @@ mod tests { impl TracerProvider for MockTracingProvider { fn get_tracer( &self, - crate_name: &str, - crate_version: &str, + azure_namespace: &'static str, + crate_name: &'static str, + crate_version: &'static str, ) -> Arc { let mut tracers = self.tracers.lock().unwrap(); let tracer = Arc::new(MockTracer { - name: crate_name.to_string(), - version: crate_version.to_string(), + namespace: azure_namespace, + name: crate_name, + version: crate_version, spans: Mutex::new(Vec::new()), }); @@ -251,12 +269,17 @@ mod tests { #[derive(Debug)] struct MockTracer { - name: String, - version: String, + namespace: &'static str, + name: &'static str, + version: &'static str, spans: Mutex>>, } impl Tracer for MockTracer { + fn namespace(&self) -> &'static str { + self.namespace + } + fn start_span_with_current( &self, name: &str, @@ -362,23 +385,30 @@ mod tests { } } - async fn run_instrumentation_test( + async fn run_instrumentation_test( + test_namespace: Option<&'static str>, crate_name: Option<&'static str>, version: Option<&'static str>, request: &mut Request, - ) -> Arc { + callback: C, + ) -> Arc + where + C: FnMut(&Request) -> BoxFuture<'_, Result> + Send + Sync + 'static, + { let mock_tracer = Arc::new(MockTracingProvider::new()); let options = RequestInstrumentationOptions { tracing_provider: Some(mock_tracer.clone()), }; let policy = Arc::new(RequestInstrumentationPolicy::new( + test_namespace, crate_name, version, - Some(&options), + &options, )); - let transport = - TransportPolicy::new(TransportOptions::new_custom_policy(Arc::new(MockTransport))); + let transport = TransportPolicy::new(TransportOptions::new(Arc::new(MockHttpClient::new( + callback, + )))); let ctx = Context::default(); let next: Vec> = vec![Arc::new(transport)]; @@ -388,9 +418,11 @@ mod tests { } fn check_instrumentation_result( mock_tracer: Arc, + expected_namespace: &str, expected_name: &str, expected_version: &str, expected_method: &str, + expected_status: SpanStatus, expected_attributes: Vec<(&str, AttributeValue)>, ) { assert_eq!( @@ -402,11 +434,13 @@ mod tests { let tracer = tracers.first().unwrap(); assert_eq!(tracer.name, expected_name); assert_eq!(tracer.version, expected_version); + assert_eq!(tracer.namespace, expected_namespace); let spans = tracer.spans.lock().unwrap(); assert_eq!(spans.len(), 1, "Expected one span to be created"); println!("Spans: {:?}", spans); let span = spans.first().unwrap(); assert_eq!(span.name, expected_method); + assert_eq!(*span.state.lock().unwrap(), expected_status); let attributes = span.attributes.lock().unwrap(); for attr in attributes.iter() { println!("Attribute: {} = {:?}", attr.key, attr.value); @@ -437,15 +471,37 @@ mod tests { let url = "http://example.com/path"; let mut request = Request::new(url.parse().unwrap(), Method::Get); - let mock_tracer = - run_instrumentation_test(Some("test_crate"), Some("1.0.0"), &mut request).await; + let mock_tracer = run_instrumentation_test( + Some("test namespace"), + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; check_instrumentation_result( mock_tracer, + "test namespace", "test_crate", "1.0.0", "GET", + SpanStatus::Unset, vec![ + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("http")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, @@ -471,15 +527,37 @@ mod tests { let mut request = Request::new(url.parse().unwrap(), Method::Get); request.insert_header(headers::CLIENT_REQUEST_ID, "test-client-request-id"); - let mock_tracer = - run_instrumentation_test(Some("test_crate"), Some("1.0.0"), &mut request).await; + let mock_tracer = run_instrumentation_test( + Some("test namespace"), + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; check_instrumentation_result( mock_tracer.clone(), + "test namespace", "test_crate", "1.0.0", "GET", + SpanStatus::Unset, vec![ + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), ( AZ_CLIENT_REQUEST_ID_ATTRIBUTE, @@ -508,14 +586,29 @@ mod tests { let url = "https://user:password@host:8080/path?query=value#fragment"; let mut request = Request::new(url.parse().unwrap(), Method::Get); - let mock_tracer_provider = run_instrumentation_test(None, None, &mut request).await; + let mock_tracer_provider = + run_instrumentation_test(None, None, None, &mut request, |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("host")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }) + .await; check_instrumentation_result( mock_tracer_provider, + "Unknown", "unknown", "unknown", "GET", + SpanStatus::Unset, vec![ + (AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("Unknown")), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, @@ -531,4 +624,66 @@ mod tests { ], ); } + + #[tokio::test] + async fn request_failed() { + let url = "https://microsoft.com/request_failed.htm"; + let mut request = Request::new(url.parse().unwrap(), Method::Put); + request.insert_header(headers::REQUEST_ID, "test-service-request-id"); + + let mock_tracer = run_instrumentation_test( + Some("test namespace"), + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("microsoft.com")); + assert_eq!(req.method(), &Method::Put); + Ok(RawResponse::from_bytes( + StatusCode::NotFound, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; + + check_instrumentation_result( + mock_tracer.clone(), + "test namespace", + "test_crate", + "1.0.0", + "PUT", + SpanStatus::Error { + description: "HTTP request failed with status code 404: Not Found".to_string(), + }, + vec![ + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), + (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + ( + AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-service-request-id"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(404), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("PUT")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("microsoft.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://microsoft.com/request_failed.htm"), + ), + ], + ); + } } diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index f2679a1302..10f0460e09 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -1,17 +1,3 @@ # Azure Core OpenTelemetry Tracing This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. It enables automatic span creation, context propagation, and telemetry collection for Azure service operations. - -## Features - -## Usage - -### Basic Setup - -### Creating Spans - -### Error Handling - -## Azure Conventions - -## Integration diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 914e1a599e..b356617a39 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -123,6 +123,7 @@ mod tests { use opentelemetry_sdk::trace::{in_memory_exporter::InMemorySpanExporter, SdkTracerProvider}; use std::io::{Error, ErrorKind}; use std::sync::Arc; + use tracing::trace; fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { let otel_exporter = InMemorySpanExporter::default(); @@ -139,7 +140,9 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Microsoft.SpecialCase", "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); span.end(); @@ -158,7 +161,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Special Name", "test", "0.1.0"); let parent_span = tracer.start_span("parent_span", SpanKind::Server, vec![]); let child_span = tracer.start_span_with_parent( "child_span", @@ -190,7 +195,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("MyNamespace", "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let child_span = @@ -220,7 +227,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Namespace", "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let _span_guard = span2 @@ -252,7 +261,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("ThisNamespace", "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.set_attribute("test_key", AttributeValue::String("test_value".to_string())); @@ -276,7 +287,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("namespace", "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let error = Error::new(ErrorKind::NotFound, "resource not found"); @@ -303,7 +316,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Namespace", "test", "0.1.0"); // Test Ok status let span = tracer.start_span("test_span_ok", SpanKind::Server, vec![]); @@ -335,7 +350,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Namespace", "test", "0.1.0"); let future = async { let context = Context::current(); @@ -371,13 +388,14 @@ mod tests { let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); for span in &spans { + trace!("Span: {:?}", span); assert_eq!(span.name, "test_span"); assert_eq!(span.events.len(), 1); - assert_eq!(span.attributes.len(), 1); + assert_eq!(span.attributes.len(), 2); assert_eq!(span.attributes[0].key, "test_key".into()); assert_eq!( format!("{:?}", span.attributes[0].value), - "String(Static(\"test_value\"))" + "String(Owned(\"test_value\"))" ); } } diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index 8231bf7739..9a48b08154 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -26,15 +26,18 @@ impl OpenTelemetryTracerProvider { impl TracerProvider for OpenTelemetryTracerProvider { fn get_tracer( &self, - name: &'static str, + namespace: &'static str, + package_name: &'static str, package_version: &'static str, ) -> Arc { - let scope = InstrumentationScope::builder(name) + let scope = InstrumentationScope::builder(package_name) .with_version(package_version) + .with_schema_url("https://opentelemetry.io/schemas/1.23.0") .build(); - Arc::new(OpenTelemetryTracer::new(BoxedTracer::new( - self.inner.boxed_tracer(scope), - ))) + Arc::new(OpenTelemetryTracer::new( + namespace, + BoxedTracer::new(self.inner.boxed_tracer(scope)), + )) } } diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index d4aa7afc69..80fd6f6716 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -15,17 +15,25 @@ use opentelemetry::{ use std::sync::Arc; pub struct OpenTelemetryTracer { + namespace: &'static str, inner: BoxedTracer, } impl OpenTelemetryTracer { /// Creates a new OpenTelemetry tracer with the given inner tracer. - pub(super) fn new(tracer: BoxedTracer) -> Self { - Self { inner: tracer } + pub(super) fn new(namespace: &'static str, tracer: BoxedTracer) -> Self { + Self { + namespace, + inner: tracer, + } } } impl Tracer for OpenTelemetryTracer { + fn namespace(&self) -> &'static str { + self.namespace + } + fn start_span( &self, name: &'static str, @@ -104,7 +112,7 @@ mod tests { fn test_create_tracer() { let noop_tracer = NoopTracerProvider::new(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); - let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer("name", "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); } @@ -113,14 +121,14 @@ mod tests { fn test_create_tracer_with_sdk_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); - let _tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); + let _tracer = otel_provider.get_tracer("My.Namespace", "test_tracer", "1.0.0"); } #[test] fn test_create_span_from_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); - let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer("My.Namespace", "test_tracer", "1.0.0"); let _span = tracer.start_span("test_span", SpanKind::Internal, vec![]); } } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index b9cc00973b..af36fcd452 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -14,7 +14,7 @@ async fn test_span_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer("test_namespace", "test_tracer", "1.0.0"); // Create a span using the Azure tracer let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); @@ -42,7 +42,7 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer and verify it works - let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer("tes.namespace", "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); @@ -56,7 +56,7 @@ async fn test_span_attributes() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer("test.namespace", "test_tracer", "1.0.0"); // Create span with multiple attributes let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 911a50919e..c3a2ac8b8a 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -7,10 +7,10 @@ use azure_core::{ credentials::TokenCredential, fmt::SafeDebug, http::{ - policies::{BearerTokenCredentialPolicy, Policy}, ClientMethodOptions, ClientOptions, Pipeline, RawResponse, Request, RequestInstrumentationOptions, Url, }, + tracing::{Attribute, Tracer}, Result, }; use azure_core_opentelemetry::OpenTelemetryTracerProvider; @@ -27,6 +27,7 @@ pub struct TestServiceClient { endpoint: Url, api_version: String, pipeline: Pipeline, + tracer: Option>, } #[derive(Default, SafeDebug)] @@ -37,7 +38,7 @@ pub struct TestServiceClientGetMethodOptions<'a> { impl TestServiceClient { pub fn new( endpoint: &str, - credential: Arc, + _credential: Arc, options: Option, ) -> Result { let options = options.unwrap_or_default(); @@ -49,20 +50,23 @@ impl TestServiceClient { )); } endpoint.set_query(None); - let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( - credential, - vec!["https://vault.azure.net/.default"], - )); - let request_instrumentation_policy = Arc::new( - azure_core::http::policies::RequestInstrumentationPolicy::new( - option_env!("CARGO_PKG_NAME"), - option_env!("CARGO_PKG_VERSION"), - options - .azure_client_options - .request_instrumentation - .as_ref(), - ), - ); + + let tracer = + if let Some(tracer_options) = &options.azure_client_options.request_instrumentation { + tracer_options + .tracing_provider + .as_ref() + .map(|tracing_provider| { + tracing_provider.get_tracer( + "Az.TestServiceClient", + option_env!("CARGO_PKG_NAME").unwrap_or("test_service_client"), + option_env!("CARGO_PKG_VERSION").unwrap_or("0.1.0"), + ) + }) + } else { + None + }; + Ok(Self { endpoint, api_version: options.api_version, @@ -71,8 +75,9 @@ impl TestServiceClient { option_env!("CARGO_PKG_VERSION"), options.azure_client_options, Vec::default(), - vec![auth_policy, request_instrumentation_policy], + Vec::default(), ), + tracer, }) } @@ -81,6 +86,11 @@ impl TestServiceClient { &self.endpoint } + /// Returns the result of a Get verb against the configured endpoint with the specified path. + /// + /// This method demonstrates a service client which does not have per-method spans but which will create + /// HTTP client spans if the `RequestInstrumentationOptions` are configured in the client options. + /// pub async fn get( &self, path: &str, @@ -109,6 +119,60 @@ impl TestServiceClient { } Ok(response) } + + /// Returns the result of a Get verb against the configured endpoint with the specified path. + /// + /// This method demonstrates a service client which has per-method spans and uses the configured tracing + /// tracing provider to create per-api spans for the function. + /// + /// To configure per-api spans, your service implementation needs to do the following: + /// 1. If the client is configured with a [`Tracer`], it will create a span whose name matches the function. + /// 1. The span should be created with the `SpanKind::Internal` kind, and + /// 2. The span should have the `az.namespace` attribute set to the namespace of the service client. + /// 2. The function should add the span created in step 1 to the ClientMethodOptions context. + /// 3. The function should add the tracer to the ClientMethodOptions context so that the pipeline can use it to populate the `az.namespace` property in the request span. + /// 4. The function should then perform the normal client operations after setting up the context. + /// 5. After the client operation completes, if the function failed, it should add an `error.type` attribute to the span + /// with the error type. + /// + /// # Note + /// This applies to most HTTP client operations, but not all. CosmosDB has its own set of conventions as listed + /// [here](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/cosmosdb.md) + /// + pub async fn get_with_function_tracing( + &self, + path: &str, + options: Option>, + ) -> Result { + let mut options = options.unwrap_or_default(); + let mut ctx = options.method_options.context.clone(); + let span = if let Some(tracer) = &self.tracer { + let span = tracer.start_span( + "get_with_tracing", + azure_core::tracing::SpanKind::Internal, + vec![Attribute { + key: "az.namespace", + value: tracer.namespace().into(), + }], + ); + // We need to add the span to the context because the pipeline will use it as the parent span + // for the request span. + ctx = ctx.with_value(span.clone()); + // And we need to add the tracer to the context so that the pipeline can use it to populate the + // az.namespace property in the request span. + ctx = ctx.with_value(tracer.clone()); + Some(span) + } else { + None + }; + options.method_options.context = ctx; + let response = self.get(path, Some(options)).await; + if let Some(span) = span { + span.set_status(azure_core::tracing::SpanStatus::Ok); + span.end(); + }; + response + } } #[cfg(test)] @@ -116,6 +180,10 @@ mod tests { use super::*; use azure_core::Result; use azure_core_test::{recorded, TestContext}; + use opentelemetry::trace::{ + SpanKind as OpenTelemetrySpanKind, Status as OpenTelemetrySpanStatus, + }; + use opentelemetry::Value as OpenTelemetryAttributeValue; use tracing::{info, trace}; fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { @@ -127,6 +195,57 @@ mod tests { (otel_tracer_provider, otel_exporter) } + // Span verification utility functions. + + struct ExpectedSpan { + name: &'static str, + kind: OpenTelemetrySpanKind, + parent_span_id: Option, + status: OpenTelemetrySpanStatus, + attributes: Vec<(&'static str, OpenTelemetryAttributeValue)>, + } + + fn verify_span( + span: &opentelemetry_sdk::trace::SpanData, + expected: ExpectedSpan, + ) -> Result<()> { + assert_eq!(span.name, expected.name); + assert_eq!(span.span_kind, expected.kind); + assert_eq!(span.status, expected.status); + assert_eq!( + span.parent_span_id, + expected + .parent_span_id + .unwrap_or(opentelemetry::trace::SpanId::INVALID) + ); + + for attr in span.attributes.iter() { + println!("Attribute: {} = {:?}", attr.key, attr.value); + let mut found = false; + for (key, value) in expected.attributes.iter() { + if attr.key.as_str() == (*key) { + found = true; + // Skip checking the value for "" as it is a placeholder + if *value != OpenTelemetryAttributeValue::String("".into()) { + assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", *key); + } + break; + } + } + if !found { + panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); + } + } + for (key, value) in expected.attributes.iter() { + if !span.attributes.iter().any(|attr| attr.key == (*key).into()) { + panic!("Expected attribute not found: {} = {:?}", key, value); + } + } + + Ok(()) + } + + // Basic functionality tests. #[recorded::test()] async fn test_service_client_new(ctx: TestContext) -> Result<()> { let recording = ctx.recording(); @@ -144,17 +263,14 @@ mod tests { Ok(()) } + // Ensure that the the test client actually does what it's supposed to do without telemetry. #[recorded::test()] async fn test_service_client_get(ctx: TestContext) -> Result<()> { let recording = ctx.recording(); let endpoint = "https://example.com"; let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), - ..Default::default() - }; - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let client = TestServiceClient::new(endpoint, credential, None).unwrap(); let response = client.get("index.html", None).await; info!("Response: {:?}", response); assert!(response.is_ok()); @@ -192,8 +308,168 @@ mod tests { assert_eq!(spans.len(), 1); for span in &spans { trace!("Span: {:?}", span); + + verify_span( + span, + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + status: OpenTelemetrySpanStatus::Unset, + parent_span_id: None, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Unknown".into()), + ("az.schema.url", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "index.html?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend.count", 0.into()), + ("http.response.status_code", 200.into()), + ], + }, + )?; + } + + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("failing_url", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + for span in &spans { + trace!("Span: {:?}", span); + + verify_span( + span, + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Error { + description: "HTTP request failed with status code 404: Not Found".into(), + }, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Unknown".into()), + ("az.schema.url", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend.count", 0.into()), + ("http.response.status_code", 404.into()), + ], + }, + )?; } Ok(()) } + + #[recorded::test()] + async fn test_service_client_get_with_full_tracing(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get_with_function_tracing("index.html", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + for span in &spans { + trace!("Span: {:?}", span); + } + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[1].span_context.span_id()), + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.schema.url", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "index.html?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend.count", 0.into()), + ("http.response.status_code", 200.into()), + ], + }, + )?; + verify_span( + &spans[1], + ExpectedSpan { + name: "get_with_tracing", + kind: OpenTelemetrySpanKind::Internal, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Ok, + attributes: vec![("az.namespace", "Az.TestServiceClient".into())], + }, + )?; + + Ok(()) + } } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index 23349e8a24..b3eadd93fb 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -29,10 +29,14 @@ pub trait TracerProvider: Send + Sync { /// Returns a tracer for the given name. /// /// Arguments: + /// - `namespace_name`: The namespace of the package for which the tracer is requested. See + /// [this page](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers) + /// for more information on namespace names. /// - `package_name`: The name of the package for which the tracer is requested. /// - `package_version`: The version of the package for which the tracer is requested. fn get_tracer( &self, + namespace_name: &'static str, package_name: &'static str, package_version: &'static str, ) -> Arc; @@ -96,6 +100,9 @@ pub trait Tracer: Send + Sync { attributes: Vec, parent: Arc, ) -> Arc; + + /// Returns the namespace the tracer was configured with. + fn namespace(&self) -> &'static str; } impl Debug for dyn Tracer { @@ -104,7 +111,7 @@ impl Debug for dyn Tracer { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum SpanStatus { Unset, Ok, From e78523761beea114452c78f3303fdc27f2d44708 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 09:09:04 -0700 Subject: [PATCH 07/84] Added test recording for opentelemetry tests --- Cargo.lock | 2 -- sdk/core/azure_core_opentelemetry/Cargo.toml | 2 -- sdk/core/azure_core_opentelemetry/assets.json | 6 ++++++ 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 sdk/core/azure_core_opentelemetry/assets.json diff --git a/Cargo.lock b/Cargo.lock index 2c43faa147..9c861de421 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,11 +205,9 @@ dependencies = [ name = "azure_core_opentelemetry" version = "0.1.0" dependencies = [ - "async-trait", "azure_core", "azure_core_test", "azure_core_test_macros", - "log", "opentelemetry 0.30.0", "opentelemetry_sdk 0.30.0", "tokio", diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index b81e4e5f2d..ba2a0ab626 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -16,13 +16,11 @@ edition.workspace = true [dependencies] azure_core.workspace = true -log.workspace = true opentelemetry = { version = "0.30", features = ["trace"] } tracing.workspace = true typespec_client_core.workspace = true [dev-dependencies] -async-trait.workspace = true azure_core_test = { workspace = true, features = ["tracing"] } azure_core_test_macros.workspace = true opentelemetry_sdk = { version = "0.30", features = ["testing"] } diff --git a/sdk/core/azure_core_opentelemetry/assets.json b/sdk/core/azure_core_opentelemetry/assets.json new file mode 100644 index 0000000000..8f0a2844a2 --- /dev/null +++ b/sdk/core/azure_core_opentelemetry/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "rust", + "Tag": "", + "TagPrefix": "rust/azure_core_opentelemetry" +} \ No newline at end of file From 534b9505775df895d1c3a161f774b4692ba77040 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 09:19:29 -0700 Subject: [PATCH 08/84] cargo publish fix --- .../src/http/policies/request_instrumentation.rs | 2 ++ sdk/core/azure_core_opentelemetry/src/span.rs | 2 ++ .../typespec_client_core/src/tracing/attributes.rs | 2 +- sdk/typespec/typespec_client_core/src/tracing/mod.rs | 7 ++++--- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 81a84dd6d8..7911028f9d 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -377,6 +377,8 @@ mod tests { { todo!() } + + fn propagate_headers(&self, request: &mut Request) {} } impl AsAny for MockSpan { diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index b356617a39..6f32d7f160 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -77,6 +77,8 @@ impl Span for OpenTelemetrySpan { self.context.span().set_status(otel_status); } + fn propagate_headers(&self, _request: &mut azure_core::http::Request) {} + fn set_current( &self, _context: &azure_core::http::Context, diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index f0321a941c..73e3a0fb73 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::fmt::SafeDebug; +use typespec_macros::SafeDebug; /// An array of homogeneous attribute values. #[derive(SafeDebug, PartialEq)] diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index b3eadd93fb..f889cd19a2 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -3,9 +3,8 @@ //! Distributed tracing trait definitions //! -use crate::http::Context; -use std::fmt::Debug; -use std::sync::Arc; +use crate::http::{Context, Request}; +use std::{fmt::Debug, sync::Arc}; /// Overall architecture for distributed tracing in the SDK. /// @@ -175,6 +174,8 @@ pub trait Span: AsAny + Send + Sync { /// enabling it to be used for tracing operations within that context. /// fn set_current(&self, context: &Context) -> crate::Result>; + + fn propagate_headers(&self, request: &mut Request); } /// A trait that allows an object to be downcast to a reference of type `Any`. From 1da7fd78043a121b26f54baa65864045da533d3d Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 09:30:39 -0700 Subject: [PATCH 09/84] cargo publish fix 2 --- sdk/core/azure_core_opentelemetry/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index ba2a0ab626..8a4f7a498e 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -17,6 +17,7 @@ edition.workspace = true [dependencies] azure_core.workspace = true opentelemetry = { version = "0.30", features = ["trace"] } +opentelemetry-http = "0.30.0" tracing.workspace = true typespec_client_core.workspace = true From 726466e5edc09c19fa3f5967e15585220b0584c6 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 09:34:45 -0700 Subject: [PATCH 10/84] Oops, commited too little stuff --- Cargo.lock | 13 +++++++++++++ .../src/http/policies/request_instrumentation.rs | 2 +- .../typespec_client_core/src/tracing/attributes.rs | 12 ++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c861de421..30559179f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,7 @@ dependencies = [ "azure_core_test", "azure_core_test_macros", "opentelemetry 0.30.0", + "opentelemetry-http", "opentelemetry_sdk 0.30.0", "tokio", "tracing", @@ -1791,6 +1792,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry 0.30.0", +] + [[package]] name = "opentelemetry_sdk" version = "0.25.0" diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 7911028f9d..a38ef054f7 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -378,7 +378,7 @@ mod tests { todo!() } - fn propagate_headers(&self, request: &mut Request) {} + fn propagate_headers(&self, _request: &mut Request) {} } impl AsAny for MockSpan { diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 73e3a0fb73..00dc5c0442 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use typespec_macros::SafeDebug; +#[cfg(feature = "derive")] +use crate::fmt::SafeDebug; /// An array of homogeneous attribute values. -#[derive(SafeDebug, PartialEq)] +#[cfg_attr(feature = "derive", derive(SafeDebug))] +#[derive(PartialEq)] pub enum AttributeArray { /// An array of boolean values. Bool(Vec), @@ -17,7 +19,8 @@ pub enum AttributeArray { } /// Represents a single attribute value, which can be of various types -#[derive(SafeDebug, PartialEq)] +#[cfg_attr(feature = "derive", derive(SafeDebug))] +#[derive(PartialEq)] pub enum AttributeValue { /// A boolean attribute value. Bool(bool), @@ -30,7 +33,8 @@ pub enum AttributeValue { /// An array of attribute values. Array(AttributeArray), } -#[derive(SafeDebug)] + +#[cfg_attr(feature = "derive", derive(SafeDebug))] pub struct Attribute { /// A key-value pair attribute. pub key: &'static str, From 6d9c1433f6195788d392f17dfd93293ca3f75e44 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 11:52:16 -0700 Subject: [PATCH 11/84] OTel feedback from Liudmila --- .../http/policies/request_instrumentation.rs | 68 +++++---- sdk/core/azure_core_opentelemetry/src/span.rs | 32 ++-- .../azure_core_opentelemetry/src/telemetry.rs | 2 +- .../azure_core_opentelemetry/src/tracer.rs | 12 +- .../tests/integration_test.rs | 6 +- .../tests/telemetry_service_implementation.rs | 144 +++++++++++++++--- .../typespec_client_core/src/tracing/mod.rs | 13 +- 7 files changed, 199 insertions(+), 78 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index a38ef054f7..0b412361e6 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -58,7 +58,7 @@ impl RequestInstrumentationPolicy { if let Some(tracing_provider) = &options.tracing_provider { Self { tracer: Some(tracing_provider.get_tracer( - azure_namespace.unwrap_or("Unknown"), + azure_namespace, crate_name.unwrap_or("unknown"), crate_version.unwrap_or("unknown"), )), @@ -94,16 +94,20 @@ impl Policy for RequestInstrumentationPolicy { key: HTTP_REQUEST_METHOD_ATTRIBUTE, value: request.method().to_string().into(), }, - Attribute { - key: AZ_NAMESPACE_ATTRIBUTE, - value: tracer.namespace().into(), - }, Attribute { key: AZ_SCHEMA_URL_ATTRIBUTE, value: request.url().scheme().into(), }, ]; + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + span_attributes.push(Attribute { + key: AZ_NAMESPACE_ATTRIBUTE, + value: namespace.into(), + }); + } + if !request.url().username().is_empty() || request.url().password().is_some() { // If the URL contains a password, we do not log it for security reasons. let full_url = format!( @@ -186,12 +190,17 @@ impl Policy for RequestInstrumentationPolicy { let result = next[0].send(ctx, request, &next[1..]).await; - if result.is_err() { + if let Some(err) = result.as_ref().err() { // If the request failed, set an error type attribute. - span.set_attribute( - ERROR_TYPE_ATTRIBUTE, - result.as_ref().err().unwrap().to_string().into(), - ); + let azure_error = err.downcast_ref::(); + if let Some(err_kind) = azure_error.map(|e| e.kind()) { + // If the error is an Azure core error, we set the error type. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, err_kind.to_string().into()); + } else { + // Otherwise, we set the error type to the error's text. This should never happen + // as the error should be an Azure core error. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, err.to_string().into()); + } } if let Ok(response) = result.as_ref() { // If the request was successful, set the HTTP response status code. @@ -202,13 +211,12 @@ impl Policy for RequestInstrumentationPolicy { if response.status().is_server_error() || response.status().is_client_error() { // If the response status indicates an error, set the span status to error. + // Since the reason can be inferred from the status code, description is left empty. span.set_status(crate::tracing::SpanStatus::Error { - description: format!( - "HTTP request failed with status code {}: {}", - response.status(), - response.status().canonical_reason() - ), + description: "".to_string(), }); + // Set the error type attribute for all HTTP 4XX or 5XX errors. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); } } @@ -250,7 +258,7 @@ mod tests { impl TracerProvider for MockTracingProvider { fn get_tracer( &self, - azure_namespace: &'static str, + azure_namespace: Option<&'static str>, crate_name: &'static str, crate_version: &'static str, ) -> Arc { @@ -269,14 +277,14 @@ mod tests { #[derive(Debug)] struct MockTracer { - namespace: &'static str, + namespace: Option<&'static str>, name: &'static str, version: &'static str, spans: Mutex>>, } impl Tracer for MockTracer { - fn namespace(&self) -> &'static str { + fn namespace(&self) -> Option<&'static str> { self.namespace } @@ -420,7 +428,7 @@ mod tests { } fn check_instrumentation_result( mock_tracer: Arc, - expected_namespace: &str, + expected_namespace: Option<&str>, expected_name: &str, expected_version: &str, expected_method: &str, @@ -494,7 +502,7 @@ mod tests { check_instrumentation_result( mock_tracer, - "test namespace", + Some("test namespace"), "test_crate", "1.0.0", "GET", @@ -530,7 +538,7 @@ mod tests { request.insert_header(headers::CLIENT_REQUEST_ID, "test-client-request-id"); let mock_tracer = run_instrumentation_test( - Some("test namespace"), + None, Some("test_crate"), Some("1.0.0"), &mut request, @@ -550,16 +558,12 @@ mod tests { check_instrumentation_result( mock_tracer.clone(), - "test namespace", + None, "test_crate", "1.0.0", "GET", SpanStatus::Unset, vec![ - ( - AZ_NAMESPACE_ATTRIBUTE, - AttributeValue::from("test namespace"), - ), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), ( AZ_CLIENT_REQUEST_ID_ATTRIBUTE, @@ -604,13 +608,12 @@ mod tests { check_instrumentation_result( mock_tracer_provider, - "Unknown", + None, "unknown", "unknown", "GET", SpanStatus::Unset, vec![ - (AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("Unknown")), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, @@ -654,14 +657,19 @@ mod tests { check_instrumentation_result( mock_tracer.clone(), - "test namespace", + Some("test namespace"), "test_crate", "1.0.0", "PUT", SpanStatus::Error { - description: "HTTP request failed with status code 404: Not Found".to_string(), + description: "".to_string(), }, vec![ + (ERROR_TYPE_ATTRIBUTE, AttributeValue::from("404")), + ( + AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-service-request-id"), + ), ( AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("test namespace"), diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 6f32d7f160..b3abef4118 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -71,7 +71,6 @@ impl Span for OpenTelemetrySpan { fn set_status(&self, status: SpanStatus) { let otel_status = match status { SpanStatus::Unset => opentelemetry::trace::Status::Unset, - SpanStatus::Ok => opentelemetry::trace::Status::Ok, SpanStatus::Error { description } => opentelemetry::trace::Status::error(description), }; self.context.span().set_status(otel_status); @@ -142,9 +141,10 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer("Microsoft.SpecialCase", "test", "0.1.0"); + let tracer = + tracer_provider + .unwrap() + .get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); span.end(); @@ -165,7 +165,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("Special Name", "test", "0.1.0"); + .get_tracer(Some("Special Name"), "test", "0.1.0"); let parent_span = tracer.start_span("parent_span", SpanKind::Server, vec![]); let child_span = tracer.start_span_with_parent( "child_span", @@ -199,7 +199,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("MyNamespace", "test", "0.1.0"); + .get_tracer(Some("MyNamespace"), "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let child_span = @@ -231,7 +231,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("Namespace", "test", "0.1.0"); + .get_tracer(Some("Namespace"), "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let _span_guard = span2 @@ -265,7 +265,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("ThisNamespace", "test", "0.1.0"); + .get_tracer(Some("ThisNamespace"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.set_attribute("test_key", AttributeValue::String("test_value".to_string())); @@ -291,7 +291,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("namespace", "test", "0.1.0"); + .get_tracer(Some("namespace"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let error = Error::new(ErrorKind::NotFound, "resource not found"); @@ -320,11 +320,10 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("Namespace", "test", "0.1.0"); + .get_tracer(Some("Namespace"), "test", "0.1.0"); - // Test Ok status - let span = tracer.start_span("test_span_ok", SpanKind::Server, vec![]); - span.set_status(SpanStatus::Ok); + // Test Unset status + let span = tracer.start_span("test_span_unset", SpanKind::Server, vec![]); span.end(); // Test Error status @@ -337,14 +336,13 @@ mod tests { let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 2); - let ok_span = spans.iter().find(|s| s.name == "test_span_ok").unwrap(); - assert_eq!(ok_span.status, opentelemetry::trace::Status::Ok); - let error_span = spans.iter().find(|s| s.name == "test_span_error").unwrap(); assert_eq!( error_span.status, opentelemetry::trace::Status::error("test error") ); + let unset_span = spans.iter().find(|s| s.name == "test_span_unset").unwrap(); + assert_eq!(unset_span.status, opentelemetry::trace::Status::Unset); } #[tokio::test] @@ -354,7 +352,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("Namespace", "test", "0.1.0"); + .get_tracer(Some("Namespace"), "test", "0.1.0"); let future = async { let context = Context::current(); diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index 9a48b08154..36dec069c9 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -26,7 +26,7 @@ impl OpenTelemetryTracerProvider { impl TracerProvider for OpenTelemetryTracerProvider { fn get_tracer( &self, - namespace: &'static str, + namespace: Option<&'static str>, package_name: &'static str, package_version: &'static str, ) -> Arc { diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 80fd6f6716..6282f5cdb5 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -15,13 +15,13 @@ use opentelemetry::{ use std::sync::Arc; pub struct OpenTelemetryTracer { - namespace: &'static str, + namespace: Option<&'static str>, inner: BoxedTracer, } impl OpenTelemetryTracer { /// Creates a new OpenTelemetry tracer with the given inner tracer. - pub(super) fn new(namespace: &'static str, tracer: BoxedTracer) -> Self { + pub(super) fn new(namespace: Option<&'static str>, tracer: BoxedTracer) -> Self { Self { namespace, inner: tracer, @@ -30,7 +30,7 @@ impl OpenTelemetryTracer { } impl Tracer for OpenTelemetryTracer { - fn namespace(&self) -> &'static str { + fn namespace(&self) -> Option<&'static str> { self.namespace } @@ -112,7 +112,7 @@ mod tests { fn test_create_tracer() { let noop_tracer = NoopTracerProvider::new(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); - let tracer = otel_provider.get_tracer("name", "test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer(Some("name"), "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); } @@ -121,14 +121,14 @@ mod tests { fn test_create_tracer_with_sdk_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); - let _tracer = otel_provider.get_tracer("My.Namespace", "test_tracer", "1.0.0"); + let _tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); } #[test] fn test_create_span_from_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); - let tracer = otel_provider.get_tracer("My.Namespace", "test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); let _span = tracer.start_span("test_span", SpanKind::Internal, vec![]); } } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index af36fcd452..e6165d45a0 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -14,7 +14,7 @@ async fn test_span_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer("test_namespace", "test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer(Some("test_namespace"), "test_tracer", "1.0.0"); // Create a span using the Azure tracer let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); @@ -42,7 +42,7 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer and verify it works - let tracer = azure_provider.get_tracer("tes.namespace", "test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); @@ -56,7 +56,7 @@ async fn test_span_attributes() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer("test.namespace", "test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); // Create span with multiple attributes let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index c3a2ac8b8a..edd8e85128 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -17,10 +17,19 @@ use azure_core_opentelemetry::OpenTelemetryTracerProvider; use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider}; use std::sync::Arc; -#[derive(Default, Clone, SafeDebug)] +#[derive(Clone, SafeDebug)] pub struct TestServiceClientOptions { pub azure_client_options: ClientOptions, - pub api_version: String, + pub api_version: Option, +} + +impl Default for TestServiceClientOptions { + fn default() -> Self { + Self { + azure_client_options: ClientOptions::default(), + api_version: Some("2023-10-01".to_string()), + } + } } pub struct TestServiceClient { @@ -58,7 +67,7 @@ impl TestServiceClient { .as_ref() .map(|tracing_provider| { tracing_provider.get_tracer( - "Az.TestServiceClient", + Some("Az.TestServiceClient"), option_env!("CARGO_PKG_NAME").unwrap_or("test_service_client"), option_env!("CARGO_PKG_VERSION").unwrap_or("0.1.0"), ) @@ -69,7 +78,7 @@ impl TestServiceClient { Ok(Self { endpoint, - api_version: options.api_version, + api_version: options.api_version.unwrap_or_default(), pipeline: Pipeline::new( option_env!("CARGO_PKG_NAME"), option_env!("CARGO_PKG_VERSION"), @@ -147,13 +156,18 @@ impl TestServiceClient { let mut options = options.unwrap_or_default(); let mut ctx = options.method_options.context.clone(); let span = if let Some(tracer) = &self.tracer { + let mut attributes = Vec::new(); + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + attributes.push(Attribute { + key: "az.namespace", + value: namespace.into(), + }); + } let span = tracer.start_span( "get_with_tracing", azure_core::tracing::SpanKind::Internal, - vec![Attribute { - key: "az.namespace", - value: tracer.namespace().into(), - }], + attributes, ); // We need to add the span to the context because the pipeline will use it as the parent span // for the request span. @@ -168,7 +182,26 @@ impl TestServiceClient { options.method_options.context = ctx; let response = self.get(path, Some(options)).await; if let Some(span) = span { - span.set_status(azure_core::tracing::SpanStatus::Ok); + if let Err(e) = &response { + // If the request failed, we set the error type on the span. + match e.kind() { + azure_core::error::ErrorKind::HttpResponse { status, .. } => { + span.set_attribute("error.type", status.to_string().into()); + if status.is_server_error() || status.is_client_error() { + span.set_status(azure_core::tracing::SpanStatus::Error { + description: "".to_string(), + }); + } + } + _ => { + span.set_attribute("error.type", e.kind().to_string().into()); + span.set_status(azure_core::tracing::SpanStatus::Error { + description: e.kind().to_string(), + }); + } + } + } + span.end(); }; response @@ -252,7 +285,6 @@ mod tests { let endpoint = "https://example.com"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), ..Default::default() }; @@ -288,13 +320,13 @@ mod tests { let endpoint = "https://example.com"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), }), ..Default::default() }, + ..Default::default() }; let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); @@ -318,7 +350,6 @@ mod tests { parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("az.namespace", "Unknown".into()), ("az.schema.url", "https".into()), ("az.client.request.id", "".into()), ( @@ -351,13 +382,13 @@ mod tests { let endpoint = "https://example.com"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), }), ..Default::default() }, + ..Default::default() }; let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); @@ -376,11 +407,10 @@ mod tests { kind: OpenTelemetrySpanKind::Client, parent_span_id: None, status: OpenTelemetrySpanStatus::Error { - description: "HTTP request failed with status code 404: Not Found".into(), + description: "".into(), }, attributes: vec![ ("http.request.method", "GET".into()), - ("az.namespace", "Unknown".into()), ("az.schema.url", "https".into()), ("az.client.request.id", "".into()), ( @@ -394,6 +424,7 @@ mod tests { ), ("server.address", "example.com".into()), ("server.port", 443.into()), + ("error.type", "404".into()), ("http.request.resend.count", 0.into()), ("http.response.status_code", 404.into()), ], @@ -405,7 +436,7 @@ mod tests { } #[recorded::test()] - async fn test_service_client_get_with_full_tracing(ctx: TestContext) -> Result<()> { + async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); @@ -413,13 +444,13 @@ mod tests { let endpoint = "https://example.com"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), }), ..Default::default() }, + ..Default::default() }; let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); @@ -465,11 +496,88 @@ mod tests { name: "get_with_tracing", kind: OpenTelemetrySpanKind::Internal, parent_span_id: None, - status: OpenTelemetrySpanStatus::Ok, + status: OpenTelemetrySpanStatus::Unset, attributes: vec![("az.namespace", "Az.TestServiceClient".into())], }, )?; Ok(()) } + + #[recorded::test()] + async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get_with_function_tracing("failing_url", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + for span in &spans { + trace!("Span: {:?}", span); + } + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[1].span_context.span_id()), + status: OpenTelemetrySpanStatus::Error { + description: "".into(), + }, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.schema.url", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend.count", 0.into()), + ("http.response.status_code", 404.into()), + ("error.type", "404".into()), + ], + }, + )?; + verify_span( + &spans[1], + ExpectedSpan { + name: "get_with_tracing", + kind: OpenTelemetrySpanKind::Internal, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Error { + description: "".into(), + }, + attributes: vec![ + ("az.namespace", "Az.TestServiceClient".into()), + ("error.type", "404".into()), + ], + }, + )?; + + Ok(()) + } } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index f889cd19a2..bbb202dcd4 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -35,7 +35,7 @@ pub trait TracerProvider: Send + Sync { /// - `package_version`: The version of the package for which the tracer is requested. fn get_tracer( &self, - namespace_name: &'static str, + namespace_name: Option<&'static str>, package_name: &'static str, package_version: &'static str, ) -> Arc; @@ -101,7 +101,7 @@ pub trait Tracer: Send + Sync { ) -> Arc; /// Returns the namespace the tracer was configured with. - fn namespace(&self) -> &'static str; + fn namespace(&self) -> Option<&'static str>; } impl Debug for dyn Tracer { @@ -110,10 +110,17 @@ impl Debug for dyn Tracer { } } +/// The status of a span. +/// +/// This enum represents the possible statuses of a span in distributed tracing. +/// It can be either `Unset`, indicating that the span has not been set to any specific status, +/// or `Error`, which contains a description of the error that occurred during the span's execution +/// +/// Note that OpenTelemetry defines an `Ok` status but that status is reserved for application and service developers, +/// so libraries should never set it. #[derive(Debug, PartialEq)] pub enum SpanStatus { Unset, - Ok, Error { description: String }, } From 85de5a94d26c7a3598b4fbd392acf6d52a6c2323 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 15:17:45 -0700 Subject: [PATCH 12/84] Propagate OpenTelemetry trace headers into request headers --- Cargo.lock | 1 + .../http/policies/request_instrumentation.rs | 19 +++++- sdk/core/azure_core_opentelemetry/Cargo.toml | 3 + sdk/core/azure_core_opentelemetry/src/span.rs | 64 ++++++++++++++++++- 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30559179f8..098894732b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,7 @@ dependencies = [ "opentelemetry 0.30.0", "opentelemetry-http", "opentelemetry_sdk 0.30.0", + "reqwest", "tokio", "tracing", "tracing-opentelemetry", diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 0b412361e6..c27cd2e88c 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -188,6 +188,9 @@ impl Policy for RequestInstrumentationPolicy { span.set_attribute(HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, retry_count.0.into()); } + // Propagate the headers for distributed tracing into the request. + span.propagate_headers(request); + let result = next[0].send(ctx, request, &next[1..]).await; if let Some(err) = result.as_ref().err() { @@ -242,6 +245,7 @@ mod tests { use azure_core_test::http::MockHttpClient; use futures::future::BoxFuture; use std::sync::{Arc, Mutex}; + use typespec_client_core::http::headers::HeaderName; #[derive(Debug)] struct MockTracingProvider { @@ -386,7 +390,15 @@ mod tests { todo!() } - fn propagate_headers(&self, _request: &mut Request) {} + /// Insert two dummy headers for distributed tracing. + // cspell: ignore traceparent tracestate + fn propagate_headers(&self, request: &mut Request) { + request.insert_header( + HeaderName::from_static("traceparent"), + "00---01", + ); + request.insert_header(HeaderName::from_static("tracestate"), "="); + } } impl AsAny for MockSpan { @@ -546,6 +558,11 @@ mod tests { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); assert_eq!(req.method(), &Method::Get); + assert_eq!( + req.headers() + .get_optional_str(HeaderName::from_static("traceparent")), + Some("00---01") + ); Ok(RawResponse::from_bytes( StatusCode::Ok, Headers::new(), diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index 8a4f7a498e..e2e44bb4f5 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -18,9 +18,12 @@ edition.workspace = true azure_core.workspace = true opentelemetry = { version = "0.30", features = ["trace"] } opentelemetry-http = "0.30.0" +opentelemetry_sdk = "0.30" +reqwest.workspace = true tracing.workspace = true typespec_client_core.workspace = true + [dev-dependencies] azure_core_test = { workspace = true, features = ["tracing"] } azure_core_test_macros.workspace = true diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index b3abef4118..728f56f011 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -8,7 +8,10 @@ use azure_core::{ tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}, Result, }; -use opentelemetry::trace::TraceContextExt; +use opentelemetry::{propagation::TextMapPropagator, trace::TraceContextExt}; +use opentelemetry_http::HeaderInjector; +use opentelemetry_sdk::propagation::TraceContextPropagator; +use reqwest::header::HeaderMap; use std::{error::Error as StdError, sync::Arc}; /// newtype for Azure Core SpanKind to enable conversion to OpenTelemetry SpanKind @@ -76,7 +79,34 @@ impl Span for OpenTelemetrySpan { self.context.span().set_status(otel_status); } - fn propagate_headers(&self, _request: &mut azure_core::http::Request) {} + fn propagate_headers(&self, request: &mut azure_core::http::Request) { + // A TraceContextPropagator is used to inject trace context information into HTTP headers. + let trace_propagator = TraceContextPropagator::new(); + // We need to map between a reqwest header map (which is what the OpenTelemetry SDK requires) + // and the Azure Core request headers. + // + // We start with an empty header map and inject the OpenTelemetry headers into it. + let mut header_map = HeaderMap::new(); + trace_propagator.inject_context(&self.context, &mut HeaderInjector(&mut header_map)); + + // We then insert each of the headers from the OpenTelemetry header map into the + // Request's header map. + for (key, value) in header_map.into_iter() { + // Note: The OpenTelemetry HeaderInjector will always produce unique header names, so we don't need to + // handle the multiple headers case here. + if let Some(key) = key { + // Convert HeaderName to &str for insertion. + let value_str = value.to_str().unwrap().to_string(); + request.insert_header( + azure_core::http::headers::HeaderName::from(key.to_string()), + azure_core::http::headers::HeaderValue::from(value_str), + ); + } else { + // If the key is invalid, we skip it + tracing::warn!("Invalid header key: {:?}", key); + } + } + } fn set_current( &self, @@ -117,7 +147,7 @@ impl Drop for OpenTelemetrySpanGuard { #[cfg(test)] mod tests { use crate::telemetry::OpenTelemetryTracerProvider; - use azure_core::http::Context as AzureContext; + use azure_core::http::{Context as AzureContext, Url}; use azure_core::tracing::{Attribute, AttributeValue, SpanKind, SpanStatus, TracerProvider}; use opentelemetry::trace::TraceContextExt; use opentelemetry::{Context, Key, KeyValue, Value}; @@ -158,6 +188,34 @@ mod tests { } } + // cspell: ignore traceparent tracestate + #[test] + fn test_open_telemetry_span_propagate() { + let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); + + let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); + assert!(tracer_provider.is_ok()); + let tracer = + tracer_provider + .unwrap() + .get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); + let span = tracer.start_span("test_span", SpanKind::Client, vec![]); + let mut request = azure_core::http::Request::new( + Url::parse("http://example.com").unwrap(), + azure_core::http::Method::Get, + ); + span.propagate_headers(&mut request); + trace!("Request headers after propagation: {:?}", request.headers()); + let traceparent = azure_core::http::headers::HeaderName::from("traceparent"); + let tracestate = azure_core::http::headers::HeaderName::from("tracestate"); + request.headers().get_as::(&traceparent).unwrap(); + request.headers().get_as::(&tracestate).unwrap(); + span.end(); + + let finished_spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(finished_spans.len(), 1); + } + #[test] fn test_open_telemetry_span_hierarchy() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); From baccb1bdd7f29c6e7c1580ec805b5870e2bb4213 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 15:33:11 -0700 Subject: [PATCH 13/84] More documentation --- .../http/policies/request_instrumentation.rs | 2 +- sdk/core/azure_core_opentelemetry/src/span.rs | 8 ++----- .../typespec_client_core/src/tracing/mod.rs | 21 ++++++++++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index c27cd2e88c..cd42881eb9 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -560,7 +560,7 @@ mod tests { assert_eq!(req.method(), &Method::Get); assert_eq!( req.headers() - .get_optional_str(HeaderName::from_static("traceparent")), + .get_optional_str(&HeaderName::from_static("traceparent")), Some("00---01") ); Ok(RawResponse::from_bytes( diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 728f56f011..beca6068d5 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -4,10 +4,7 @@ //! OpenTelemetry implementation of typespec_client_core tracing traits. use crate::attributes::AttributeValue as ConversionAttributeValue; -use azure_core::{ - tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}, - Result, -}; +use azure_core::tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}; use opentelemetry::{propagation::TextMapPropagator, trace::TraceContextExt}; use opentelemetry_http::HeaderInjector; use opentelemetry_sdk::propagation::TraceContextPropagator; @@ -132,9 +129,8 @@ struct OpenTelemetrySpanGuard { } impl SpanGuard for OpenTelemetrySpanGuard { - fn end(self) -> Result<()> { + fn end(self) { // The span is ended when the guard is dropped, so no action needed here. - Ok(()) } } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index bbb202dcd4..c6de90a87f 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -136,9 +136,13 @@ pub enum SpanKind { pub trait SpanGuard { /// Ends the span when dropped. - fn end(self) -> crate::Result<()>; + fn end(self); } +/// A trait that represents a span in distributed tracing. +/// +/// This trait defines the methods that a span must implement to be used in distributed tracing. +/// It includes methods for setting attributes, recording errors, and managing the span's lifecycle. pub trait Span: AsAny + Send + Sync { fn is_recording(&self) -> bool; @@ -158,6 +162,11 @@ pub trait Span: AsAny + Send + Sync { fn set_status(&self, status: SpanStatus); /// Sets an attribute on the current span. + /// + /// # Arguments + /// - `key`: The key of the attribute to set. + /// - `value`: The value of the attribute to set. + /// fn set_attribute(&self, key: &'static str, value: attributes::AttributeValue); /// Records a Rust standard error on the current span. @@ -171,6 +180,7 @@ pub trait Span: AsAny + Send + Sync { fn record_error(&self, error: &dyn std::error::Error); /// Temporarily sets the span as the current active span in the context. + /// /// # Arguments /// - `context`: The context in which to set the current span. /// @@ -182,6 +192,15 @@ pub trait Span: AsAny + Send + Sync { /// fn set_current(&self, context: &Context) -> crate::Result>; + /// Adds telemetry headers to the request for distributed tracing. + /// + /// # Arguments + /// - `request`: A mutable reference to the request to which headers will be added. + /// + /// This method should be called before sending the request to ensure that the tracing information + /// is included in the request headers. It typically adds the [W3C Distributed Tracing](https://www.w3.org/TR/trace-context/) + /// headers to the request. + /// fn propagate_headers(&self, request: &mut Request); } From 7629a8ceccfbe56af65ccb87d89c57f994d631aa Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 15:56:09 -0700 Subject: [PATCH 14/84] Small cleanup on propagate_headers --- sdk/core/azure_core_opentelemetry/src/span.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index beca6068d5..b7b4aa3b3d 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -4,11 +4,13 @@ //! OpenTelemetry implementation of typespec_client_core tracing traits. use crate::attributes::AttributeValue as ConversionAttributeValue; -use azure_core::tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}; +use azure_core::{ + http::headers::{HeaderName, HeaderValue}, + tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}, +}; use opentelemetry::{propagation::TextMapPropagator, trace::TraceContextExt}; use opentelemetry_http::HeaderInjector; use opentelemetry_sdk::propagation::TraceContextPropagator; -use reqwest::header::HeaderMap; use std::{error::Error as StdError, sync::Arc}; /// newtype for Azure Core SpanKind to enable conversion to OpenTelemetry SpanKind @@ -83,7 +85,7 @@ impl Span for OpenTelemetrySpan { // and the Azure Core request headers. // // We start with an empty header map and inject the OpenTelemetry headers into it. - let mut header_map = HeaderMap::new(); + let mut header_map = reqwest::header::HeaderMap::new(); trace_propagator.inject_context(&self.context, &mut HeaderInjector(&mut header_map)); // We then insert each of the headers from the OpenTelemetry header map into the @@ -92,11 +94,9 @@ impl Span for OpenTelemetrySpan { // Note: The OpenTelemetry HeaderInjector will always produce unique header names, so we don't need to // handle the multiple headers case here. if let Some(key) = key { - // Convert HeaderName to &str for insertion. - let value_str = value.to_str().unwrap().to_string(); request.insert_header( - azure_core::http::headers::HeaderName::from(key.to_string()), - azure_core::http::headers::HeaderValue::from(value_str), + HeaderName::from(key.as_str().to_owned()), + HeaderValue::from(value.to_str().unwrap().to_owned()), ); } else { // If the key is invalid, we skip it From ec08e04386396b346728924c8646a206f14487e5 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 16:58:37 -0700 Subject: [PATCH 15/84] Started filling out OpenTelemetry readme.md --- Cargo.lock | 1 + sdk/core/azure_core_opentelemetry/Cargo.toml | 1 + sdk/core/azure_core_opentelemetry/README.md | 43 ++++++++++++++++++- sdk/core/azure_core_opentelemetry/src/lib.rs | 3 ++ sdk/core/azure_core_opentelemetry/src/span.rs | 3 ++ .../azure_core_opentelemetry/src/telemetry.rs | 4 +- .../tests/telemetry_service_implementation.rs | 8 ++-- 7 files changed, 56 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 098894732b..a6ad1378f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,7 @@ dependencies = [ "azure_core", "azure_core_test", "azure_core_test_macros", + "azure_identity", "opentelemetry 0.30.0", "opentelemetry-http", "opentelemetry_sdk 0.30.0", diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index e2e44bb4f5..3b3b20f473 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -27,6 +27,7 @@ typespec_client_core.workspace = true [dev-dependencies] azure_core_test = { workspace = true, features = ["tracing"] } azure_core_test_macros.workspace = true +azure_identity.workspace = true opentelemetry_sdk = { version = "0.30", features = ["testing"] } tokio.workspace = true tracing-opentelemetry = "0.26" diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index 10f0460e09..8c3deaee7c 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -1,3 +1,44 @@ # Azure Core OpenTelemetry Tracing -This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. It enables automatic span creation, context propagation, and telemetry collection for Azure service operations. +This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. + +It allows Rust applications which use the [OpenTelemetry](https://opentelemetry.io/) APIs to generated OpenTelemetry spans for Azure SDK for Rust Clients. + +It implements the [Rust OpenTelemetry](https://opentelemetry.io/docs/languages/rust/) APIs for the Azure SDK distributed tracing traits. + +## OpenTelemetry integration with the Azure SDK for Rust + +To integrate the OpenTelemetry APIs with the Azure SDK for Rust, you create a [`OpenTelemetryTracerProvider`] and pass it into your SDK ClientOptions. + +```rust no_run +# use azure_identity::DefaultAzureCredential; +# use azure_core::{http::{ClientOptions, RequestInstrumentationOptions}}; +# #[derive(Default)] +# struct ServiceClientOptions { +# azure_client_options: ClientOptions, +# } +use azure_core_opentelemetry::OpenTelemetryTracerProvider; +use opentelemetry_sdk::trace::SdkTracerProvider; +use std::sync::Arc; + +# fn test_fn() -> azure_core::Result<()> { +// Create an OpenTelemetry tracer provider adapter from an OpenTelemetry TracerProvider +let otel_tracer_provider = Arc::new(SdkTracerProvider::builder().build()); + +let azure_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider)?; + +let options = ServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + +# Ok(()) +# } +``` + +Once the `OpenTelemetryTracerProvider` is integrated with the Azure Service ClientOptions, the Azure SDK will be configured to capture per-API and per-HTTP operation tracing options, and the HTTP requests will be annotated with [W3C Trace Context headers](https://www.w3.org/TR/trace-context/). diff --git a/sdk/core/azure_core_opentelemetry/src/lib.rs b/sdk/core/azure_core_opentelemetry/src/lib.rs index a192707bd3..4f25c7ef33 100644 --- a/sdk/core/azure_core_opentelemetry/src/lib.rs +++ b/sdk/core/azure_core_opentelemetry/src/lib.rs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + //! Azure Core OpenTelemetry tracing integration. //! //! This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index b7b4aa3b3d..2bffc32968 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -93,9 +93,12 @@ impl Span for OpenTelemetrySpan { for (key, value) in header_map.into_iter() { // Note: The OpenTelemetry HeaderInjector will always produce unique header names, so we don't need to // handle the multiple headers case here. + if let Some(key) = key { request.insert_header( HeaderName::from(key.as_str().to_owned()), + // The value is guaranteed to be a valid UTF-8 string by the OpenTelemetry SDK, + // so we can safely unwrap it. HeaderValue::from(value.to_str().unwrap().to_owned()), ); } else { diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index 36dec069c9..d347413d83 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -18,8 +18,8 @@ pub struct OpenTelemetryTracerProvider { impl OpenTelemetryTracerProvider { /// Creates a new Azure telemetry provider with the given SDK tracer provider. #[allow(dead_code)] - pub fn new(provider: Arc) -> Result { - Ok(Self { inner: provider }) + pub fn new(provider: Arc) -> Result> { + Ok(Arc::new(Self { inner: provider })) } } diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index edd8e85128..7ed16566a2 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -314,7 +314,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -376,7 +376,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -438,7 +438,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -507,7 +507,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; let recording = ctx.recording(); let endpoint = "https://example.com"; From 0daa061dda9ff9a7c36702bb89eb942353223837 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 12:03:31 -0700 Subject: [PATCH 16/84] PR feedback from Liudmila --- .../http/policies/request_instrumentation.rs | 30 ++++++++++++++----- .../tests/telemetry_service_implementation.rs | 16 +++++----- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index cd42881eb9..4e217e226f 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -12,15 +12,15 @@ use typespec_client_core::{ }; const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; -const AZ_SCHEMA_URL_ATTRIBUTE: &str = "az.schema.url"; const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client.request.id"; const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; -const AZ_SERVICE_REQUEST_ID_ATTRIBUTE: &str = "az.service.request.id"; -const HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE: &str = "http.request.resend.count"; +const AZ_SERVICE_REQUEST_ID_ATTRIBUTE: &str = "az.service_request.id"; +const HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE: &str = "http.request.resend_count"; const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE: &str = "http.response.status_code"; const HTTP_REQUEST_METHOD_ATTRIBUTE: &str = "http.request.method"; const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; const SERVER_PORT_ATTRIBUTE: &str = "server.port"; +const URL_SCHEME_ATTRIBUTE: &str = "url.scheme"; const URL_FULL_ATTRIBUTE: &str = "url.full"; /// Sets distributed tracing information for HTTP requests. @@ -88,6 +88,15 @@ impl Policy for RequestInstrumentationPolicy { self.tracer.as_ref() }; + // If there is a span in the context, if it's not recording, just forward the request + // without instrumentation. + if let Some(span) = ctx.value::>() { + if !span.is_recording() { + // If the span is not recording, we skip instrumentation. + return next[0].send(ctx, request, &next[1..]).await; + } + } + if let Some(tracer) = tracer { let mut span_attributes = vec![ Attribute { @@ -95,7 +104,7 @@ impl Policy for RequestInstrumentationPolicy { value: request.method().to_string().into(), }, Attribute { - key: AZ_SCHEMA_URL_ATTRIBUTE, + key: URL_SCHEME_ATTRIBUTE, value: request.url().scheme().into(), }, ]; @@ -171,6 +180,11 @@ impl Policy for RequestInstrumentationPolicy { tracer.start_span_with_current(method_str, SpanKind::Client, span_attributes) }; + if !span.is_recording() { + // If the span is not recording, we skip instrumentation. + return next[0].send(ctx, request, &next[1..]).await; + } + if let Some(client_request_id) = request .headers() .get_optional_str(&headers::CLIENT_REQUEST_ID) @@ -524,7 +538,7 @@ mod tests { AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("test namespace"), ), - (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("http")), + (URL_SCHEME_ATTRIBUTE, AttributeValue::from("http")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, AttributeValue::from(200), @@ -581,7 +595,7 @@ mod tests { "GET", SpanStatus::Unset, vec![ - (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + (URL_SCHEME_ATTRIBUTE, AttributeValue::from("https")), ( AZ_CLIENT_REQUEST_ID_ATTRIBUTE, AttributeValue::from("test-client-request-id"), @@ -631,7 +645,7 @@ mod tests { "GET", SpanStatus::Unset, vec![ - (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + (URL_SCHEME_ATTRIBUTE, AttributeValue::from("https")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, AttributeValue::from(200), @@ -691,7 +705,7 @@ mod tests { AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("test namespace"), ), - (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + (URL_SCHEME_ATTRIBUTE, AttributeValue::from("https")), ( AZ_SERVICE_REQUEST_ID_ATTRIBUTE, AttributeValue::from("test-service-request-id"), diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 7ed16566a2..d2fa3534c3 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -350,7 +350,7 @@ mod tests { parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("az.schema.url", "https".into()), + ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -363,7 +363,7 @@ mod tests { ), ("server.address", "example.com".into()), ("server.port", 443.into()), - ("http.request.resend.count", 0.into()), + ("http.request.resend_count", 0.into()), ("http.response.status_code", 200.into()), ], }, @@ -411,7 +411,7 @@ mod tests { }, attributes: vec![ ("http.request.method", "GET".into()), - ("az.schema.url", "https".into()), + ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -425,7 +425,7 @@ mod tests { ("server.address", "example.com".into()), ("server.port", 443.into()), ("error.type", "404".into()), - ("http.request.resend.count", 0.into()), + ("http.request.resend_count", 0.into()), ("http.response.status_code", 404.into()), ], }, @@ -472,7 +472,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.schema.url", "https".into()), + ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -485,7 +485,7 @@ mod tests { ), ("server.address", "example.com".into()), ("server.port", 443.into()), - ("http.request.resend.count", 0.into()), + ("http.request.resend_count", 0.into()), ("http.response.status_code", 200.into()), ], }, @@ -543,7 +543,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.schema.url", "https".into()), + ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -556,7 +556,7 @@ mod tests { ), ("server.address", "example.com".into()), ("server.port", 443.into()), - ("http.request.resend.count", 0.into()), + ("http.request.resend_count", 0.into()), ("http.response.status_code", 404.into()), ("error.type", "404".into()), ], From 5c14d45d7a89a2350672f2b3da596777d874b52f Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 12:14:31 -0700 Subject: [PATCH 17/84] Cargo fmt fixes --- sdk/core/azure_core/src/http/pipeline.rs | 3 +-- sdk/typespec/typespec_client_core/src/http/request/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sdk/core/azure_core/src/http/pipeline.rs b/sdk/core/azure_core/src/http/pipeline.rs index 7358e5d65b..c53dc02b81 100644 --- a/sdk/core/azure_core/src/http/pipeline.rs +++ b/sdk/core/azure_core/src/http/pipeline.rs @@ -53,10 +53,9 @@ impl Pipeline { let (core_client_options, options) = options.deconstruct(); let user_agent_policy = - UserAgentPolicy::new(crate_name, crate_version, &core_client_options.user_agent); + UserAgentPolicy::new(crate_name, crate_version, &core_client_options.user_agent); push_unique(&mut per_call_policies, user_agent_policy); - let mut per_try_policies = per_try_policies.clone(); if core_client_options .request_instrumentation diff --git a/sdk/typespec/typespec_client_core/src/http/request/mod.rs b/sdk/typespec/typespec_client_core/src/http/request/mod.rs index ab2549d0e0..9f82e66564 100644 --- a/sdk/typespec/typespec_client_core/src/http/request/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/request/mod.rs @@ -160,8 +160,8 @@ impl Request { } } - /// Inserts zero or more headers from a type that implements [`AsHeaders`]. - pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { + /// Inserts zero or more headers from a type that implements [`AsHeaders`]. + pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { for (name, value) in headers.as_headers()? { self.insert_header(name, value); } From caef25202e8ac852417c6237b54f1415868d356d Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 15:24:36 -0700 Subject: [PATCH 18/84] Removed unused public function --- .../typespec_client_core/src/http/request/mod.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/sdk/typespec/typespec_client_core/src/http/request/mod.rs b/sdk/typespec/typespec_client_core/src/http/request/mod.rs index 9f82e66564..dc38355600 100644 --- a/sdk/typespec/typespec_client_core/src/http/request/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/request/mod.rs @@ -145,21 +145,6 @@ impl Request { &self.method } - /// Returns the HTTP method as a static string. - /// - /// This is not generally useful and should be avoided in favor of using the ['Request::method()'] method. - #[doc(hidden)] - pub fn method_as_str(&self) -> &'static str { - match self.method { - Method::Delete => "DELETE", - Method::Get => "GET", - Method::Head => "HEAD", - Method::Patch => "PATCH", - Method::Post => "POST", - Method::Put => "PUT", - } - } - /// Inserts zero or more headers from a type that implements [`AsHeaders`]. pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { for (name, value) in headers.as_headers()? { From 5e4839893649e1f0db3261f2003dbab778b119df Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 16:12:06 -0700 Subject: [PATCH 19/84] Added the ability to configure an OpenTelemetryProvider on the global OpenTelemetry provider --- .../http/policies/request_instrumentation.rs | 3 +- sdk/core/azure_core_opentelemetry/README.md | 33 ++++++++- sdk/core/azure_core_opentelemetry/src/span.rs | 62 ++++------------- .../azure_core_opentelemetry/src/telemetry.rs | 69 +++++++++++++++---- .../azure_core_opentelemetry/src/tracer.rs | 6 +- .../tests/integration_test.rs | 6 +- .../tests/telemetry_service_implementation.rs | 8 +-- .../typespec_client_core/src/tracing/mod.rs | 8 +-- .../src/tracing/with_context.rs | 4 +- 9 files changed, 119 insertions(+), 80 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 4e217e226f..8cb22c03a4 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -399,8 +399,7 @@ mod tests { fn set_current( &self, _context: &Context, - ) -> typespec_client_core::Result> - { + ) -> Box { todo!() } diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index 8c3deaee7c..2db3f51aa9 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -25,7 +25,38 @@ use std::sync::Arc; // Create an OpenTelemetry tracer provider adapter from an OpenTelemetry TracerProvider let otel_tracer_provider = Arc::new(SdkTracerProvider::builder().build()); -let azure_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider)?; +let azure_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); + +let options = ServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + +# Ok(()) +# } +``` + +If it is more convenient to use the global OpenTelemetry provider, then the [`OpenTelemetryTracerProvider::new_from_global_provider`] method will configure the OpenTelemetry support to use the global provider instead of a custom configured provider. + +```rust no_run +# use azure_identity::DefaultAzureCredential; +# use azure_core::{http::{ClientOptions, RequestInstrumentationOptions}}; +# #[derive(Default)] +# struct ServiceClientOptions { +# azure_client_options: ClientOptions, +# } +use azure_core_opentelemetry::OpenTelemetryTracerProvider; +use opentelemetry_sdk::trace::SdkTracerProvider; +use std::sync::Arc; + +# fn test_fn() -> azure_core::Result<()> { + +let azure_provider = OpenTelemetryTracerProvider::new_from_global_provider(); let options = ServiceClientOptions { azure_client_options: ClientOptions { diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 2bffc32968..a864f615f6 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -108,16 +108,13 @@ impl Span for OpenTelemetrySpan { } } - fn set_current( - &self, - _context: &azure_core::http::Context, - ) -> typespec_client_core::Result> { + fn set_current(&self, _context: &azure_core::http::Context) -> Box { // Create a context with the current span let context_guard = self.context.clone().attach(); - Ok(Box::new(OpenTelemetrySpanGuard { + Box::new(OpenTelemetrySpanGuard { _inner: context_guard, - })) + }) } } @@ -169,11 +166,7 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = - tracer_provider - .unwrap() - .get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); span.end(); @@ -193,11 +186,7 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = - tracer_provider - .unwrap() - .get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let mut request = azure_core::http::Request::new( Url::parse("http://example.com").unwrap(), @@ -219,10 +208,7 @@ mod tests { fn test_open_telemetry_span_hierarchy() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("Special Name"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Special Name"), "test", "0.1.0"); let parent_span = tracer.start_span("parent_span", SpanKind::Server, vec![]); let child_span = tracer.start_span_with_parent( "child_span", @@ -253,10 +239,7 @@ mod tests { fn test_open_telemetry_span_start_with_parent() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("MyNamespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("MyNamespace"), "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let child_span = @@ -285,15 +268,10 @@ mod tests { fn test_open_telemetry_span_start_with_current() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("Namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Namespace"), "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); - let _span_guard = span2 - .set_current(&azure_core::http::Context::new()) - .unwrap(); + let _span_guard = span2.set_current(&azure_core::http::Context::new()); let child_span = tracer.start_span_with_current("child_span", SpanKind::Client, vec![]); child_span.end(); @@ -319,10 +297,7 @@ mod tests { fn test_open_telemetry_span_set_attribute() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("ThisNamespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("ThisNamespace"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.set_attribute("test_key", AttributeValue::String("test_value".to_string())); @@ -345,10 +320,7 @@ mod tests { fn test_open_telemetry_span_record_error() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("namespace"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let error = Error::new(ErrorKind::NotFound, "resource not found"); @@ -374,10 +346,7 @@ mod tests { fn test_open_telemetry_span_set_status() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("Namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Namespace"), "test", "0.1.0"); // Test Unset status let span = tracer.start_span("test_span_unset", SpanKind::Server, vec![]); @@ -406,10 +375,7 @@ mod tests { async fn test_open_telemetry_span_futures() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("Namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Namespace"), "test", "0.1.0"); let future = async { let context = Context::current(); @@ -435,7 +401,7 @@ mod tests { let azure_context = AzureContext::new(); let azure_context = azure_context.with_value(span.clone()); - let _guard = span.set_current(&azure_context).unwrap(); + let _guard = span.set_current(&azure_context); let result = future.await; diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index d347413d83..12127a43ee 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -3,7 +3,6 @@ use crate::tracer::OpenTelemetryTracer; use azure_core::tracing::TracerProvider; -use azure_core::Result; use opentelemetry::{ global::{BoxedTracer, ObjectSafeTracerProvider}, InstrumentationScope, @@ -12,14 +11,36 @@ use std::sync::Arc; /// Enum to hold different OpenTelemetry tracer provider implementations. pub struct OpenTelemetryTracerProvider { - inner: Arc, + inner: Option>, } impl OpenTelemetryTracerProvider { /// Creates a new Azure telemetry provider with the given SDK tracer provider. - #[allow(dead_code)] - pub fn new(provider: Arc) -> Result> { - Ok(Arc::new(Self { inner: provider })) + /// + /// # Arguments + /// - `provider`: An `Arc` to an object-safe tracer provider that implements the + /// `ObjectSafeTracerProvider` trait. + /// + /// # Returns + /// An `Arc` to the newly created `OpenTelemetryTracerProvider`. + /// + /// + pub fn new(provider: Arc) -> Arc { + Arc::new(Self { + inner: Some(provider), + }) + } + + /// Creates a new Azure telemetry provider that uses the global OpenTelemetry tracer provider. + /// + /// This is useful when you want to use the global OpenTelemetry provider without + /// explicitly instantiating a specific provider. + /// + /// # Returns + /// An `Arc` to the newly created `OpenTelemetryTracerProvider` that uses the global provider. + /// + pub fn new_from_global_provider() -> Arc { + Arc::new(Self { inner: None }) } } @@ -34,10 +55,19 @@ impl TracerProvider for OpenTelemetryTracerProvider { .with_version(package_version) .with_schema_url("https://opentelemetry.io/schemas/1.23.0") .build(); - Arc::new(OpenTelemetryTracer::new( - namespace, - BoxedTracer::new(self.inner.boxed_tracer(scope)), - )) + if let Some(provider) = &self.inner { + // If we have a specific provider set, use it to create the tracer. + Arc::new(OpenTelemetryTracer::new( + namespace, + BoxedTracer::new(provider.boxed_tracer(scope)), + )) + } else { + // Use the global tracer if no specific provider has been set. + Arc::new(OpenTelemetryTracer::new( + namespace, + opentelemetry::global::tracer_with_scope(scope), + )) + } } } @@ -50,14 +80,27 @@ mod tests { #[test] fn test_create_tracer_provider_sdk_tracer() { let provider = Arc::new(SdkTracerProvider::builder().build()); - let tracer_provider = OpenTelemetryTracerProvider::new(provider); - assert!(tracer_provider.is_ok()); + let _tracer_provider = OpenTelemetryTracerProvider::new(provider); } #[test] fn test_create_tracer_provider_noop_tracer() { let provider = Arc::new(NoopTracerProvider::new()); - let tracer_provider = OpenTelemetryTracerProvider::new(provider); - assert!(tracer_provider.is_ok()); + let _tracer_provider = OpenTelemetryTracerProvider::new(provider); + } + + #[test] + fn test_create_tracer_provider_from_global() { + let tracer_provider = OpenTelemetryTracerProvider::new_from_global_provider(); + let _tracer = tracer_provider.get_tracer(Some("My Namespace"), "test", "0.1.0"); + } + + #[test] + fn test_create_tracer_provider_from_global_provider_set() { + let provider = SdkTracerProvider::builder().build(); + opentelemetry::global::set_tracer_provider(provider); + + let tracer_provider = OpenTelemetryTracerProvider::new_from_global_provider(); + let _tracer = tracer_provider.get_tracer(Some("My Namespace"), "test", "0.1.0"); } } diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 6282f5cdb5..9051643534 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -111,7 +111,7 @@ mod tests { #[test] fn test_create_tracer() { let noop_tracer = NoopTracerProvider::new(); - let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); + let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)); let tracer = otel_provider.get_tracer(Some("name"), "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); @@ -120,14 +120,14 @@ mod tests { #[test] fn test_create_tracer_with_sdk_tracer() { let provider = SdkTracerProvider::builder().build(); - let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); + let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)); let _tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); } #[test] fn test_create_span_from_tracer() { let provider = SdkTracerProvider::builder().build(); - let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); + let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)); let tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); let _span = tracer.start_span("test_span", SpanKind::Internal, vec![]); } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index e6165d45a0..bd8911fcaa 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -11,7 +11,7 @@ use std::sync::Arc; async fn test_span_creation() -> Result<(), Box> { // Set up a tracer provider for testing let sdk_provider = Arc::new(SdkTracerProvider::builder().build()); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); // Get a tracer from the Azure provider let tracer = azure_provider.get_tracer(Some("test_namespace"), "test_tracer", "1.0.0"); @@ -39,7 +39,7 @@ async fn test_span_creation() -> Result<(), Box> { async fn test_tracer_provider_creation() -> Result<(), Box> { // Create multiple tracer provider instances to test initialization let sdk_provider = Arc::new(SdkTracerProvider::builder().build()); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); // Get a tracer and verify it works let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); @@ -53,7 +53,7 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { async fn test_span_attributes() -> Result<(), Box> { // Set up a tracer provider for testing let sdk_provider = Arc::new(SdkTracerProvider::builder().build()); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); // Get a tracer from the Azure provider let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index d2fa3534c3..57e3b9aade 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -314,7 +314,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -376,7 +376,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -438,7 +438,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -507,7 +507,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); let endpoint = "https://example.com"; diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index c6de90a87f..b096b9ee27 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -55,7 +55,7 @@ pub trait Tracer: Send + Sync { /// - `kind`: The type of the span to start. /// /// # Returns - /// An `Arc` representing the started span. + /// An `Arc` representing the started span. /// fn start_span( &self, @@ -71,7 +71,7 @@ pub trait Tracer: Send + Sync { /// - `kind`: The type of the span to start. /// /// # Returns - /// An `Arc` representing the started span. + /// An `Arc` representing the started span. /// fn start_span_with_current( &self, @@ -88,7 +88,7 @@ pub trait Tracer: Send + Sync { /// - `parent`: The parent span to use for the new span. /// /// # Returns - /// An `Arc` representing the started span + /// An `Arc` representing the started span /// /// Note: This method may panic if the parent span cannot be downcasted to the expected type. /// @@ -190,7 +190,7 @@ pub trait Span: AsAny + Send + Sync { /// This method allows the span to be set as the current span in the context, /// enabling it to be used for tracing operations within that context. /// - fn set_current(&self, context: &Context) -> crate::Result>; + fn set_current(&self, context: &Context) -> Box; /// Adds telemetry headers to the request for distributed tracing. /// diff --git a/sdk/typespec/typespec_client_core/src/tracing/with_context.rs b/sdk/typespec/typespec_client_core/src/tracing/with_context.rs index 14ef2808bf..8dfcf050b6 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/with_context.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/with_context.rs @@ -20,8 +20,8 @@ impl std::future::Future for WithContext<'_, T> { fn poll(self: Pin<&mut Self>, task_cx: &mut TaskContext<'_>) -> Poll { let this = self.project(); - if let Some(span) = this.context.value::>() { - let _guard = span.set_current(this.context).unwrap(); + if let Some(span) = this.context.value::>() { + let _guard = span.set_current(this.context); this.inner.poll(task_cx) } else { From be6107924e65973a5418ba29f5d52805da8b5b92 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 16:21:26 -0700 Subject: [PATCH 20/84] Documentation updates --- sdk/core/azure_core_opentelemetry/README.md | 6 +++--- sdk/core/azure_core_opentelemetry/src/lib.rs | 6 ------ .../typespec_client_core/src/tracing/mod.rs | 15 +++++++++++++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index 2db3f51aa9..a87a8db7f9 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -1,10 +1,10 @@ # Azure Core OpenTelemetry Tracing This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. +It bridges the standardized azure_core tracing traits with OpenTelemetry implementation, +enabling automatic span creation, context propagation, and telemetry collection for Azure services. -It allows Rust applications which use the [OpenTelemetry](https://opentelemetry.io/) APIs to generated OpenTelemetry spans for Azure SDK for Rust Clients. - -It implements the [Rust OpenTelemetry](https://opentelemetry.io/docs/languages/rust/) APIs for the Azure SDK distributed tracing traits. +It allows Rust applications which use the [OpenTelemetry](https://opentelemetry.io/) APIs to generate OpenTelemetry spans for Azure SDK for Rust Clients. ## OpenTelemetry integration with the Azure SDK for Rust diff --git a/sdk/core/azure_core_opentelemetry/src/lib.rs b/sdk/core/azure_core_opentelemetry/src/lib.rs index 4f25c7ef33..37bed17edb 100644 --- a/sdk/core/azure_core_opentelemetry/src/lib.rs +++ b/sdk/core/azure_core_opentelemetry/src/lib.rs @@ -4,12 +4,6 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -//! Azure Core OpenTelemetry tracing integration. -//! -//! This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. -//! It bridges the standardized typespec_client_core tracing traits with OpenTelemetry implementation, -//! enabling automatic span creation, context propagation, and telemetry collection for Azure services. - mod attributes; mod span; mod telemetry; diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index b096b9ee27..2b49bfd2b5 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -50,9 +50,12 @@ impl Debug for dyn TracerProvider { pub trait Tracer: Send + Sync { /// Starts a new span with the given name and type. /// + /// The newly created span will not have a parent span. + /// /// # Arguments /// - `name`: The name of the span to start. /// - `kind`: The type of the span to start. + /// - `attributes`: A vector of attributes to associate with the span. /// /// # Returns /// An `Arc` representing the started span. @@ -64,11 +67,15 @@ pub trait Tracer: Send + Sync { attributes: Vec, ) -> Arc; - /// Starts a new span with the given type, using the current span as the parent span. + /// Starts a new span with the given name and type. + /// + /// The parent span of the newly created span will be the current span (if one + /// exists). /// /// # Arguments /// - `name`: The name of the span to start. /// - `kind`: The type of the span to start. + /// - `attributes`: A vector of attributes to associate with the span. /// /// # Returns /// An `Arc` representing the started span. @@ -85,6 +92,7 @@ pub trait Tracer: Send + Sync { /// # Arguments /// - `name`: The name of the span to start. /// - `kind`: The type of the span to start. + /// - `attributes`: A vector of attributes to associate with the span. /// - `parent`: The parent span to use for the new span. /// /// # Returns @@ -100,7 +108,10 @@ pub trait Tracer: Send + Sync { parent: Arc, ) -> Arc; - /// Returns the namespace the tracer was configured with. + /// Returns the namespace the tracer was configured with (if any). + /// + /// # Returns + /// An `Option<&'static str>` representing the namespace of the tracer, fn namespace(&self) -> Option<&'static str>; } From 9cb989c56de4bd94f13634c4207c379286d4b7de Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 2 Jul 2025 10:22:17 -0700 Subject: [PATCH 21/84] Corrected name of opentelemetry package --- eng/scripts/verify-dependencies.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/scripts/verify-dependencies.rs b/eng/scripts/verify-dependencies.rs index 20ac504a48..36efb52800 100755 --- a/eng/scripts/verify-dependencies.rs +++ b/eng/scripts/verify-dependencies.rs @@ -20,9 +20,9 @@ use std::{ static EXEMPTIONS: &[(&str, &str)] = &[ ("azure_core_test", "dotenvy"), ("azure_template", "serde"), - ("azure_core_tracing_opentelemetry", "opentelemetry"), - ("azure_core_tracing_opentelemetry", "opentelemetry_sdk"), - ("azure_core_tracing_opentelemetry", "tracing-opentelemetry"), + ("azure_core_opentelemetry", "opentelemetry"), + ("azure_core_opentelemetry", "opentelemetry_sdk"), + ("azure_core_opentelemetry", "tracing-opentelemetry"), ]; fn main() { From 06afa8278cd3e120e3ee61f436a8e15037823ddd Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 8 Jul 2025 13:17:04 -0700 Subject: [PATCH 22/84] First cut at EH perf tests --- Cargo.lock | 12 + Cargo.toml | 6 + sdk/core/azure_core/Cargo.toml | 1 + sdk/core/azure_core/src/http/options/mod.rs | 3 +- sdk/core/azure_core/src/lib.rs | 5 +- sdk/core/azure_core_macros/Cargo.toml | 27 + sdk/core/azure_core_macros/README.md | 3 + sdk/core/azure_core_macros/src/lib.rs | 119 ++++ sdk/core/azure_core_macros/src/tracing.rs | 108 ++++ .../azure_core_macros/src/tracing_client.rs | 93 +++ .../azure_core_macros/src/tracing_function.rs | 98 +++ sdk/core/azure_core_macros/src/tracing_new.rs | 310 ++++++++++ sdk/core/azure_core_opentelemetry/README.md | 8 +- .../tests/telemetry_service_implementation.rs | 52 +- .../tests/telemetry_service_macros.rs | 584 ++++++++++++++++++ 15 files changed, 1399 insertions(+), 30 deletions(-) create mode 100644 sdk/core/azure_core_macros/Cargo.toml create mode 100644 sdk/core/azure_core_macros/README.md create mode 100644 sdk/core/azure_core_macros/src/lib.rs create mode 100644 sdk/core/azure_core_macros/src/tracing.rs create mode 100644 sdk/core/azure_core_macros/src/tracing_client.rs create mode 100644 sdk/core/azure_core_macros/src/tracing_function.rs create mode 100644 sdk/core/azure_core_macros/src/tracing_new.rs create mode 100644 sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs diff --git a/Cargo.lock b/Cargo.lock index a6ad1378f9..d12e9854e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,7 @@ version = "0.26.0" dependencies = [ "async-lock", "async-trait", + "azure_core_macros", "azure_core_test", "azure_identity", "azure_security_keyvault_secrets", @@ -201,6 +202,17 @@ dependencies = [ "typespec_macros", ] +[[package]] +name = "azure_core_macros" +version = "0.1.0" +dependencies = [ + "azure_core", + "proc-macro2", + "quote", + "syn", + "tokio", +] + [[package]] name = "azure_core_opentelemetry" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 73169eb9cd..6c5dc45e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "sdk/typespec/typespec_macros", "sdk/core/azure_core", "sdk/core/azure_core_amqp", + "sdk/core/azure_core_macros", "sdk/core/azure_core_test", "sdk/core/azure_core_test_macros", "sdk/core/azure_core_opentelemetry", @@ -49,6 +50,11 @@ path = "sdk/typespec/typespec_macros" version = "0.26.0" path = "sdk/core/azure_core" +[workspace.dependencies.azure_core_macros] +version = "0.1.0" +path = "sdk/core/azure_core_macros" + + [workspace.dependencies.azure_core_amqp] version = "0.5.0" path = "sdk/core/azure_core_amqp" diff --git a/sdk/core/azure_core/Cargo.toml b/sdk/core/azure_core/Cargo.toml index 364e4b43c4..df21ea039c 100644 --- a/sdk/core/azure_core/Cargo.toml +++ b/sdk/core/azure_core/Cargo.toml @@ -16,6 +16,7 @@ rust-version.workspace = true [dependencies] async-lock.workspace = true async-trait.workspace = true +azure_core_macros.workspace = true bytes.workspace = true futures.workspace = true hmac = { workspace = true, optional = true } diff --git a/sdk/core/azure_core/src/http/options/mod.rs b/sdk/core/azure_core/src/http/options/mod.rs index fd2e0e835e..468e5e0296 100644 --- a/sdk/core/azure_core/src/http/options/mod.rs +++ b/sdk/core/azure_core/src/http/options/mod.rs @@ -8,7 +8,8 @@ pub use request_instrumentation::*; use std::sync::Arc; use typespec_client_core::http::policies::Policy; pub use typespec_client_core::http::{ - ClientMethodOptions, ExponentialRetryOptions, FixedRetryOptions, RetryOptions, TransportOptions, + ClientMethodOptions, ExponentialRetryOptions, FixedRetryOptions, + RetryOptions, TransportOptions, }; pub use user_agent::*; diff --git a/sdk/core/azure_core/src/lib.rs b/sdk/core/azure_core/src/lib.rs index edf21fa808..a945b385d0 100644 --- a/sdk/core/azure_core/src/lib.rs +++ b/sdk/core/azure_core/src/lib.rs @@ -27,7 +27,10 @@ pub use typespec_client_core::{ fmt, json, sleep, stream, time, Bytes, Uuid, }; -pub use typespec_client_core::tracing; +pub mod tracing { + pub use azure_core_macros::*; + pub use typespec_client_core::tracing::*; +} #[cfg(feature = "xml")] pub use typespec_client_core::xml; diff --git a/sdk/core/azure_core_macros/Cargo.toml b/sdk/core/azure_core_macros/Cargo.toml new file mode 100644 index 0000000000..88f3db6092 --- /dev/null +++ b/sdk/core/azure_core_macros/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "azure_core_macros" +version = "0.1.0" +description = "Procedural macros for client libraries built on azure_core." +readme = "README.md" +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage = "https://github.com/azure/azure-sdk-for-rust" +documentation = "https://docs.rs/azure_core" +keywords = ["azure", "cloud", "iot", "rest", "sdk"] +categories = ["development-tools"] +edition.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true + +[dev-dependencies] +azure_core.workspace = true +#azure_core_test.workspace = true +tokio.workspace = true diff --git a/sdk/core/azure_core_macros/README.md b/sdk/core/azure_core_macros/README.md new file mode 100644 index 0000000000..cbe67c0b26 --- /dev/null +++ b/sdk/core/azure_core_macros/README.md @@ -0,0 +1,3 @@ +# Azure client library macros + +Macros for client libraries built on `azure_core`. diff --git a/sdk/core/azure_core_macros/src/lib.rs b/sdk/core/azure_core_macros/src/lib.rs new file mode 100644 index 0000000000..0e12335b8c --- /dev/null +++ b/sdk/core/azure_core_macros/src/lib.rs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#![doc = include_str!("../README.md")] + +mod tracing; +mod tracing_client; +mod tracing_function; +mod tracing_new; + +use proc_macro::TokenStream; + +/// Attribute client struct declarations to enable distributed tracing. +/// +/// # Examples +/// +/// +/// +/// For example, to declare a client that will be traced, you should use the `#[trace::client]` attribute. +/// +/// ``` +/// use azure_core::tracing; +/// use azure_core::http::Url; +/// use std::sync::Arc; +/// +/// #[tracing::client] +/// pub struct MyServiceClient { +/// endpoint: Url, +/// } +/// ``` +/// +#[proc_macro_attribute] +pub fn client(attr: TokenStream, item: TokenStream) -> TokenStream { + tracing_client::parse_client(attr.into(), item.into()) + .map_or_else(|e| e.into_compile_error().into(), |v| v.into()) +} + +/// Attribute client struct instantiation to enable distributed tracing. +/// +/// # Examples +/// +/// To declare a client that will be traced, you should use the `#[traced::client]` attribute. +/// To instantiate a client, use the `[traced::new]` which generates a distributed tracing tracer associated with the client namespace. +/// +/// ``` +/// use azure_core::{tracing, http::{Url, ClientOptions}}; +/// use std::sync::Arc; +/// +/// #[tracing::client] +/// pub struct MyServiceClient { +/// endpoint: Url, +/// } +/// +/// #[derive(Default)] +/// pub struct MyServiceClientOptions { +/// pub client_options: ClientOptions, +/// } +/// +/// impl MyServiceClient { +/// +/// #[tracing::new("MyServiceClientNamespace")] +/// pub fn new(endpoint: &str, _credential: Arc, options: Option) -> Self { +/// let options = options.unwrap_or_default(); +/// let url = Url::parse(endpoint).expect("Invalid endpoint URL"); +/// Self { +/// endpoint: url, +/// } +/// } +/// } +/// ``` +/// +#[proc_macro_attribute] +pub fn new(attr: TokenStream, item: TokenStream) -> TokenStream { + tracing_new::parse_new(attr.into(), item.into()) + .map_or_else(|e| e.into_compile_error().into(), |v| v.into()) +} + +/// Attribute client struct instantiation to enable distributed tracing. +/// +/// # Examples +/// +/// To declare a client that will be traced, you should use the `#[traced::client]` attribute. +/// To instantiate a client, use the `[traced::new]` which generates a distributed tracing tracer associated with the client namespace. +/// +/// ``` +/// use azure_core::{tracing, http::{Url, ClientOptions}, Result}; +/// use azure_core::http::ClientMethodOptions; +/// use std::sync::Arc; +/// +/// #[tracing::client] +/// pub struct MyServiceClient { +/// endpoint: Url, +/// } +/// +/// #[derive(Default)] +/// pub struct MyServiceClientOptions { +/// pub client_options: ClientOptions, +/// } +/// +/// #[derive(Default)] +/// pub struct MyServiceClientMethodOptions<'a> { +/// pub method_options: ClientMethodOptions<'a>, +/// } +/// +/// impl MyServiceClient { +/// +/// #[tracing::function] +/// pub fn public_function(param: &str, options: Option) -> Result<()> { +/// let options = options.unwrap_or_default(); +/// Ok(()) +/// } +/// } +/// ``` +/// +#[proc_macro_attribute] +pub fn function(attr: TokenStream, item: TokenStream) -> TokenStream { + tracing_function::parse_function(attr.into(), item.into()) + .map_or_else(|e| e.into_compile_error().into(), |v| v.into()) +} diff --git a/sdk/core/azure_core_macros/src/tracing.rs b/sdk/core/azure_core_macros/src/tracing.rs new file mode 100644 index 0000000000..72d4d124a8 --- /dev/null +++ b/sdk/core/azure_core_macros/src/tracing.rs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#[cfg(test)] +pub(crate) mod tests { + use proc_macro2::{TokenStream, TokenTree}; + + use super::*; + + // cspell: ignore punct + + pub(crate) fn compare_token_tree(token: &TokenTree, expected_token: &TokenTree) -> bool { + // println!("Comparing token: {token:?} with expected token: {expected_token:?}"); + match token { + TokenTree::Group(group) => match expected_token { + TokenTree::Group(expected_group) => { + compare_token_stream(group.stream(), expected_group.stream()) + } + _ => { + println!("Unexpected token: {expected_token:?}"); + false + } + }, + TokenTree::Ident(ident) => match expected_token { + TokenTree::Ident(expected_ident) => *expected_ident == *ident, + _ => { + println!("Unexpected token: {expected_token:?}"); + false + } + }, + TokenTree::Punct(punct) => match expected_token { + TokenTree::Punct(expected_punct) => punct.as_char() == expected_punct.as_char(), + _ => { + println!("Unexpected token: {expected_token:?}"); + false + } + }, + TokenTree::Literal(literal) => match expected_token { + TokenTree::Literal(expected_literal) => { + literal.to_string() == expected_literal.to_string() + } + _ => { + println!("Unexpected token: {expected_token:?}"); + false + } + }, + } + } + + pub(crate) fn compare_token_stream(actual: TokenStream, expected: TokenStream) -> bool { + let actual_tokens = Vec::from_iter(actual); + let expected_tokens = Vec::from_iter(expected); + + if actual_tokens.len() != expected_tokens.len() { + println!( + "Token lengths do not match: actual: {} != expected: {}", + actual_tokens.len(), + expected_tokens.len() + ); + for (i, actual) in actual_tokens.iter().enumerate() { + println!("Actual token at index {i}: {actual:?}"); + } + + for (i, expected) in expected_tokens.iter().enumerate() { + println!("Expected token at index {i}: {expected:?}"); + } + return false; + } + + for (actual, expected) in actual_tokens.iter().zip(expected_tokens.iter()) { + let equal = compare_token_tree(actual, expected); + if !equal { + println!("Tokens do not match: {actual:?} != {expected:?}"); + return false; + } + } + true + } + + use azure_core::{ + http::{ClientOptions, Url}, + tracing, + }; + use std::sync::Arc; + + #[tracing::client] + pub struct MyServiceClient { + endpoint: Url, + } + + #[derive(Default)] + pub struct MyServiceClientOptions { + pub client_options: ClientOptions, + } + + impl MyServiceClient { + #[tracing::new("MyServiceClientNamespace")] + pub fn new( + endpoint: &str, + _credential: Arc, + options: Option, + ) -> Self { + let options = options.unwrap_or_default(); + let url = Url::parse(endpoint).expect("Invalid endpoint URL"); + Self { endpoint: url } + } + } +} diff --git a/sdk/core/azure_core_macros/src/tracing_client.rs b/sdk/core/azure_core_macros/src/tracing_client.rs new file mode 100644 index 0000000000..bde8f8350e --- /dev/null +++ b/sdk/core/azure_core_macros/src/tracing_client.rs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{spanned::Spanned, ItemStruct, Result}; + +const INVALID_SERVICE_CLIENT_MESSAGE: &str = "client attribute must be applied to a public struct"; + +/// Parse the token stream for an Azure Service client declaration. +/// +/// An Azure Service client is a public struct that represents a client for an Azure service. +/// +/// This macro will ensure that the struct is public and has a `tracer` field of type `Option`. +/// +pub fn parse_client(_attr: TokenStream, item: TokenStream) -> Result { + if !is_client_declaration(&item) { + return Err(syn::Error::new(item.span(), INVALID_SERVICE_CLIENT_MESSAGE)); + } + + let client_struct: ItemStruct = syn::parse2(item.clone())?; + + let vis = &client_struct.vis; + let ident = &client_struct.ident; + let fields = client_struct.fields.iter(); + Ok(quote! { + #vis + struct #ident { + #(#fields),*, + tracer: Option>, + } + }) +} + +/// Returns true if the item at the head of the token stream is a valid service client declaration. +fn is_client_declaration(item: &TokenStream) -> bool { + let item_struct: ItemStruct = match syn::parse2(item.clone()) { + Ok(struct_item) => struct_item, + Err(_) => return false, + }; + + // Service clients must be public structs. + if !matches!(item_struct.vis, syn::Visibility::Public(_)) { + return false; + } + true +} + +#[cfg(test)] +mod tests { + use super::*; + + // cspell: ignore punct + #[test] + fn parse_service_client() { + let attr = TokenStream::new(); + let item = quote! { + pub struct ServiceClient { + name: &'static str, + endpoint: Url, + } + }; + let actual = parse_client(attr, item).expect("Failed to parse client declaration"); + let expected = quote! { + pub struct ServiceClient { + name: &'static str, + endpoint: Url, + tracer: Option>, + } + }; + // println!("Parsed tokens: {:?}", tokens); + // println!("Expected tokens: {:?}", expected); + + assert!( + crate::tracing::tests::compare_token_stream(actual, expected), + "Parsed tokens do not match expected tokens" + ); + } + + #[test] + fn parse_not_service_client() { + let attr = TokenStream::new(); + let item = quote! { + fn NotServiceClient(&self, name: &'static str) -> Result<(), Box> { + Ok(()) + } + }; + assert!( + parse_client(attr, item).is_err(), + "Expected error for non-client declaration" + ); + } +} diff --git a/sdk/core/azure_core_macros/src/tracing_function.rs b/sdk/core/azure_core_macros/src/tracing_function.rs new file mode 100644 index 0000000000..71c4051300 --- /dev/null +++ b/sdk/core/azure_core_macros/src/tracing_function.rs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{spanned::Spanned, ItemFn, Result}; + +const INVALID_PUBLIC_FUNCTION_MESSAGE: &str = + "function attribute must be applied to a public function returning a Result."; + +// cspell: ignore asyncness + +/// Parse the token stream for an Azure Service client "new" declaration. +/// +/// An Azure Service client "new" declaration is a public function whose name starts with +/// `new` and returns either a new client instance or an error. +/// +/// This macro will ensure that the fn is public and returns one of the following: +/// 1) `Self` +/// 1) `Arc` +/// 1) `Result` +/// 1) `Result, E>` +/// +pub fn parse_function(_attr: TokenStream, item: TokenStream) -> Result { + if !is_function_declaration(&item) { + return Err(syn::Error::new( + item.span(), + INVALID_PUBLIC_FUNCTION_MESSAGE, + )); + } + + let client_fn: ItemFn = syn::parse2(item.clone())?; + + let vis = &client_fn.vis; + let asyncness = &client_fn.sig.asyncness; + let ident = &client_fn.sig.ident; + let inputs = client_fn.sig.inputs.iter(); + let body = client_fn.block.stmts.iter(); + let output = &client_fn.sig.output; + + Ok(quote! { + #vis #asyncness + fn #ident(#(#inputs),*) #output { + let mut options = options.unwrap_or_default(); + let mut ctx = options.method_options.context.clone(); + let span = if ctx.value::>().is_none() { + if let Some(tracer) = &self.tracer { + let mut attributes = Vec::new(); + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + attributes.push(azure_core::tracing::Attribute { + key: "az.namespace", + value: namespace.into(), + }); + } + let span = tracer.start_span( + stringify!(#ident), + azure_core::tracing::SpanKind::Internal, + attributes, + ); + ctx = ctx.with_value(span.clone()); + ctx = ctx.with_value(tracer.clone()); + Some(span) + } else { + None + } + } else { + None + }; + options.method_options.context = ctx; + let options = Some(options); + #(#body)* + } + }) +} + +fn is_function_declaration(item: &TokenStream) -> bool { + let item_fn: ItemFn = match syn::parse2(item.clone()) { + Ok(fn_item) => fn_item, + Err(_) => return false, + }; + + // Function must be public. + if !matches!(item_fn.vis, syn::Visibility::Public(_)) { + return false; + } + + // Function must return a Result type. + if let syn::ReturnType::Type(_, ty) = &item_fn.sig.output { + if !matches!(ty.as_ref(), syn::Type::Path(_)) { + return false; + } + } else { + return false; + } + + true +} diff --git a/sdk/core/azure_core_macros/src/tracing_new.rs b/sdk/core/azure_core_macros/src/tracing_new.rs new file mode 100644 index 0000000000..9c68e580c2 --- /dev/null +++ b/sdk/core/azure_core_macros/src/tracing_new.rs @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{parse::Parse, spanned::Spanned, ExprStruct, ItemFn, Result}; + +const INVALID_SERVICE_CLIENT_NEW_MESSAGE: &str = + "new attribute must be applied to a public function with a name starting with `new`"; + +fn parse_struct_expr( + client_namespace: &str, + struct_body: &ExprStruct, + default: TokenStream, + is_ok: bool, +) -> TokenStream { + if struct_body.path.is_ident("Self") { + let fields = struct_body.fields.iter(); + let before_self = quote! { + let tracer = + if let Some(tracer_options) = &options.client_options.request_instrumentation { + tracer_options + .tracing_provider + .as_ref() + .map(|tracing_provider| { + tracing_provider.get_tracer( + Some(#client_namespace), + option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + ) + }) + } else { + None + }; + }; + if is_ok { + quote! { + #before_self + Ok(Self { + tracer, + #(#fields),*, + }) + } + } else { + quote! { + #before_self + Self { + tracer, + #(#fields),*, + } + } + } + } else { + println!("ident is not Self, emitting expression: {default:?}"); + default + } +} + +struct NamespaceAttribute { + client_namespace: String, +} + +impl Parse for NamespaceAttribute { + fn parse(input: syn::parse::ParseStream) -> Result { + let client_namespace = input.parse::()?.value(); + Ok(NamespaceAttribute { client_namespace }) + } +} + +/// Parse the token stream for an Azure Service client "new" declaration. +/// +/// An Azure Service client "new" declaration is a public function whose name starts with +/// `new` and returns either a new client instance or an error. +/// +/// This macro will ensure that the fn is public and returns one of the following: +/// 1) `Self` +/// 1) `Arc` +/// 1) `Result` +/// 1) `Result, E>` +/// +pub fn parse_new(attr: TokenStream, item: TokenStream) -> Result { + if !is_new_declaration(&item) { + return Err(syn::Error::new( + item.span(), + INVALID_SERVICE_CLIENT_NEW_MESSAGE, + )); + } + let namespace_attrs: NamespaceAttribute = syn::parse2(attr)?; + + let client_fn: ItemFn = syn::parse2(item.clone())?; + + let vis = &client_fn.vis; + let ident = &client_fn.sig.ident; + let inputs = client_fn.sig.inputs.iter(); + let body = client_fn.block.stmts.iter().map(|stmt| { + // Ensure that the body of the new function initializes the `tracer` field. + + if let syn::Stmt::Expr(expr, _) = stmt { + if let syn::Expr::Call(c) = expr { + // If the expression is a call, we need to check if it is a struct initialization. + if c.args.len() != 1 { + println!("Call expression does not have exactly one argument, emitting expression: {stmt:?}"); + // If the call does not have exactly one argument, just return it as is. + quote! {#stmt} + } else if let syn::Expr::Struct(struct_body) = &c.args[0] { + parse_struct_expr(namespace_attrs.client_namespace.as_str(), struct_body, stmt.to_token_stream(), true) + } else { + println!("Call expression is not a struct, emitting expression: {stmt:?}"); + // If the expression is not a struct, just return it as is. + stmt.to_token_stream() + } + } else if let syn::Expr::Struct(struct_body) = expr { + parse_struct_expr(namespace_attrs.client_namespace.as_str(), struct_body, stmt.to_token_stream(), false) + } else { + // If the expression is not a struct, just return it as is. + stmt.to_token_stream() + } + } else { + stmt.to_token_stream() + } + }); + let output = &client_fn.sig.output; + Ok(quote! { + #vis + fn #ident(#(#inputs),*) #output { + #(#body)* + } + }) +} + +/// Returns true if the item at the head of the token stream is a valid service client declaration. +fn is_new_declaration(item: &TokenStream) -> bool { + let item_fn: ItemFn = match syn::parse2(item.clone()) { + Ok(fn_item) => fn_item, + Err(_) => return false, + }; + + // Service clients new functions must be public. + if !matches!(item_fn.vis, syn::Visibility::Public(_)) { + return false; + } + + // Service clients new functions must have a name that starts with `new_`. + if !item_fn.sig.ident.to_string().starts_with("new") { + return false; + } + + true +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_new_function() { + let attr = quote!("Az.Namespace"); + let item = quote! { + pub fn new_service_client(name: &'static str, endpoint: Url) -> Self { + let function = newtype::new(); + println!("Function: {:?}", function); + i = i + 1; + let this = Self { + name, + endpoint, + }; + Self { + name, + endpoint, + } + } + }; + let actual = parse_new(attr, item).expect("Failed to parse new function declaration"); + println!("Parsed tokens: {actual}"); + + let expected = quote! { + pub fn new_service_client(name: &'static str, endpoint: Url) -> Self { + let function = newtype::new(); + println!("Function: {:?}", function); + i = i + 1; + let this = Self { + name, + endpoint, + }; + let tracer = if let Some(tracer_options) = + &options.client_options.request_instrumentation + { + tracer_options + .tracing_provider + .as_ref() + .map(|tracing_provider| { + tracing_provider.get_tracer( + Some("Az.Namespace"), + option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + ) + }) + } else { + None + }; + Self { + tracer, + name, + endpoint, + } + } + }; + assert!( + crate::tracing::tests::compare_token_stream(actual, expected), + "Parsed tokens do not match expected tokens" + ); + } + + #[test] + fn parse_generated_new() { + let attr = quote!("Az.GeneratedNamespace"); + let new_function = quote! { + pub fn new( + endpoint: &str, + credential: Arc, + options: Option, + ) -> Result { + let options = options.unwrap_or_default(); + let mut endpoint = Url::parse(endpoint)?; + if !endpoint.scheme().starts_with("http") { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("{endpoint} must use http(s)"), + )); + } + endpoint.set_query(None); + let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( + credential, + vec!["https://vault.azure.net/.default"], + )); + Ok(Self { + endpoint, + api_version: options.api_version, + pipeline: Pipeline::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options.client_options, + Vec::default(), + vec![auth_policy], + ), + }) + } + }; + let actual = + parse_new(attr, new_function).expect("Failed to parse new function declaration"); + + println!("Parsed tokens: {actual}"); + + // I am not at all sure why the parameters to `new` are not being parsed correctly - + // the trailing comma in the `new_function` token stream is not present. + let expected = quote! { + pub fn new( + endpoint: &str, + credential: Arc, + options: Option + ) -> Result { + let options = options.unwrap_or_default(); + let mut endpoint = Url::parse(endpoint)?; + if !endpoint.scheme().starts_with("http") { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("{endpoint} must use http(s)"), + )); + } + endpoint.set_query(None); + let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( + credential, + vec!["https://vault.azure.net/.default"], + )); + let tracer = if let Some(tracer_options) = + &options.client_options.request_instrumentation + { + tracer_options + .tracing_provider + .as_ref() + .map(|tracing_provider| { + tracing_provider.get_tracer( + Some("Az.GeneratedNamespace"), + option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + ) + }) + } else { + None + }; + Ok(Self { + tracer, + endpoint, + api_version: options.api_version, + pipeline: Pipeline::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options.client_options, + Vec::default(), + vec![auth_policy], + ), + }) + } + }; + assert!( + crate::tracing::tests::compare_token_stream(actual, expected), + "Parsed tokens do not match expected tokens" + ); + } +} diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index a87a8db7f9..c97a2433a1 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -15,7 +15,7 @@ To integrate the OpenTelemetry APIs with the Azure SDK for Rust, you create a [` # use azure_core::{http::{ClientOptions, RequestInstrumentationOptions}}; # #[derive(Default)] # struct ServiceClientOptions { -# azure_client_options: ClientOptions, +# client_options: ClientOptions, # } use azure_core_opentelemetry::OpenTelemetryTracerProvider; use opentelemetry_sdk::trace::SdkTracerProvider; @@ -28,7 +28,7 @@ let otel_tracer_provider = Arc::new(SdkTracerProvider::builder().build()); let azure_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); let options = ServiceClientOptions { - azure_client_options: ClientOptions { + client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), }), @@ -48,7 +48,7 @@ If it is more convenient to use the global OpenTelemetry provider, then the [`Op # use azure_core::{http::{ClientOptions, RequestInstrumentationOptions}}; # #[derive(Default)] # struct ServiceClientOptions { -# azure_client_options: ClientOptions, +# client_options: ClientOptions, # } use azure_core_opentelemetry::OpenTelemetryTracerProvider; use opentelemetry_sdk::trace::SdkTracerProvider; @@ -59,7 +59,7 @@ use std::sync::Arc; let azure_provider = OpenTelemetryTracerProvider::new_from_global_provider(); let options = ServiceClientOptions { - azure_client_options: ClientOptions { + client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), }), diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 57e3b9aade..f59a147afb 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -10,7 +10,7 @@ use azure_core::{ ClientMethodOptions, ClientOptions, Pipeline, RawResponse, Request, RequestInstrumentationOptions, Url, }, - tracing::{Attribute, Tracer}, + tracing::{Attribute, Span, Tracer}, Result, }; use azure_core_opentelemetry::OpenTelemetryTracerProvider; @@ -68,8 +68,8 @@ impl TestServiceClient { .map(|tracing_provider| { tracing_provider.get_tracer( Some("Az.TestServiceClient"), - option_env!("CARGO_PKG_NAME").unwrap_or("test_service_client"), - option_env!("CARGO_PKG_VERSION").unwrap_or("0.1.0"), + option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), ) }) } else { @@ -155,27 +155,31 @@ impl TestServiceClient { ) -> Result { let mut options = options.unwrap_or_default(); let mut ctx = options.method_options.context.clone(); - let span = if let Some(tracer) = &self.tracer { - let mut attributes = Vec::new(); - if let Some(namespace) = tracer.namespace() { - // If the tracer has a namespace, we set it as an attribute. - attributes.push(Attribute { - key: "az.namespace", - value: namespace.into(), - }); + let span = if ctx.value::>().is_none() { + if let Some(tracer) = &self.tracer { + let mut attributes = Vec::new(); + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + attributes.push(Attribute { + key: "az.namespace", + value: namespace.into(), + }); + } + let span = tracer.start_span( + "get_with_tracing", + azure_core::tracing::SpanKind::Internal, + attributes, + ); + // We need to add the span to the context because the pipeline will use it as the parent span + // for the request span. + ctx = ctx.with_value(span.clone()); + // And we need to add the tracer to the context so that the pipeline can use it to populate the + // az.namespace property in the request span. + ctx = ctx.with_value(tracer.clone()); + Some(span) + } else { + None } - let span = tracer.start_span( - "get_with_tracing", - azure_core::tracing::SpanKind::Internal, - attributes, - ); - // We need to add the span to the context because the pipeline will use it as the parent span - // for the request span. - ctx = ctx.with_value(span.clone()); - // And we need to add the tracer to the context so that the pipeline can use it to populate the - // az.namespace property in the request span. - ctx = ctx.with_value(tracer.clone()); - Some(span) } else { None }; @@ -211,13 +215,13 @@ impl TestServiceClient { #[cfg(test)] mod tests { use super::*; + use ::tracing::{info, trace}; use azure_core::Result; use azure_core_test::{recorded, TestContext}; use opentelemetry::trace::{ SpanKind as OpenTelemetrySpanKind, Status as OpenTelemetrySpanStatus, }; use opentelemetry::Value as OpenTelemetryAttributeValue; - use tracing::{info, trace}; fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { let otel_exporter = InMemorySpanExporter::default(); diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs new file mode 100644 index 0000000000..e0dc9b4db4 --- /dev/null +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs @@ -0,0 +1,584 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//! This file contains an Azure SDK for Rust fake service client API. +//! +use azure_core::{ + credentials::TokenCredential, + fmt::SafeDebug, + http::{ + ClientMethodOptions, ClientOptions, Pipeline, RawResponse, Request, + RequestInstrumentationOptions, Url, + }, + tracing, + tracing::Attribute, + Result, +}; +use azure_core_opentelemetry::OpenTelemetryTracerProvider; +use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider}; +use std::sync::Arc; + +#[derive(Clone, SafeDebug)] +pub struct TestServiceClientOptions { + pub client_options: ClientOptions, + pub api_version: Option, +} + +impl Default for TestServiceClientOptions { + fn default() -> Self { + Self { + client_options: ClientOptions::default(), + api_version: Some("2023-10-01".to_string()), + } + } +} + +/// Define a TestServiceClient which is a fake service client for testing purposes. +/// This client demonstrates how to implement a service client using the tracing convenience proc macros. +#[tracing::client] +pub struct TestServiceClient { + endpoint: Url, + api_version: String, + pipeline: Pipeline, +} + +#[derive(Default, SafeDebug)] +pub struct TestServiceClientGetMethodOptions<'a> { + pub method_options: ClientMethodOptions<'a>, +} + +impl TestServiceClient { + /// Creates a new instance of the TestServiceClient. + /// + /// This function demonstrates how to create a service client using the tracing convenience proc macros. + /// + /// # Arguments + /// * `endpoint` - The endpoint URL for the service. + /// * `_credential` - The credential used for authentication (not used in this example). + /// * `options` - Optional client options to configure the client. + /// + #[tracing::new("Az.TestServiceClient")] + pub fn new( + endpoint: &str, + _credential: Arc, + options: Option, + ) -> Result { + let options = options.unwrap_or_default(); + let mut endpoint = Url::parse(endpoint)?; + if !endpoint.scheme().starts_with("http") { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("{endpoint} must use http(s)"), + )); + } + endpoint.set_query(None); + + Ok(Self { + endpoint, + api_version: options.api_version.unwrap_or_default(), + pipeline: Pipeline::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options.client_options, + Vec::default(), + Vec::default(), + ), + }) + } + + /// Returns the Url associated with this client. + pub fn endpoint(&self) -> &Url { + &self.endpoint + } + + /// Returns the result of a Get verb against the configured endpoint with the specified path. + /// + /// This method demonstrates a service client which does not have per-method spans but which will create + /// HTTP client spans if the `RequestInstrumentationOptions` are configured in the client options. + /// + pub async fn get( + &self, + path: &str, + options: Option>, + ) -> Result { + let options = options.unwrap_or_default(); + let mut url = self.endpoint.clone(); + url.set_path(path); + url.query_pairs_mut() + .append_pair("api-version", &self.api_version); + + let mut request = Request::new(url, azure_core::http::Method::Get); + + let response = self + .pipeline + .send(&options.method_options.context, &mut request) + .await?; + if !response.status().is_success() { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::HttpResponse { + status: response.status(), + error_code: None, + }, + format!("Failed to GET {}: {}", request.url(), response.status()), + )); + } + Ok(response) + } + + /// Returns the result of a Get verb against the configured endpoint with the specified path. + /// + /// This method demonstrates a service client which has per-method spans and uses the configured tracing + /// tracing provider to create per-api spans for the function. + /// + /// To configure per-api spans, your service implementation needs to do the following: + /// 1. If the client is configured with a [`Tracer`], it will create a span whose name matches the function. + /// 1. The span should be created with the `SpanKind::Internal` kind, and + /// 2. The span should have the `az.namespace` attribute set to the namespace of the service client. + /// 2. The function should add the span created in step 1 to the ClientMethodOptions context. + /// 3. The function should add the tracer to the ClientMethodOptions context so that the pipeline can use it to populate the `az.namespace` property in the request span. + /// 4. The function should then perform the normal client operations after setting up the context. + /// 5. After the client operation completes, if the function failed, it should add an `error.type` attribute to the span + /// with the error type. + /// + /// # Note + /// This applies to most HTTP client operations, but not all. CosmosDB has its own set of conventions as listed + /// [here](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/cosmosdb.md) + /// + #[tracing::function] + pub async fn get_with_function_tracing( + &self, + path: &str, + options: Option>, + ) -> Result { + let mut options = options.unwrap_or_default(); + let mut ctx = options.method_options.context.clone(); + let span = if ctx.value::>().is_none() { + if let Some(tracer) = &self.tracer { + let mut attributes = Vec::new(); + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + attributes.push(Attribute { + key: "az.namespace", + value: namespace.into(), + }); + } + let span = tracer.start_span( + "get_with_tracing", + azure_core::tracing::SpanKind::Internal, + attributes, + ); + // We need to add the span to the context because the pipeline will use it as the parent span + // for the request span. + ctx = ctx.with_value(span.clone()); + // And we need to add the tracer to the context so that the pipeline can use it to populate the + // az.namespace property in the request span. + ctx = ctx.with_value(tracer.clone()); + Some(span) + } else { + None + } + } else { + None + }; + options.method_options.context = ctx; + let response = self.get(path, Some(options)).await; + if let Some(span) = span { + if let Err(e) = &response { + // If the request failed, we set the error type on the span. + match e.kind() { + azure_core::error::ErrorKind::HttpResponse { status, .. } => { + span.set_attribute("error.type", status.to_string().into()); + if status.is_server_error() || status.is_client_error() { + span.set_status(azure_core::tracing::SpanStatus::Error { + description: "".to_string(), + }); + } + } + _ => { + span.set_attribute("error.type", e.kind().to_string().into()); + span.set_status(azure_core::tracing::SpanStatus::Error { + description: e.kind().to_string(), + }); + } + } + } + + span.end(); + }; + response + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ::tracing::{info, trace}; + use azure_core::Result; + use azure_core_test::{recorded, TestContext}; + use opentelemetry::trace::{ + SpanKind as OpenTelemetrySpanKind, Status as OpenTelemetrySpanStatus, + }; + use opentelemetry::Value as OpenTelemetryAttributeValue; + + fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { + let otel_exporter = InMemorySpanExporter::default(); + let otel_tracer_provider = SdkTracerProvider::builder() + .with_simple_exporter(otel_exporter.clone()) + .build(); + let otel_tracer_provider = Arc::new(otel_tracer_provider); + (otel_tracer_provider, otel_exporter) + } + + // Span verification utility functions. + + struct ExpectedSpan { + name: &'static str, + kind: OpenTelemetrySpanKind, + parent_span_id: Option, + status: OpenTelemetrySpanStatus, + attributes: Vec<(&'static str, OpenTelemetryAttributeValue)>, + } + + fn verify_span( + span: &opentelemetry_sdk::trace::SpanData, + expected: ExpectedSpan, + ) -> Result<()> { + assert_eq!(span.name, expected.name); + assert_eq!(span.span_kind, expected.kind); + assert_eq!(span.status, expected.status); + assert_eq!( + span.parent_span_id, + expected + .parent_span_id + .unwrap_or(opentelemetry::trace::SpanId::INVALID) + ); + + for attr in span.attributes.iter() { + println!("Attribute: {} = {:?}", attr.key, attr.value); + let mut found = false; + for (key, value) in expected.attributes.iter() { + if attr.key.as_str() == (*key) { + found = true; + // Skip checking the value for "" as it is a placeholder + if *value != OpenTelemetryAttributeValue::String("".into()) { + assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", *key); + } + break; + } + } + if !found { + panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); + } + } + for (key, value) in expected.attributes.iter() { + if !span.attributes.iter().any(|attr| attr.key == (*key).into()) { + panic!("Expected attribute not found: {} = {:?}", key, value); + } + } + + Ok(()) + } + + // Basic functionality tests. + #[recorded::test()] + async fn test_service_client_new(ctx: TestContext) -> Result<()> { + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + assert_eq!(client.endpoint().as_str(), "https://example.com/"); + assert_eq!(client.api_version, "2023-10-01"); + + Ok(()) + } + + // Ensure that the the test client actually does what it's supposed to do without telemetry. + #[recorded::test()] + async fn test_service_client_get(ctx: TestContext) -> Result<()> { + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + + let client = TestServiceClient::new(endpoint, credential, None).unwrap(); + let response = client.get("index.html", None).await; + info!("Response: {:?}", response); + assert!(response.is_ok()); + let response = response.unwrap(); + assert_eq!(response.status(), azure_core::http::StatusCode::Ok); + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("index.html", None).await; + info!("Response: {:?}", response); + assert!(response.is_ok()); + let response = response.unwrap(); + assert_eq!(response.status(), azure_core::http::StatusCode::Ok); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + for span in &spans { + trace!("Span: {:?}", span); + + verify_span( + span, + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + status: OpenTelemetrySpanStatus::Unset, + parent_span_id: None, + attributes: vec![ + ("http.request.method", "GET".into()), + ("url.scheme", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "index.html?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend_count", 0.into()), + ("http.response.status_code", 200.into()), + ], + }, + )?; + } + + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("failing_url", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + for span in &spans { + trace!("Span: {:?}", span); + + verify_span( + span, + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Error { + description: "".into(), + }, + attributes: vec![ + ("http.request.method", "GET".into()), + ("url.scheme", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("error.type", "404".into()), + ("http.request.resend_count", 0.into()), + ("http.response.status_code", 404.into()), + ], + }, + )?; + } + + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get_with_function_tracing("index.html", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + for span in &spans { + trace!("Span: {:?}", span); + } + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[1].span_context.span_id()), + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("url.scheme", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "index.html?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend_count", 0.into()), + ("http.response.status_code", 200.into()), + ], + }, + )?; + verify_span( + &spans[1], + ExpectedSpan { + name: "get_with_tracing", + kind: OpenTelemetrySpanKind::Internal, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![("az.namespace", "Az.TestServiceClient".into())], + }, + )?; + + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get_with_function_tracing("failing_url", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + for span in &spans { + trace!("Span: {:?}", span); + } + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[1].span_context.span_id()), + status: OpenTelemetrySpanStatus::Error { + description: "".into(), + }, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("url.scheme", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend_count", 0.into()), + ("http.response.status_code", 404.into()), + ("error.type", "404".into()), + ], + }, + )?; + verify_span( + &spans[1], + ExpectedSpan { + name: "get_with_tracing", + kind: OpenTelemetrySpanKind::Internal, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Error { + description: "".into(), + }, + attributes: vec![ + ("az.namespace", "Az.TestServiceClient".into()), + ("error.type", "404".into()), + ], + }, + )?; + + Ok(()) + } +} From 2017281451a008f32eb16bddc5438a0f58deadcd Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 25 Jun 2025 11:29:02 -0700 Subject: [PATCH 23/84] PR feedback from previous PR --- sdk/core/azure_core/src/lib.rs | 4 +- .../src/attributes.rs | 20 ++++------ sdk/core/azure_core_opentelemetry/src/span.rs | 38 ++++++++----------- .../azure_core_opentelemetry/src/tracer.rs | 26 +++++-------- .../tests/integration_test.rs | 8 ++-- .../src/tracing/attributes.rs | 15 +++++++- .../typespec_client_core/src/tracing/mod.rs | 10 ++--- 7 files changed, 56 insertions(+), 65 deletions(-) diff --git a/sdk/core/azure_core/src/lib.rs b/sdk/core/azure_core/src/lib.rs index 97182941cb..edf21fa808 100644 --- a/sdk/core/azure_core/src/lib.rs +++ b/sdk/core/azure_core/src/lib.rs @@ -27,9 +27,7 @@ pub use typespec_client_core::{ fmt, json, sleep, stream, time, Bytes, Uuid, }; -pub mod tracing { - pub use typespec_client_core::tracing::*; -} +pub use typespec_client_core::tracing; #[cfg(feature = "xml")] pub use typespec_client_core::xml; diff --git a/sdk/core/azure_core_opentelemetry/src/attributes.rs b/sdk/core/azure_core_opentelemetry/src/attributes.rs index b15101cc7c..4b389d5420 100644 --- a/sdk/core/azure_core_opentelemetry/src/attributes.rs +++ b/sdk/core/azure_core_opentelemetry/src/attributes.rs @@ -24,9 +24,9 @@ impl From for AttributeValue { } } -impl From for AttributeValue { - fn from(value: u64) -> Self { - AttributeValue(AzureAttributeValue::U64(value)) +impl From for AttributeValue { + fn from(value: f64) -> Self { + AttributeValue(AzureAttributeValue::I64(value as i64)) } } @@ -47,10 +47,9 @@ impl From> for AttributeArray { AttributeArray(AzureAttributeArray::I64(values)) } } - -impl From> for AttributeArray { - fn from(values: Vec) -> Self { - AttributeArray(AzureAttributeArray::U64(values)) +impl From> for AttributeArray { + fn from(values: Vec) -> Self { + AttributeArray(AzureAttributeArray::F64(values)) } } @@ -66,7 +65,7 @@ impl From for opentelemetry::Value { match value.0 { AzureAttributeValue::Bool(b) => opentelemetry::Value::Bool(b), AzureAttributeValue::I64(i) => opentelemetry::Value::I64(i), - AzureAttributeValue::U64(u) => opentelemetry::Value::I64(u as i64), + AzureAttributeValue::F64(u) => opentelemetry::Value::F64(u), AzureAttributeValue::String(s) => opentelemetry::Value::String(s.into()), AzureAttributeValue::Array(arr) => { opentelemetry::Value::Array(opentelemetry::Array::from(AttributeArray(arr))) @@ -81,10 +80,7 @@ impl From for opentelemetry::Array { match array.0 { AzureAttributeArray::Bool(values) => values.into(), AzureAttributeArray::I64(values) => values.into(), - AzureAttributeArray::U64(values) => { - let i64_values: Vec = values.into_iter().map(|v| v as i64).collect(); - i64_values.into() - } + AzureAttributeArray::F64(values) => values.into(), AzureAttributeArray::String(values) => { let string_values: Vec = values.into_iter().map(|s| s.into()).collect(); diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 2981e3c10c..62c861a5b2 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -140,7 +140,7 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Client).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Client); assert!(span.end().is_ok()); let spans = otel_exporter.get_finished_spans().unwrap(); @@ -159,10 +159,9 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let parent_span = tracer.start_span("parent_span", SpanKind::Server).unwrap(); - let child_span = tracer - .start_span_with_parent("child_span", SpanKind::Client, parent_span.clone()) - .unwrap(); + let parent_span = tracer.start_span("parent_span", SpanKind::Server); + let child_span = + tracer.start_span_with_parent("child_span", SpanKind::Client, parent_span.clone()); assert!(child_span.end().is_ok()); assert!(parent_span.end().is_ok()); @@ -188,11 +187,10 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span1 = tracer.start_span("span1", SpanKind::Internal).unwrap(); - let span2 = tracer.start_span("span2", SpanKind::Server).unwrap(); - let child_span = tracer - .start_span_with_parent("child_span", SpanKind::Client, span1.clone()) - .unwrap(); + let span1 = tracer.start_span("span1", SpanKind::Internal); + let span2 = tracer.start_span("span2", SpanKind::Server); + let child_span = + tracer.start_span_with_parent("child_span", SpanKind::Client, span1.clone()); assert!(child_span.end().is_ok()); assert!(span2.end().is_ok()); @@ -219,14 +217,12 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span1 = tracer.start_span("span1", SpanKind::Internal).unwrap(); - let span2 = tracer.start_span("span2", SpanKind::Server).unwrap(); + let span1 = tracer.start_span("span1", SpanKind::Internal); + let span2 = tracer.start_span("span2", SpanKind::Server); let _span_guard = span2 .set_current(&azure_core::http::Context::new()) .unwrap(); - let child_span = tracer - .start_span_with_current("child_span", SpanKind::Client) - .unwrap(); + let child_span = tracer.start_span_with_current("child_span", SpanKind::Client); assert!(child_span.end().is_ok()); assert!(span2.end().is_ok()); @@ -253,7 +249,7 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); assert!(span .set_attribute("test_key", AttributeValue::String("test_value".to_string())) @@ -279,7 +275,7 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Client).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Client); let error = Error::new(ErrorKind::NotFound, "resource not found"); assert!(span.record_error(&error).is_ok()); @@ -308,14 +304,12 @@ mod tests { let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); // Test Ok status - let span = tracer.start_span("test_span_ok", SpanKind::Server).unwrap(); + let span = tracer.start_span("test_span_ok", SpanKind::Server); assert!(span.set_status(SpanStatus::Ok).is_ok()); assert!(span.end().is_ok()); // Test Error status - let span = tracer - .start_span("test_span_error", SpanKind::Server) - .unwrap(); + let span = tracer.start_span("test_span_error", SpanKind::Server); assert!(span .set_status(SpanStatus::Error { description: "test error".to_string() @@ -355,7 +349,7 @@ mod tests { 42 }; - let span = tracer.start_span("test_span", SpanKind::Client).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Client); let azure_context = AzureContext::new(); let azure_context = azure_context.with_value(span.clone()); diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 9eb9e0fe7f..d32ff66241 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -2,10 +2,7 @@ // Licensed under the MIT License. use crate::span::{OpenTelemetrySpan, OpenTelemetrySpanKind}; -use azure_core::{ - tracing::{SpanKind, Tracer}, - Result, -}; +use azure_core::tracing::{SpanKind, Tracer}; use opentelemetry::{ global::BoxedTracer, trace::{TraceContextExt, Tracer as OpenTelemetryTracerTrait}, @@ -29,26 +26,26 @@ impl Tracer for OpenTelemetryTracer { &self, name: &'static str, kind: SpanKind, - ) -> Result> { + ) -> Arc { let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()); let context = Context::new(); let span = self.inner.build_with_context(span_builder, &context); - Ok(OpenTelemetrySpan::new(context.with_span(span))) + OpenTelemetrySpan::new(context.with_span(span)) } fn start_span_with_current( &self, name: &'static str, kind: SpanKind, - ) -> Result> { + ) -> Arc { let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()); let context = Context::current(); let span = self.inner.build_with_context(span_builder, &context); - Ok(OpenTelemetrySpan::new(context.with_span(span))) + OpenTelemetrySpan::new(context.with_span(span)) } fn start_span_with_parent( @@ -56,7 +53,7 @@ impl Tracer for OpenTelemetryTracer { name: &'static str, kind: SpanKind, parent: Arc, - ) -> Result> { + ) -> Arc { let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()); @@ -64,17 +61,12 @@ impl Tracer for OpenTelemetryTracer { let context = parent .as_any() .downcast_ref::() - .ok_or_else(|| { - azure_core::Error::message( - azure_core::error::ErrorKind::DataConversion, - "Could not downcast parent span to OpenTelemetrySpan", - ) - })? + .expect("Could not downcast parent span to OpenTelemetrySpan") .context() .clone(); let span = self.inner.build_with_context(span_builder, &context); - Ok(OpenTelemetrySpan::new(context.with_span(span))) + OpenTelemetrySpan::new(context.with_span(span)) } } @@ -91,7 +83,7 @@ mod tests { let noop_tracer = NoopTracerProvider::new(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); assert!(span.end().is_ok()); } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index 65c17b31af..584e48b8ee 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use azure_core::tracing::{SpanKind, TracerProvider}; +use azure_core::tracing::{SpanKind, TracerProvider as _}; use azure_core_opentelemetry::OpenTelemetryTracerProvider; use opentelemetry_sdk::trace::SdkTracerProvider; use std::error::Error; @@ -17,7 +17,7 @@ async fn test_span_creation() -> Result<(), Box> { let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); // Create a span using the Azure tracer - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); // Add attributes to the span using individual set_attribute calls span.set_attribute( @@ -43,7 +43,7 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { // Get a tracer and verify it works let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); span.end()?; Ok(()) @@ -59,7 +59,7 @@ async fn test_span_attributes() -> Result<(), Box> { let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); // Create span with multiple attributes - let span = tracer.start_span("test_span", SpanKind::Internal).unwrap(); + let span = tracer.start_span("test_span", SpanKind::Internal); // Add attributes using individual set_attribute calls span.set_attribute( diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 66401bdd06..32eb53c593 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,17 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/// An array of homogeneous attribute values. pub enum AttributeArray { + /// An array of boolean values. Bool(Vec), + /// An array of 64-bit signed integers. I64(Vec), - U64(Vec), + /// An array of 64bit floating point values. + F64(Vec), + /// An array of strings. String(Vec), } +/// Represents a single attribute value, which can be of various types. pub enum AttributeValue { + /// A boolean attribute value. Bool(bool), + /// A signed 64-bit integer attribute value. I64(i64), - U64(u64), + /// A 64-bit floating point attribute value + F64(f64), + /// A string attribute value. String(String), + /// An array of attribute values. Array(AttributeArray), } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index bc2c25c59f..498a26d86c 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -4,7 +4,6 @@ //! Distributed tracing trait definitions //! use crate::http::Context; -use crate::Result; use std::sync::Arc; /// Overall architecture for distributed tracing in the SDK. @@ -48,8 +47,7 @@ pub trait Tracer { /// # Returns /// An `Arc` representing the started span. /// - fn start_span(&self, name: &'static str, kind: SpanKind) - -> Result>; + fn start_span(&self, name: &'static str, kind: SpanKind) -> Arc; /// Starts a new span with the given type, using the current span as the parent span. /// @@ -64,7 +62,7 @@ pub trait Tracer { &self, name: &'static str, kind: SpanKind, - ) -> Result>; + ) -> Arc; /// Starts a new child with the given name, type, and parent span. /// @@ -76,12 +74,14 @@ pub trait Tracer { /// # Returns /// An `Arc` representing the started span /// + /// Note: This method may panic if the parent span cannot be downcasted to the expected type. + /// fn start_span_with_parent( &self, name: &'static str, kind: SpanKind, parent: Arc, - ) -> Result>; + ) -> Arc; } pub enum SpanStatus { Unset, From 9dac777c4c95e27f1414ea2790bfe8f29157e9b3 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 25 Jun 2025 13:52:42 -0700 Subject: [PATCH 24/84] Start of a dummy service client upon which to hang tracing stuff --- Cargo.lock | 8 ++++++-- sdk/core/azure_core_opentelemetry/Cargo.toml | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 685cb51fa2..ff07b561ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,14 +206,18 @@ dependencies = [ name = "azure_core_opentelemetry" version = "0.1.0" dependencies = [ + "async-trait", "azure_core", + "azure_core_test", + "azure_core_test_macros", "log", - "opentelemetry", - "opentelemetry_sdk", + "opentelemetry 0.30.0", + "opentelemetry_sdk 0.30.0", "tokio", "tracing", "tracing-subscriber", "typespec_client_core", + "url", ] [[package]] diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index 5dd0903b06..ee4ef6329a 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -22,9 +22,13 @@ tracing.workspace = true typespec_client_core.workspace = true [dev-dependencies] +async-trait.workspace = true +azure_core_test = { workspace = true, features = ["tracing"] } +azure_core_test_macros.workspace = true opentelemetry_sdk = { version = "0.30", features = ["testing"] } tokio.workspace = true tracing-subscriber.workspace = true +url.workspace = true [lints] workspace = true From 381c005223ad5be30ed27ce3c35f60455ded0679 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Thu, 26 Jun 2025 15:25:30 -0700 Subject: [PATCH 25/84] Create RequestInstrumentationPolicy to add spans for HTTP operations --- sdk/core/azure_core/src/http/options/mod.rs | 4 + .../http/options/request_instrumentation.rs | 11 + sdk/core/azure_core/src/http/policies/mod.rs | 2 + .../http/policies/request_instrumentation.rs | 535 ++++++++++++++++++ .../src/attributes.rs | 17 +- sdk/core/azure_core_opentelemetry/src/span.rs | 103 ++-- .../azure_core_opentelemetry/src/telemetry.rs | 12 +- .../azure_core_opentelemetry/src/tracer.rs | 60 +- .../tests/integration_test.rs | 22 +- .../tests/telemetry_service_implementation.rs | 199 +++++++ .../src/http/policies/retry/mod.rs | 8 +- .../src/tracing/attributes.rs | 102 +++- .../typespec_client_core/src/tracing/mod.rs | 57 +- 13 files changed, 1021 insertions(+), 111 deletions(-) create mode 100644 sdk/core/azure_core/src/http/options/request_instrumentation.rs create mode 100644 sdk/core/azure_core/src/http/policies/request_instrumentation.rs create mode 100644 sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs diff --git a/sdk/core/azure_core/src/http/options/mod.rs b/sdk/core/azure_core/src/http/options/mod.rs index a9593c3563..70d8b6eded 100644 --- a/sdk/core/azure_core/src/http/options/mod.rs +++ b/sdk/core/azure_core/src/http/options/mod.rs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +mod request_instrumentation; mod user_agent; +pub use request_instrumentation::*; use std::sync::Arc; use typespec_client_core::http::policies::Policy; pub use typespec_client_core::http::{ @@ -27,6 +29,8 @@ pub struct ClientOptions { /// User-Agent telemetry options. pub user_agent: Option, + + pub request_instrumentation: Option, } impl ClientOptions { diff --git a/sdk/core/azure_core/src/http/options/request_instrumentation.rs b/sdk/core/azure_core/src/http/options/request_instrumentation.rs new file mode 100644 index 0000000000..d15dd01584 --- /dev/null +++ b/sdk/core/azure_core/src/http/options/request_instrumentation.rs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use std::sync::Arc; + +/// Policy options to enable distributed tracing. +#[derive(Clone, Debug, Default)] +pub struct RequestInstrumentationOptions { + /// Set the tracing provider for distributed tracing. + pub tracing_provider: Option>, +} diff --git a/sdk/core/azure_core/src/http/policies/mod.rs b/sdk/core/azure_core/src/http/policies/mod.rs index 81c8e3769a..2d07bf36f7 100644 --- a/sdk/core/azure_core/src/http/policies/mod.rs +++ b/sdk/core/azure_core/src/http/policies/mod.rs @@ -5,9 +5,11 @@ mod bearer_token_policy; mod client_request_id; +mod request_instrumentation; mod user_agent; pub use bearer_token_policy::BearerTokenCredentialPolicy; pub use client_request_id::*; +pub use request_instrumentation::*; pub use typespec_client_core::http::policies::*; pub use user_agent::*; diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs new file mode 100644 index 0000000000..9785269650 --- /dev/null +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -0,0 +1,535 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use crate::{ + http::{headers, options::RequestInstrumentationOptions, Context, Request}, + tracing::{Span, SpanKind}, +}; +use std::sync::Arc; +use typespec_client_core::{ + http::policies::{Policy, PolicyResult, RetryPolicyCount}, + tracing::Attribute, +}; + +#[allow(dead_code)] +const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; + +const AZ_SCHEMA_URL_ATTRIBUTE: &str = "az.schema.url"; +const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client.request.id"; +const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; +const AZ_SERVICE_REQUEST_ID_ATTRIBUTE: &str = "az.service.request.id"; +const HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE: &str = "http.request.resend.count"; +const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE: &str = "http.response.status_code"; +const HTTP_REQUEST_METHOD_ATTRIBUTE: &str = "http.request.method"; +const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; +const SERVER_PORT_ATTRIBUTE: &str = "server.port"; +const URL_FULL_ATTRIBUTE: &str = "url.full"; + +/// Sets the User-Agent header with useful information in a typical format for Azure SDKs. +#[derive(Clone, Debug)] +pub struct RequestInstrumentationPolicy { + tracer: Option>, +} + +impl<'a> RequestInstrumentationPolicy { + pub fn new( + crate_name: Option<&'a str>, + crate_version: Option<&'a str>, + options: Option<&RequestInstrumentationOptions>, + ) -> Self { + if let Some(tracing_provider) = options.and_then(|o| o.tracing_provider.clone()) { + Self { + tracer: Some(tracing_provider.get_tracer( + crate_name.unwrap_or("unknown"), + crate_version.unwrap_or("unknown"), + )), + } + } else { + // If no tracing provider is set, we return a policy with no tracer. + Self { tracer: None } + } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Policy for RequestInstrumentationPolicy { + async fn send( + &self, + ctx: &Context, + request: &mut Request, + next: &[Arc], + ) -> PolicyResult { + if let Some(tracer) = &self.tracer { + let mut span_attributes = vec![ + Attribute { + key: HTTP_REQUEST_METHOD_ATTRIBUTE, + value: request.method().to_string().into(), + }, + Attribute { + key: AZ_SCHEMA_URL_ATTRIBUTE, + value: request.url().scheme().into(), + }, + ]; + + if !request.url().username().is_empty() || request.url().password().is_some() { + // If the URL contains a password, we do not log it for security reasons. + let full_url = format!( + "{}://{}{}{}{}{}", + request.url().scheme(), + request + .url() + .host() + .map_or_else(|| "unknown_host".to_string(), |h| h.to_string()), + request + .url() + .port() + .map_or_else(String::new, |p| format!(":{}", p)), + request.url().path(), + request + .url() + .query() + .map_or_else(String::new, |q| format!("?{}", q)), + request + .url() + .fragment() + .map_or_else(String::new, |f| format!("#{}", f)), + ); + span_attributes.push(Attribute { + key: URL_FULL_ATTRIBUTE, + value: full_url.into(), + }); + } else { + // If no password is present, we log the full URL. + span_attributes.push(Attribute { + key: URL_FULL_ATTRIBUTE, + value: request.url().to_string().into(), + }); + } + + if let Some(host) = request.url().host() { + span_attributes.push(Attribute { + key: SERVER_ADDRESS_ATTRIBUTE, + value: host.to_string().into(), + }); + } + if let Some(port) = request.url().port_or_known_default() { + span_attributes.push(Attribute { + key: SERVER_PORT_ATTRIBUTE, + value: port.into(), + }); + } + let span = if let Some(parent_span) = ctx.value::>() { + // If a parent span exists, start a new span with the parent. + tracer.start_span_with_parent( + request.method().to_string().as_str(), + SpanKind::Client, + span_attributes, + parent_span.clone(), + ) + } else { + // If no parent span exists, start a new span without a parent. + tracer.start_span_with_current( + request.method().to_string().as_str(), + SpanKind::Client, + span_attributes, + ) + }; + + if let Some(client_request_id) = request + .headers() + .get_optional_str(&headers::CLIENT_REQUEST_ID) + { + span.set_attribute(AZ_CLIENT_REQUEST_ID_ATTRIBUTE, client_request_id.into()); + } + + if let Some(service_request_id) = + request.headers().get_optional_str(&headers::REQUEST_ID) + { + span.set_attribute(AZ_SERVICE_REQUEST_ID_ATTRIBUTE, service_request_id.into()); + } + + if let Some(retry_count) = ctx.value::() { + span.set_attribute(HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, retry_count.0.into()); + } + + let result = next[0].send(ctx, request, &next[1..]).await; + + if result.is_err() { + // If the request failed, set an error type attribute. + span.set_attribute( + ERROR_TYPE_ATTRIBUTE, + result.as_ref().err().unwrap().to_string().into(), + ); + } + if let Ok(response) = result.as_ref() { + // If the request was successful, set the HTTP response status code. + span.set_attribute( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + u16::from(response.status()).into(), + ); + + if response.status().is_server_error() || response.status().is_client_error() { + // If the response status indicates an error, set the span status to error. + span.set_status(crate::tracing::SpanStatus::Error { + description: format!( + "HTTP request failed with status code {}: {}", + response.status(), + response.status().canonical_reason() + ), + }); + } + } + + span.end(); + return result; + } else { + // If no tracer is set, we simply forward the request without instrumentation. + next[0].send(ctx, request, &next[1..]).await + } + } +} +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + use typespec_client_core::{ + http::{ + headers::Headers, policies::TransportPolicy, Method, RawResponse, StatusCode, + TransportOptions, + }, + tracing::{AsAny, AttributeValue, Span, SpanStatus, Tracer, TracerProvider}, + }; + + #[derive(Debug)] + struct MockTransport; + + #[async_trait::async_trait] + impl Policy for MockTransport { + async fn send( + &self, + _ctx: &Context, + _request: &mut Request, + _next: &[Arc], + ) -> PolicyResult { + PolicyResult::Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + Vec::new(), + )) + } + } + + #[derive(Debug)] + struct MockTracingProvider { + tracers: Mutex>>, + } + + impl MockTracingProvider { + fn new() -> Self { + Self { + tracers: Mutex::new(Vec::new()), + } + } + } + impl TracerProvider for MockTracingProvider { + fn get_tracer( + &self, + crate_name: &str, + crate_version: &str, + ) -> Arc { + let mut tracers = self.tracers.lock().unwrap(); + let tracer = Arc::new(MockTracer { + name: crate_name.to_string(), + version: crate_version.to_string(), + spans: Mutex::new(Vec::new()), + }); + + tracers.push(tracer.clone()); + tracer + } + } + + #[derive(Debug)] + struct MockTracer { + name: String, + version: String, + spans: Mutex>>, + } + + impl Tracer for MockTracer { + fn start_span_with_current( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + + fn start_span_with_parent( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + _parent: Arc, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + + fn start_span( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + } + + #[derive(Debug)] + struct MockSpan { + name: String, + #[allow(dead_code)] + kind: SpanKind, + #[allow(dead_code)] + attributes: Mutex>, + state: Mutex, + is_open: Mutex, + } + impl MockSpan { + fn new(name: &str, kind: SpanKind, attributes: Vec) -> Self { + println!("Creating MockSpan: {}", name); + println!("Attributes: {:?}", attributes); + Self { + name: name.to_string(), + kind, + attributes: Mutex::new(attributes), + state: Mutex::new(SpanStatus::Unset), + is_open: Mutex::new(true), + } + } + } + + impl Span for MockSpan { + fn set_attribute(&self, key: &'static str, value: AttributeValue) { + println!("{}: Setting attribute {}: {:?}", self.name, key, value); + let mut attributes = self.attributes.lock().unwrap(); + attributes.push(Attribute { key, value }); + } + + fn set_status(&self, status: crate::tracing::SpanStatus) { + println!("{}: Setting span status: {:?}", self.name, status); + let mut state = self.state.lock().unwrap(); + *state = status; + } + + fn end(&self) { + println!("Ending span: {}", self.name); + let mut is_open = self.is_open.lock().unwrap(); + *is_open = false; + } + + fn is_recording(&self) -> bool { + true + } + + fn span_id(&self) -> [u8; 8] { + [0; 8] // Mock span ID + } + + fn record_error(&self, _error: &dyn std::error::Error) { + todo!() + } + + fn set_current( + &self, + _context: &Context, + ) -> typespec_client_core::Result> + { + todo!() + } + } + + impl AsAny for MockSpan { + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + async fn run_instrumentation_test( + crate_name: Option<&str>, + version: Option<&str>, + request: &mut Request, + ) -> Arc { + let mock_tracer = Arc::new(MockTracingProvider::new()); + let options = RequestInstrumentationOptions { + tracing_provider: Some(mock_tracer.clone()), + }; + let policy = Arc::new(RequestInstrumentationPolicy::new( + crate_name, + version, + Some(&options), + )); + + let transport = + TransportPolicy::new(TransportOptions::new_custom_policy(Arc::new(MockTransport))); + + let ctx = Context::default(); + let next: Vec> = vec![Arc::new(transport)]; + let _result = policy.send(&ctx, request, &next).await; + + mock_tracer + } + fn check_instrumentation_result( + mock_tracer: Arc, + expected_name: &str, + expected_version: &str, + expected_method: &str, + expected_attributes: Vec<(&str, AttributeValue)>, + ) { + assert_eq!( + mock_tracer.tracers.lock().unwrap().len(), + 1, + "Expected one tracer to be created", + ); + let tracers = mock_tracer.tracers.lock().unwrap(); + let tracer = tracers.first().unwrap(); + assert_eq!(tracer.name, expected_name); + assert_eq!(tracer.version, expected_version); + let spans = tracer.spans.lock().unwrap(); + assert_eq!(spans.len(), 1, "Expected one span to be created"); + println!("Spans: {:?}", spans); + let span = spans.first().unwrap(); + assert_eq!(span.name, expected_method); + let attributes = span.attributes.lock().unwrap(); + for attr in attributes.iter() { + println!("Attribute: {} = {:?}", attr.key, attr.value); + let mut found = false; + for (key, value) in &expected_attributes { + if attr.key == *key { + assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); + found = true; + break; + } + } + if !found { + panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); + } + } + for (key, value) in &expected_attributes { + if !attributes + .iter() + .any(|attr| attr.key == *key && attr.value == *value) + { + panic!("Expected attribute not found: {} = {:?}", key, value); + } + } + } + + #[tokio::test] + async fn simple_instrumentation_policy() { + let url = "http://example.com/path"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer = + run_instrumentation_test(Some("test_crate"), Some("1.0.0"), &mut request).await; + + check_instrumentation_result( + mock_tracer, + "test_crate", + "1.0.0", + "GET", + vec![ + (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("http")), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(80)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("http://example.com/path"), + ), + ], + ); + } + + #[tokio::test] + async fn client_request_id() { + let url = "https://example.com/client_request_id"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + request.insert_header(headers::CLIENT_REQUEST_ID, "test-client-request-id"); + + let mock_tracer = + run_instrumentation_test(Some("test_crate"), Some("1.0.0"), &mut request).await; + + check_instrumentation_result( + mock_tracer.clone(), + "test_crate", + "1.0.0", + "GET", + vec![ + (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + ( + AZ_CLIENT_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-client-request-id"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://example.com/client_request_id"), + ), + ], + ); + } + + #[tokio::test] + async fn test_url_with_password() { + let url = "https://user:password@host:8080/path?query=value#fragment"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer_provider = run_instrumentation_test(None, None, &mut request).await; + + check_instrumentation_result( + mock_tracer_provider, + "unknown", + "unknown", + "GET", + vec![ + (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + (SERVER_ADDRESS_ATTRIBUTE, AttributeValue::from("host")), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(8080)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://host:8080/path?query=value#fragment"), + ), + ], + ); + } +} diff --git a/sdk/core/azure_core_opentelemetry/src/attributes.rs b/sdk/core/azure_core_opentelemetry/src/attributes.rs index 4b389d5420..2a87b25199 100644 --- a/sdk/core/azure_core_opentelemetry/src/attributes.rs +++ b/sdk/core/azure_core_opentelemetry/src/attributes.rs @@ -5,13 +5,17 @@ // Re-export typespec_client_core tracing attributes for convenience use azure_core::tracing::{ - AttributeArray as AzureAttributeArray, AttributeValue as AzureAttributeValue, + Attribute as AzureAttribute, AttributeArray as AzureAttributeArray, + AttributeValue as AzureAttributeValue, }; +use opentelemetry::KeyValue; pub(super) struct AttributeArray(AzureAttributeArray); pub(super) struct AttributeValue(pub AzureAttributeValue); +pub(super) struct OpenTelemetryAttribute(pub AzureAttribute); + impl From for AttributeValue { fn from(value: bool) -> Self { AttributeValue(AzureAttributeValue::Bool(value)) @@ -59,13 +63,22 @@ impl From> for AttributeArray { } } +impl From for KeyValue { + fn from(attr: OpenTelemetryAttribute) -> Self { + KeyValue::new( + attr.0.key, + opentelemetry::Value::from(AttributeValue(attr.0.value)), + ) + } +} + /// Conversion from typespec_client_core AttributeValue to OpenTelemetry Value impl From for opentelemetry::Value { fn from(value: AttributeValue) -> Self { match value.0 { AzureAttributeValue::Bool(b) => opentelemetry::Value::Bool(b), AzureAttributeValue::I64(i) => opentelemetry::Value::I64(i), - AzureAttributeValue::F64(u) => opentelemetry::Value::F64(u), + AzureAttributeValue::F64(f) => opentelemetry::Value::F64(f), AzureAttributeValue::String(s) => opentelemetry::Value::String(s.into()), AzureAttributeValue::Array(arr) => { opentelemetry::Value::Array(opentelemetry::Array::from(AttributeArray(arr))) diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 62c861a5b2..914e1a599e 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -41,40 +41,40 @@ impl OpenTelemetrySpan { } impl Span for OpenTelemetrySpan { - fn end(&self) -> Result<()> { + fn is_recording(&self) -> bool { + self.context.span().is_recording() + } + + fn end(&self) { self.context.span().end(); - Ok(()) } fn span_id(&self) -> [u8; 8] { self.context.span().span_context().span_id().to_bytes() } - fn set_attribute(&self, key: &'static str, value: AttributeValue) -> Result<()> { + fn set_attribute(&self, key: &'static str, value: AttributeValue) { let otel_value = opentelemetry::Value::from(ConversionAttributeValue(value)); self.context .span() .set_attribute(opentelemetry::KeyValue::new(key, otel_value)); - Ok(()) } - fn record_error(&self, error: &dyn StdError) -> Result<()> { + fn record_error(&self, error: &dyn StdError) { self.context.span().record_error(error); self.context .span() .set_status(opentelemetry::trace::Status::error(error.to_string())); - Ok(()) } - fn set_status(&self, status: SpanStatus) -> Result<()> { + fn set_status(&self, status: SpanStatus) { let otel_status = match status { SpanStatus::Unset => opentelemetry::trace::Status::Unset, SpanStatus::Ok => opentelemetry::trace::Status::Ok, SpanStatus::Error { description } => opentelemetry::trace::Status::error(description), }; self.context.span().set_status(otel_status); - Ok(()) } fn set_current( @@ -117,7 +117,7 @@ impl Drop for OpenTelemetrySpanGuard { mod tests { use crate::telemetry::OpenTelemetryTracerProvider; use azure_core::http::Context as AzureContext; - use azure_core::tracing::{AttributeValue, SpanKind, SpanStatus, TracerProvider}; + use azure_core::tracing::{Attribute, AttributeValue, SpanKind, SpanStatus, TracerProvider}; use opentelemetry::trace::TraceContextExt; use opentelemetry::{Context, Key, KeyValue, Value}; use opentelemetry_sdk::trace::{in_memory_exporter::InMemorySpanExporter, SdkTracerProvider}; @@ -140,8 +140,8 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Client); - assert!(span.end().is_ok()); + let span = tracer.start_span("test_span", SpanKind::Client, vec![]); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); @@ -159,12 +159,16 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let parent_span = tracer.start_span("parent_span", SpanKind::Server); - let child_span = - tracer.start_span_with_parent("child_span", SpanKind::Client, parent_span.clone()); + let parent_span = tracer.start_span("parent_span", SpanKind::Server, vec![]); + let child_span = tracer.start_span_with_parent( + "child_span", + SpanKind::Client, + vec![], + parent_span.clone(), + ); - assert!(child_span.end().is_ok()); - assert!(parent_span.end().is_ok()); + child_span.end(); + parent_span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 2); @@ -187,14 +191,14 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span1 = tracer.start_span("span1", SpanKind::Internal); - let span2 = tracer.start_span("span2", SpanKind::Server); + let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); + let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let child_span = - tracer.start_span_with_parent("child_span", SpanKind::Client, span1.clone()); + tracer.start_span_with_parent("child_span", SpanKind::Client, vec![], span1.clone()); - assert!(child_span.end().is_ok()); - assert!(span2.end().is_ok()); - assert!(span1.end().is_ok()); + child_span.end(); + span2.end(); + span1.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 3); @@ -217,16 +221,16 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span1 = tracer.start_span("span1", SpanKind::Internal); - let span2 = tracer.start_span("span2", SpanKind::Server); + let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); + let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let _span_guard = span2 .set_current(&azure_core::http::Context::new()) .unwrap(); - let child_span = tracer.start_span_with_current("child_span", SpanKind::Client); + let child_span = tracer.start_span_with_current("child_span", SpanKind::Client, vec![]); - assert!(child_span.end().is_ok()); - assert!(span2.end().is_ok()); - assert!(span1.end().is_ok()); + child_span.end(); + span2.end(); + span1.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 3); @@ -249,12 +253,10 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Internal); + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); - assert!(span - .set_attribute("test_key", AttributeValue::String("test_value".to_string())) - .is_ok()); - assert!(span.end().is_ok()); + span.set_attribute("test_key", AttributeValue::String("test_value".to_string())); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); @@ -275,11 +277,11 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); - let span = tracer.start_span("test_span", SpanKind::Client); + let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let error = Error::new(ErrorKind::NotFound, "resource not found"); - assert!(span.record_error(&error).is_ok()); - assert!(span.end().is_ok()); + span.record_error(&error); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); @@ -304,18 +306,16 @@ mod tests { let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); // Test Ok status - let span = tracer.start_span("test_span_ok", SpanKind::Server); - assert!(span.set_status(SpanStatus::Ok).is_ok()); - assert!(span.end().is_ok()); + let span = tracer.start_span("test_span_ok", SpanKind::Server, vec![]); + span.set_status(SpanStatus::Ok); + span.end(); // Test Error status - let span = tracer.start_span("test_span_error", SpanKind::Server); - assert!(span - .set_status(SpanStatus::Error { - description: "test error".to_string() - }) - .is_ok()); - assert!(span.end().is_ok()); + let span = tracer.start_span("test_span_error", SpanKind::Server, vec![]); + span.set_status(SpanStatus::Error { + description: "test error".to_string(), + }); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 2); @@ -349,7 +349,14 @@ mod tests { 42 }; - let span = tracer.start_span("test_span", SpanKind::Client); + let span = tracer.start_span( + "test_span", + SpanKind::Client, + vec![Attribute { + key: "test_key", + value: "test_value".into(), + }], + ); let azure_context = AzureContext::new(); let azure_context = azure_context.with_value(span.clone()); @@ -359,7 +366,7 @@ mod tests { let result = future.await; assert_eq!(result, 42); - span.end().unwrap(); + span.end(); let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index ea7f1ebf57..dbe9cbddba 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -26,13 +26,13 @@ impl OpenTelemetryTracerProvider { impl TracerProvider for OpenTelemetryTracerProvider { fn get_tracer( &self, - name: &'static str, - package_version: &'static str, - ) -> Box { - let scope = InstrumentationScope::builder(name) - .with_version(package_version) + name: &str, + package_version: &str, + ) -> Arc { + let scope = InstrumentationScope::builder(name.to_owned()) + .with_version(package_version.to_owned()) .build(); - Box::new(OpenTelemetryTracer::new(BoxedTracer::new( + Arc::new(OpenTelemetryTracer::new(BoxedTracer::new( self.inner.boxed_tracer(scope), ))) } diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index d32ff66241..8cc8bfc4a9 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -1,12 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::span::{OpenTelemetrySpan, OpenTelemetrySpanKind}; -use azure_core::tracing::{SpanKind, Tracer}; +use crate::{ + attributes::OpenTelemetryAttribute, + span::{OpenTelemetrySpan, OpenTelemetrySpanKind}, +}; + +use azure_core::tracing::{Attribute, SpanKind, Tracer}; use opentelemetry::{ global::BoxedTracer, trace::{TraceContextExt, Tracer as OpenTelemetryTracerTrait}, - Context, + Context, KeyValue, }; use std::sync::Arc; @@ -24,11 +28,17 @@ impl OpenTelemetryTracer { impl Tracer for OpenTelemetryTracer { fn start_span( &self, - name: &'static str, + name: &str, kind: SpanKind, - ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) - .with_kind(OpenTelemetrySpanKind(kind).into()); + attributes: Vec, + ) -> Arc { + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + .with_kind(OpenTelemetrySpanKind(kind).into()) + .with_attributes( + attributes + .into_iter() + .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr))), + ); let context = Context::new(); let span = self.inner.build_with_context(span_builder, &context); @@ -37,11 +47,17 @@ impl Tracer for OpenTelemetryTracer { fn start_span_with_current( &self, - name: &'static str, + name: &str, kind: SpanKind, - ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) - .with_kind(OpenTelemetrySpanKind(kind).into()); + attributes: Vec, + ) -> Arc { + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + .with_kind(OpenTelemetrySpanKind(kind).into()) + .with_attributes( + attributes + .into_iter() + .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr))), + ); let context = Context::current(); let span = self.inner.build_with_context(span_builder, &context); @@ -50,12 +66,18 @@ impl Tracer for OpenTelemetryTracer { fn start_span_with_parent( &self, - name: &'static str, + name: &str, kind: SpanKind, - parent: Arc, - ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) - .with_kind(OpenTelemetrySpanKind(kind).into()); + attributes: Vec, + parent: Arc, + ) -> Arc { + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + .with_kind(OpenTelemetrySpanKind(kind).into()) + .with_attributes( + attributes + .into_iter() + .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr))), + ); // Cast the parent span to Any type let context = parent @@ -83,8 +105,8 @@ mod tests { let noop_tracer = NoopTracerProvider::new(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); - let span = tracer.start_span("test_span", SpanKind::Internal); - assert!(span.end().is_ok()); + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); + span.end(); } #[test] @@ -99,6 +121,6 @@ mod tests { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); - let _span = tracer.start_span("test_span", SpanKind::Internal); + let _span = tracer.start_span("test_span", SpanKind::Internal, vec![]); } } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index 584e48b8ee..b9cc00973b 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -17,20 +17,20 @@ async fn test_span_creation() -> Result<(), Box> { let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); // Create a span using the Azure tracer - let span = tracer.start_span("test_span", SpanKind::Internal); + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); // Add attributes to the span using individual set_attribute calls span.set_attribute( "test_key", azure_core::tracing::AttributeValue::String("test_value".to_string()), - )?; + ); span.set_attribute( "service.name", azure_core::tracing::AttributeValue::String("azure-test".to_string()), - )?; + ); // End the span - span.end()?; + span.end(); Ok(()) } @@ -43,8 +43,8 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { // Get a tracer and verify it works let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); - let span = tracer.start_span("test_span", SpanKind::Internal); - span.end()?; + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); + span.end(); Ok(()) } @@ -59,24 +59,24 @@ async fn test_span_attributes() -> Result<(), Box> { let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); // Create span with multiple attributes - let span = tracer.start_span("test_span", SpanKind::Internal); + let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); // Add attributes using individual set_attribute calls span.set_attribute( "service.name", azure_core::tracing::AttributeValue::String("test-service".to_string()), - )?; + ); span.set_attribute( "operation.name", azure_core::tracing::AttributeValue::String("test-operation".to_string()), - )?; + ); span.set_attribute( "request.id", azure_core::tracing::AttributeValue::String("req-123".to_string()), - )?; + ); // End the span - span.end()?; + span.end(); Ok(()) } diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs new file mode 100644 index 0000000000..911a50919e --- /dev/null +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -0,0 +1,199 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//! This file contains an Azure SDK for Rust fake service client API. +//! +use azure_core::{ + credentials::TokenCredential, + fmt::SafeDebug, + http::{ + policies::{BearerTokenCredentialPolicy, Policy}, + ClientMethodOptions, ClientOptions, Pipeline, RawResponse, Request, + RequestInstrumentationOptions, Url, + }, + Result, +}; +use azure_core_opentelemetry::OpenTelemetryTracerProvider; +use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider}; +use std::sync::Arc; + +#[derive(Default, Clone, SafeDebug)] +pub struct TestServiceClientOptions { + pub azure_client_options: ClientOptions, + pub api_version: String, +} + +pub struct TestServiceClient { + endpoint: Url, + api_version: String, + pipeline: Pipeline, +} + +#[derive(Default, SafeDebug)] +pub struct TestServiceClientGetMethodOptions<'a> { + pub method_options: ClientMethodOptions<'a>, +} + +impl TestServiceClient { + pub fn new( + endpoint: &str, + credential: Arc, + options: Option, + ) -> Result { + let options = options.unwrap_or_default(); + let mut endpoint = Url::parse(endpoint)?; + if !endpoint.scheme().starts_with("http") { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("{endpoint} must use http(s)"), + )); + } + endpoint.set_query(None); + let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( + credential, + vec!["https://vault.azure.net/.default"], + )); + let request_instrumentation_policy = Arc::new( + azure_core::http::policies::RequestInstrumentationPolicy::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options + .azure_client_options + .request_instrumentation + .as_ref(), + ), + ); + Ok(Self { + endpoint, + api_version: options.api_version, + pipeline: Pipeline::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options.azure_client_options, + Vec::default(), + vec![auth_policy, request_instrumentation_policy], + ), + }) + } + + /// Returns the Url associated with this client. + pub fn endpoint(&self) -> &Url { + &self.endpoint + } + + pub async fn get( + &self, + path: &str, + options: Option>, + ) -> Result { + let options = options.unwrap_or_default(); + let mut url = self.endpoint.clone(); + url.set_path(path); + url.query_pairs_mut() + .append_pair("api-version", &self.api_version); + + let mut request = Request::new(url, azure_core::http::Method::Get); + + let response = self + .pipeline + .send(&options.method_options.context, &mut request) + .await?; + if !response.status().is_success() { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::HttpResponse { + status: response.status(), + error_code: None, + }, + format!("Failed to GET {}: {}", request.url(), response.status()), + )); + } + Ok(response) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use azure_core::Result; + use azure_core_test::{recorded, TestContext}; + use tracing::{info, trace}; + + fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { + let otel_exporter = InMemorySpanExporter::default(); + let otel_tracer_provider = SdkTracerProvider::builder() + .with_simple_exporter(otel_exporter.clone()) + .build(); + let otel_tracer_provider = Arc::new(otel_tracer_provider); + (otel_tracer_provider, otel_exporter) + } + + #[recorded::test()] + async fn test_service_client_new(ctx: TestContext) -> Result<()> { + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + assert_eq!(client.endpoint().as_str(), "https://example.com/"); + assert_eq!(client.api_version, "2023-10-01"); + + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get(ctx: TestContext) -> Result<()> { + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("index.html", None).await; + info!("Response: {:?}", response); + assert!(response.is_ok()); + let response = response.unwrap(); + assert_eq!(response.status(), azure_core::http::StatusCode::Ok); + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("index.html", None).await; + info!("Response: {:?}", response); + assert!(response.is_ok()); + let response = response.unwrap(); + assert_eq!(response.status(), azure_core::http::StatusCode::Ok); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + for span in &spans { + trace!("Span: {:?}", span); + } + + Ok(()) + } +} diff --git a/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs b/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs index b4ea0e5098..818efe34c7 100644 --- a/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs @@ -68,6 +68,11 @@ pub fn get_retry_after(headers: &Headers, now: DateTimeFn) -> Option { }) } +/// A wrapper around a retry count to be used in the context of a retry policy. +/// +/// This allows a post-retry policy to access the retry count +pub struct RetryPolicyCount(pub u32); + /// A retry policy. /// /// In the simple form, the policies need only differ in how @@ -131,7 +136,8 @@ where "failed to reset body stream before retrying request", )?; } - let result = next[0].send(ctx, request, &next[1..]).await; + let try_context = ctx.clone().with_value(RetryPolicyCount(retry_count)); + let result = next[0].send(&try_context, request, &next[1..]).await; // only start keeping track of time after the first request is made let start = start.get_or_insert_with(OffsetDateTime::now_utc); let (last_error, retry_after) = match result { diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 32eb53c593..f0321a941c 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use crate::fmt::SafeDebug; + /// An array of homogeneous attribute values. +#[derive(SafeDebug, PartialEq)] pub enum AttributeArray { /// An array of boolean values. Bool(Vec), @@ -13,7 +16,8 @@ pub enum AttributeArray { String(Vec), } -/// Represents a single attribute value, which can be of various types. +/// Represents a single attribute value, which can be of various types +#[derive(SafeDebug, PartialEq)] pub enum AttributeValue { /// A boolean attribute value. Bool(bool), @@ -26,3 +30,99 @@ pub enum AttributeValue { /// An array of attribute values. Array(AttributeArray), } +#[derive(SafeDebug)] +pub struct Attribute { + /// A key-value pair attribute. + pub key: &'static str, + pub value: AttributeValue, +} + +impl PartialEq<&str> for AttributeValue { + fn eq(&self, other: &&str) -> bool { + match self { + AttributeValue::String(s) => s == *other, + _ => false, + } + } +} + +impl PartialEq for AttributeValue { + fn eq(&self, other: &i64) -> bool { + match self { + AttributeValue::I64(i) => i == other, + _ => false, + } + } +} + +impl From for AttributeValue { + fn from(value: String) -> Self { + AttributeValue::String(value) + } +} + +impl From<&str> for AttributeValue { + fn from(value: &str) -> Self { + AttributeValue::String(value.to_string()) + } +} + +impl From for AttributeValue { + fn from(value: bool) -> Self { + AttributeValue::Bool(value) + } +} + +impl From for AttributeValue { + fn from(value: i32) -> Self { + AttributeValue::I64(value as i64) + } +} + +impl From for AttributeValue { + fn from(value: u16) -> Self { + AttributeValue::I64(value as i64) + } +} + +impl From for AttributeValue { + fn from(value: u32) -> Self { + AttributeValue::I64(value as i64) + } +} + +impl From for AttributeValue { + fn from(value: i64) -> Self { + AttributeValue::I64(value) + } +} + +impl From for AttributeValue { + fn from(value: f64) -> Self { + AttributeValue::F64(value) + } +} + +impl From> for AttributeValue { + fn from(value: Vec) -> Self { + AttributeValue::Array(AttributeArray::Bool(value)) + } +} + +impl From> for AttributeValue { + fn from(value: Vec) -> Self { + AttributeValue::Array(AttributeArray::I64(value)) + } +} + +impl From> for AttributeValue { + fn from(value: Vec) -> Self { + AttributeValue::Array(AttributeArray::F64(value)) + } +} + +impl From> for AttributeValue { + fn from(value: Vec) -> Self { + AttributeValue::Array(AttributeArray::String(value)) + } +} diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index 498a26d86c..f901ea4e27 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -4,6 +4,7 @@ //! Distributed tracing trait definitions //! use crate::http::Context; +use std::fmt::Debug; use std::sync::Arc; /// Overall architecture for distributed tracing in the SDK. @@ -18,26 +19,28 @@ use std::sync::Arc; mod attributes; mod with_context; -pub use attributes::{AttributeArray, AttributeValue}; +pub use attributes::{Attribute, AttributeArray, AttributeValue}; pub use with_context::{FutureExt, WithContext}; /// The TracerProvider trait is the entrypoint for distributed tracing in the SDK. /// /// It provides a method to get a tracer for a specific name and package version. -pub trait TracerProvider { +pub trait TracerProvider: Send + Sync { /// Returns a tracer for the given name. /// /// Arguments: /// - `package_name`: The name of the package for which the tracer is requested. /// - `package_version`: The version of the package for which the tracer is requested. - fn get_tracer( - &self, - package_name: &'static str, - package_version: &'static str, - ) -> Box; + fn get_tracer(&self, package_name: &str, package_version: &str) -> Arc; +} + +impl Debug for dyn TracerProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TracerProvider").finish_non_exhaustive() + } } -pub trait Tracer { +pub trait Tracer: Send + Sync { /// Starts a new span with the given name and type. /// /// # Arguments @@ -47,7 +50,7 @@ pub trait Tracer { /// # Returns /// An `Arc` representing the started span. /// - fn start_span(&self, name: &'static str, kind: SpanKind) -> Arc; + fn start_span(&self, name: &str, kind: SpanKind, attributes: Vec) -> Arc; /// Starts a new span with the given type, using the current span as the parent span. /// @@ -60,9 +63,10 @@ pub trait Tracer { /// fn start_span_with_current( &self, - name: &'static str, + name: &str, kind: SpanKind, - ) -> Arc; + attributes: Vec, + ) -> Arc; /// Starts a new child with the given name, type, and parent span. /// @@ -78,11 +82,20 @@ pub trait Tracer { /// fn start_span_with_parent( &self, - name: &'static str, + name: &str, kind: SpanKind, - parent: Arc, - ) -> Arc; + attributes: Vec, + parent: Arc, + ) -> Arc; +} + +impl Debug for dyn Tracer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tracer").finish_non_exhaustive() + } } + +#[derive(Debug)] pub enum SpanStatus { Unset, Ok, @@ -104,12 +117,14 @@ pub trait SpanGuard { fn end(self) -> crate::Result<()>; } -pub trait Span: AsAny { +pub trait Span: AsAny + Send + Sync { + fn is_recording(&self) -> bool; + /// The 8 byte value which identifies the span. fn span_id(&self) -> [u8; 8]; /// Ends the current span. - fn end(&self) -> crate::Result<()>; + fn end(&self); /// Sets the status of the current span. /// # Arguments @@ -118,14 +133,10 @@ pub trait Span: AsAny { /// # Returns /// A `Result` indicating success or failure of the operation. /// - fn set_status(&self, status: SpanStatus) -> crate::Result<()>; + fn set_status(&self, status: SpanStatus); /// Sets an attribute on the current span. - fn set_attribute( - &self, - key: &'static str, - value: attributes::AttributeValue, - ) -> crate::Result<()>; + fn set_attribute(&self, key: &'static str, value: attributes::AttributeValue); /// Records a Rust standard error on the current span. /// @@ -135,7 +146,7 @@ pub trait Span: AsAny { /// # Returns /// A `Result` indicating success or failure of the operation. /// - fn record_error(&self, error: &dyn std::error::Error) -> crate::Result<()>; + fn record_error(&self, error: &dyn std::error::Error); /// Temporarily sets the span as the current active span in the context. /// # Arguments From c1febef8a204e47efeb8b7bc1d03cde0a0c1ab26 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Thu, 26 Jun 2025 16:24:50 -0700 Subject: [PATCH 26/84] Telemetry static strings again --- .../http/policies/request_instrumentation.rs | 20 +++++++++---------- .../azure_core_opentelemetry/src/telemetry.rs | 8 ++++---- .../azure_core_opentelemetry/src/tracer.rs | 10 +++++----- .../typespec_client_core/src/http/method.rs | 2 +- .../src/http/request/mod.rs | 19 ++++++++++++++++-- .../typespec_client_core/src/tracing/mod.rs | 17 ++++++++++++---- 6 files changed, 49 insertions(+), 27 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 9785269650..16ae755205 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -31,10 +31,10 @@ pub struct RequestInstrumentationPolicy { tracer: Option>, } -impl<'a> RequestInstrumentationPolicy { +impl RequestInstrumentationPolicy { pub fn new( - crate_name: Option<&'a str>, - crate_version: Option<&'a str>, + crate_name: Option<&'static str>, + crate_version: Option<&'static str>, options: Option<&RequestInstrumentationOptions>, ) -> Self { if let Some(tracing_provider) = options.and_then(|o| o.tracing_provider.clone()) { @@ -119,21 +119,19 @@ impl Policy for RequestInstrumentationPolicy { value: port.into(), }); } + // Get the method as a string to avoid lifetime issues + let method_str = request.method_as_str(); let span = if let Some(parent_span) = ctx.value::>() { // If a parent span exists, start a new span with the parent. tracer.start_span_with_parent( - request.method().to_string().as_str(), + method_str, SpanKind::Client, span_attributes, parent_span.clone(), ) } else { // If no parent span exists, start a new span without a parent. - tracer.start_span_with_current( - request.method().to_string().as_str(), - SpanKind::Client, - span_attributes, - ) + tracer.start_span_with_current(method_str, SpanKind::Client, span_attributes) }; if let Some(client_request_id) = request @@ -364,8 +362,8 @@ mod tests { } async fn run_instrumentation_test( - crate_name: Option<&str>, - version: Option<&str>, + crate_name: Option<&'static str>, + version: Option<&'static str>, request: &mut Request, ) -> Arc { let mock_tracer = Arc::new(MockTracingProvider::new()); diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index dbe9cbddba..8231bf7739 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -26,11 +26,11 @@ impl OpenTelemetryTracerProvider { impl TracerProvider for OpenTelemetryTracerProvider { fn get_tracer( &self, - name: &str, - package_version: &str, + name: &'static str, + package_version: &'static str, ) -> Arc { - let scope = InstrumentationScope::builder(name.to_owned()) - .with_version(package_version.to_owned()) + let scope = InstrumentationScope::builder(name) + .with_version(package_version) .build(); Arc::new(OpenTelemetryTracer::new(BoxedTracer::new( self.inner.boxed_tracer(scope), diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 8cc8bfc4a9..d4aa7afc69 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -28,11 +28,11 @@ impl OpenTelemetryTracer { impl Tracer for OpenTelemetryTracer { fn start_span( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()) .with_attributes( attributes @@ -47,11 +47,11 @@ impl Tracer for OpenTelemetryTracer { fn start_span_with_current( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, ) -> Arc { - let span_builder = opentelemetry::trace::SpanBuilder::from_name(name.to_owned()) + let span_builder = opentelemetry::trace::SpanBuilder::from_name(name) .with_kind(OpenTelemetrySpanKind(kind).into()) .with_attributes( attributes @@ -66,7 +66,7 @@ impl Tracer for OpenTelemetryTracer { fn start_span_with_parent( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, parent: Arc, diff --git a/sdk/typespec/typespec_client_core/src/http/method.rs b/sdk/typespec/typespec_client_core/src/http/method.rs index 3c185d66af..2803fb9952 100644 --- a/sdk/typespec/typespec_client_core/src/http/method.rs +++ b/sdk/typespec/typespec_client_core/src/http/method.rs @@ -194,7 +194,7 @@ impl<'a> std::convert::TryFrom<&'a str> for Method { } impl AsRef for Method { - fn as_ref(&self) -> &str { + fn as_ref(&self) -> &'static str { match self { Self::Delete => "DELETE", Self::Get => "GET", diff --git a/sdk/typespec/typespec_client_core/src/http/request/mod.rs b/sdk/typespec/typespec_client_core/src/http/request/mod.rs index dc38355600..ab2549d0e0 100644 --- a/sdk/typespec/typespec_client_core/src/http/request/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/request/mod.rs @@ -145,8 +145,23 @@ impl Request { &self.method } - /// Inserts zero or more headers from a type that implements [`AsHeaders`]. - pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { + /// Returns the HTTP method as a static string. + /// + /// This is not generally useful and should be avoided in favor of using the ['Request::method()'] method. + #[doc(hidden)] + pub fn method_as_str(&self) -> &'static str { + match self.method { + Method::Delete => "DELETE", + Method::Get => "GET", + Method::Head => "HEAD", + Method::Patch => "PATCH", + Method::Post => "POST", + Method::Put => "PUT", + } + } + + /// Inserts zero or more headers from a type that implements [`AsHeaders`]. + pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { for (name, value) in headers.as_headers()? { self.insert_header(name, value); } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index f901ea4e27..23349e8a24 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -31,7 +31,11 @@ pub trait TracerProvider: Send + Sync { /// Arguments: /// - `package_name`: The name of the package for which the tracer is requested. /// - `package_version`: The version of the package for which the tracer is requested. - fn get_tracer(&self, package_name: &str, package_version: &str) -> Arc; + fn get_tracer( + &self, + package_name: &'static str, + package_version: &'static str, + ) -> Arc; } impl Debug for dyn TracerProvider { @@ -50,7 +54,12 @@ pub trait Tracer: Send + Sync { /// # Returns /// An `Arc` representing the started span. /// - fn start_span(&self, name: &str, kind: SpanKind, attributes: Vec) -> Arc; + fn start_span( + &self, + name: &'static str, + kind: SpanKind, + attributes: Vec, + ) -> Arc; /// Starts a new span with the given type, using the current span as the parent span. /// @@ -63,7 +72,7 @@ pub trait Tracer: Send + Sync { /// fn start_span_with_current( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, ) -> Arc; @@ -82,7 +91,7 @@ pub trait Tracer: Send + Sync { /// fn start_span_with_parent( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, parent: Arc, From 1372431fbe530a41d6d5b6eba3fd3d6d491a4258 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Thu, 26 Jun 2025 16:35:48 -0700 Subject: [PATCH 27/84] http::Method as_str() --- .../src/http/policies/request_instrumentation.rs | 3 ++- sdk/typespec/typespec_client_core/src/http/method.rs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 16ae755205..1911b30d53 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -120,7 +120,8 @@ impl Policy for RequestInstrumentationPolicy { }); } // Get the method as a string to avoid lifetime issues - let method_str = request.method_as_str(); + // let method_str = request.method_as_str(); + let method_str = request.method().as_str(); let span = if let Some(parent_span) = ctx.value::>() { // If a parent span exists, start a new span with the parent. tracer.start_span_with_parent( diff --git a/sdk/typespec/typespec_client_core/src/http/method.rs b/sdk/typespec/typespec_client_core/src/http/method.rs index 2803fb9952..40b559b691 100644 --- a/sdk/typespec/typespec_client_core/src/http/method.rs +++ b/sdk/typespec/typespec_client_core/src/http/method.rs @@ -110,6 +110,18 @@ impl Method { pub fn is_safe(&self) -> bool { matches!(self, Method::Get | Method::Head) } + + /// Returns the HTTP method as a static string slice. + pub fn as_str(&self) -> &'static str { + match self { + Method::Delete => "DELETE", + Method::Get => "GET", + Method::Head => "HEAD", + Method::Patch => "PATCH", + Method::Post => "POST", + Method::Put => "PUT", + } + } } #[cfg(any(feature = "json", feature = "xml"))] From a08bc7eedfbc63b3ddf3b462815968ffb82d34ac Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Sat, 28 Jun 2025 09:30:14 -0700 Subject: [PATCH 28/84] Added service tracing spans; populate az.namespace --- sdk/core/azure_core/src/http/options/mod.rs | 15 +- sdk/core/azure_core/src/http/pipeline.rs | 37 +- .../http/policies/request_instrumentation.rs | 243 ++++++++++--- sdk/core/azure_core_opentelemetry/README.md | 14 - sdk/core/azure_core_opentelemetry/src/span.rs | 38 ++- .../azure_core_opentelemetry/src/telemetry.rs | 13 +- .../azure_core_opentelemetry/src/tracer.rs | 18 +- .../tests/integration_test.rs | 6 +- .../tests/telemetry_service_implementation.rs | 320 ++++++++++++++++-- .../typespec_client_core/src/tracing/mod.rs | 9 +- 10 files changed, 603 insertions(+), 110 deletions(-) diff --git a/sdk/core/azure_core/src/http/options/mod.rs b/sdk/core/azure_core/src/http/options/mod.rs index 70d8b6eded..fd2e0e835e 100644 --- a/sdk/core/azure_core/src/http/options/mod.rs +++ b/sdk/core/azure_core/src/http/options/mod.rs @@ -33,13 +33,18 @@ pub struct ClientOptions { pub request_instrumentation: Option, } +pub(crate) struct CoreClientOptions { + pub(crate) user_agent: UserAgentOptions, + pub(crate) request_instrumentation: RequestInstrumentationOptions, +} + impl ClientOptions { /// Efficiently deconstructs into owned [`typespec_client_core::http::ClientOptions`] as well as unwrapped or default Azure-specific options. /// /// If instead we implemented [`Into`], we'd have to clone Azure-specific options instead of moving memory of [`Some`] values. pub(in crate::http) fn deconstruct( self, - ) -> (UserAgentOptions, typespec_client_core::http::ClientOptions) { + ) -> (CoreClientOptions, typespec_client_core::http::ClientOptions) { let options = typespec_client_core::http::ClientOptions { per_call_policies: self.per_call_policies, per_try_policies: self.per_try_policies, @@ -47,6 +52,12 @@ impl ClientOptions { transport: self.transport, }; - (self.user_agent.unwrap_or_default(), options) + ( + CoreClientOptions { + user_agent: self.user_agent.unwrap_or_default(), + request_instrumentation: self.request_instrumentation.unwrap_or_default(), + }, + options, + ) } } diff --git a/sdk/core/azure_core/src/http/pipeline.rs b/sdk/core/azure_core/src/http/pipeline.rs index ce69465631..7358e5d65b 100644 --- a/sdk/core/azure_core/src/http/pipeline.rs +++ b/sdk/core/azure_core/src/http/pipeline.rs @@ -3,7 +3,7 @@ use super::policies::ClientRequestIdPolicy; use crate::http::{ - policies::{Policy, UserAgentPolicy}, + policies::{Policy, RequestInstrumentationPolicy, UserAgentPolicy}, ClientOptions, }; use std::{ @@ -51,9 +51,38 @@ impl Pipeline { let mut per_call_policies = per_call_policies.clone(); push_unique(&mut per_call_policies, ClientRequestIdPolicy::default()); - let (user_agent, options) = options.deconstruct(); - let telemetry_policy = UserAgentPolicy::new(crate_name, crate_version, &user_agent); - push_unique(&mut per_call_policies, telemetry_policy); + let (core_client_options, options) = options.deconstruct(); + let user_agent_policy = + UserAgentPolicy::new(crate_name, crate_version, &core_client_options.user_agent); + push_unique(&mut per_call_policies, user_agent_policy); + + + let mut per_try_policies = per_try_policies.clone(); + if core_client_options + .request_instrumentation + .tracing_provider + .is_some() + { + // Note that the choice to use "None" as the namespace here + // is intentional. + // The `azure_namespace` parameter is used to populate the `az.namespace` + // span attribute, however that information is only known by the author of the + // client library, not the core library. + // It is also *not* a constant that can be derived from the crate information - + // it is a value that is determined from the list of resource providers + // listed [here](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers). + // + // This information can only come from the package owner. It doesn't make sense + // to burden all users of the azure_core pipeline with determining this + // information, so we use `None` here. + let request_instrumentation_policy = RequestInstrumentationPolicy::new( + None, + crate_name, + crate_version, + &core_client_options.request_instrumentation, + ); + push_unique(&mut per_try_policies, request_instrumentation_policy); + } Self(http::Pipeline::new( options, diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 1911b30d53..81a84dd6d8 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -11,9 +11,7 @@ use typespec_client_core::{ tracing::Attribute, }; -#[allow(dead_code)] const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; - const AZ_SCHEMA_URL_ATTRIBUTE: &str = "az.schema.url"; const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client.request.id"; const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; @@ -25,27 +23,47 @@ const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; const SERVER_PORT_ATTRIBUTE: &str = "server.port"; const URL_FULL_ATTRIBUTE: &str = "url.full"; -/// Sets the User-Agent header with useful information in a typical format for Azure SDKs. +/// Sets distributed tracing information for HTTP requests. #[derive(Clone, Debug)] pub struct RequestInstrumentationPolicy { tracer: Option>, } impl RequestInstrumentationPolicy { + /// Creates a new `RequestInstrumentationPolicy`. + /// + /// # Arguments + /// - `azure_namespace`: The Azure namespace for the tracer. + /// - `crate_name`: The name of the crate for which the tracer is created. + /// - `crate_version`: The version of the crate for which the tracer is created. + /// - `options`: Options for request instrumentation, including the tracing provider. + /// + /// # Returns + /// A new instance of `RequestInstrumentationPolicy`. + /// + /// # Note + /// This policy will only create a tracer if a tracing provider is provided in the options. + /// + /// This policy will create a tracer that can be used to instrument HTTP requests. + /// However this tracer is only used when the client method is NOT instrumented. + /// A part of the client method instrumentation sets a client-specific tracer into the + /// request `[Context]` which will be used instead of the tracer from this policy. + /// pub fn new( + azure_namespace: Option<&'static str>, crate_name: Option<&'static str>, crate_version: Option<&'static str>, - options: Option<&RequestInstrumentationOptions>, + options: &RequestInstrumentationOptions, ) -> Self { - if let Some(tracing_provider) = options.and_then(|o| o.tracing_provider.clone()) { + if let Some(tracing_provider) = &options.tracing_provider { Self { tracer: Some(tracing_provider.get_tracer( + azure_namespace.unwrap_or("Unknown"), crate_name.unwrap_or("unknown"), crate_version.unwrap_or("unknown"), )), } } else { - // If no tracing provider is set, we return a policy with no tracer. Self { tracer: None } } } @@ -60,12 +78,26 @@ impl Policy for RequestInstrumentationPolicy { request: &mut Request, next: &[Arc], ) -> PolicyResult { - if let Some(tracer) = &self.tracer { + // If the context has a tracer (which happens when called from an instrumented method), + // we prefer the tracer from the context. + // Otherwise, we use the tracer from the policy itself. + // This allows for flexibility in using different tracers in different contexts. + let tracer = if ctx.value::>().is_some() { + ctx.value::>() + } else { + self.tracer.as_ref() + }; + + if let Some(tracer) = tracer { let mut span_attributes = vec![ Attribute { key: HTTP_REQUEST_METHOD_ATTRIBUTE, value: request.method().to_string().into(), }, + Attribute { + key: AZ_NAMESPACE_ATTRIBUTE, + value: tracer.namespace().into(), + }, Attribute { key: AZ_SCHEMA_URL_ATTRIBUTE, value: request.url().scheme().into(), @@ -191,33 +223,17 @@ impl Policy for RequestInstrumentationPolicy { #[cfg(test)] mod tests { use super::*; - use std::sync::{Arc, Mutex}; - use typespec_client_core::{ + use crate::{ http::{ headers::Headers, policies::TransportPolicy, Method, RawResponse, StatusCode, TransportOptions, }, tracing::{AsAny, AttributeValue, Span, SpanStatus, Tracer, TracerProvider}, + Result, }; - - #[derive(Debug)] - struct MockTransport; - - #[async_trait::async_trait] - impl Policy for MockTransport { - async fn send( - &self, - _ctx: &Context, - _request: &mut Request, - _next: &[Arc], - ) -> PolicyResult { - PolicyResult::Ok(RawResponse::from_bytes( - StatusCode::Ok, - Headers::new(), - Vec::new(), - )) - } - } + use azure_core_test::http::MockHttpClient; + use futures::future::BoxFuture; + use std::sync::{Arc, Mutex}; #[derive(Debug)] struct MockTracingProvider { @@ -234,13 +250,15 @@ mod tests { impl TracerProvider for MockTracingProvider { fn get_tracer( &self, - crate_name: &str, - crate_version: &str, + azure_namespace: &'static str, + crate_name: &'static str, + crate_version: &'static str, ) -> Arc { let mut tracers = self.tracers.lock().unwrap(); let tracer = Arc::new(MockTracer { - name: crate_name.to_string(), - version: crate_version.to_string(), + namespace: azure_namespace, + name: crate_name, + version: crate_version, spans: Mutex::new(Vec::new()), }); @@ -251,12 +269,17 @@ mod tests { #[derive(Debug)] struct MockTracer { - name: String, - version: String, + namespace: &'static str, + name: &'static str, + version: &'static str, spans: Mutex>>, } impl Tracer for MockTracer { + fn namespace(&self) -> &'static str { + self.namespace + } + fn start_span_with_current( &self, name: &str, @@ -362,23 +385,30 @@ mod tests { } } - async fn run_instrumentation_test( + async fn run_instrumentation_test( + test_namespace: Option<&'static str>, crate_name: Option<&'static str>, version: Option<&'static str>, request: &mut Request, - ) -> Arc { + callback: C, + ) -> Arc + where + C: FnMut(&Request) -> BoxFuture<'_, Result> + Send + Sync + 'static, + { let mock_tracer = Arc::new(MockTracingProvider::new()); let options = RequestInstrumentationOptions { tracing_provider: Some(mock_tracer.clone()), }; let policy = Arc::new(RequestInstrumentationPolicy::new( + test_namespace, crate_name, version, - Some(&options), + &options, )); - let transport = - TransportPolicy::new(TransportOptions::new_custom_policy(Arc::new(MockTransport))); + let transport = TransportPolicy::new(TransportOptions::new(Arc::new(MockHttpClient::new( + callback, + )))); let ctx = Context::default(); let next: Vec> = vec![Arc::new(transport)]; @@ -388,9 +418,11 @@ mod tests { } fn check_instrumentation_result( mock_tracer: Arc, + expected_namespace: &str, expected_name: &str, expected_version: &str, expected_method: &str, + expected_status: SpanStatus, expected_attributes: Vec<(&str, AttributeValue)>, ) { assert_eq!( @@ -402,11 +434,13 @@ mod tests { let tracer = tracers.first().unwrap(); assert_eq!(tracer.name, expected_name); assert_eq!(tracer.version, expected_version); + assert_eq!(tracer.namespace, expected_namespace); let spans = tracer.spans.lock().unwrap(); assert_eq!(spans.len(), 1, "Expected one span to be created"); println!("Spans: {:?}", spans); let span = spans.first().unwrap(); assert_eq!(span.name, expected_method); + assert_eq!(*span.state.lock().unwrap(), expected_status); let attributes = span.attributes.lock().unwrap(); for attr in attributes.iter() { println!("Attribute: {} = {:?}", attr.key, attr.value); @@ -437,15 +471,37 @@ mod tests { let url = "http://example.com/path"; let mut request = Request::new(url.parse().unwrap(), Method::Get); - let mock_tracer = - run_instrumentation_test(Some("test_crate"), Some("1.0.0"), &mut request).await; + let mock_tracer = run_instrumentation_test( + Some("test namespace"), + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; check_instrumentation_result( mock_tracer, + "test namespace", "test_crate", "1.0.0", "GET", + SpanStatus::Unset, vec![ + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("http")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, @@ -471,15 +527,37 @@ mod tests { let mut request = Request::new(url.parse().unwrap(), Method::Get); request.insert_header(headers::CLIENT_REQUEST_ID, "test-client-request-id"); - let mock_tracer = - run_instrumentation_test(Some("test_crate"), Some("1.0.0"), &mut request).await; + let mock_tracer = run_instrumentation_test( + Some("test namespace"), + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; check_instrumentation_result( mock_tracer.clone(), + "test namespace", "test_crate", "1.0.0", "GET", + SpanStatus::Unset, vec![ + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), ( AZ_CLIENT_REQUEST_ID_ATTRIBUTE, @@ -508,14 +586,29 @@ mod tests { let url = "https://user:password@host:8080/path?query=value#fragment"; let mut request = Request::new(url.parse().unwrap(), Method::Get); - let mock_tracer_provider = run_instrumentation_test(None, None, &mut request).await; + let mock_tracer_provider = + run_instrumentation_test(None, None, None, &mut request, |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("host")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }) + .await; check_instrumentation_result( mock_tracer_provider, + "Unknown", "unknown", "unknown", "GET", + SpanStatus::Unset, vec![ + (AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("Unknown")), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, @@ -531,4 +624,66 @@ mod tests { ], ); } + + #[tokio::test] + async fn request_failed() { + let url = "https://microsoft.com/request_failed.htm"; + let mut request = Request::new(url.parse().unwrap(), Method::Put); + request.insert_header(headers::REQUEST_ID, "test-service-request-id"); + + let mock_tracer = run_instrumentation_test( + Some("test namespace"), + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("microsoft.com")); + assert_eq!(req.method(), &Method::Put); + Ok(RawResponse::from_bytes( + StatusCode::NotFound, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; + + check_instrumentation_result( + mock_tracer.clone(), + "test namespace", + "test_crate", + "1.0.0", + "PUT", + SpanStatus::Error { + description: "HTTP request failed with status code 404: Not Found".to_string(), + }, + vec![ + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), + (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + ( + AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-service-request-id"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(404), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("PUT")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("microsoft.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://microsoft.com/request_failed.htm"), + ), + ], + ); + } } diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index f2679a1302..10f0460e09 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -1,17 +1,3 @@ # Azure Core OpenTelemetry Tracing This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. It enables automatic span creation, context propagation, and telemetry collection for Azure service operations. - -## Features - -## Usage - -### Basic Setup - -### Creating Spans - -### Error Handling - -## Azure Conventions - -## Integration diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 914e1a599e..b356617a39 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -123,6 +123,7 @@ mod tests { use opentelemetry_sdk::trace::{in_memory_exporter::InMemorySpanExporter, SdkTracerProvider}; use std::io::{Error, ErrorKind}; use std::sync::Arc; + use tracing::trace; fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { let otel_exporter = InMemorySpanExporter::default(); @@ -139,7 +140,9 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Microsoft.SpecialCase", "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); span.end(); @@ -158,7 +161,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Special Name", "test", "0.1.0"); let parent_span = tracer.start_span("parent_span", SpanKind::Server, vec![]); let child_span = tracer.start_span_with_parent( "child_span", @@ -190,7 +195,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("MyNamespace", "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let child_span = @@ -220,7 +227,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Namespace", "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let _span_guard = span2 @@ -252,7 +261,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("ThisNamespace", "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.set_attribute("test_key", AttributeValue::String("test_value".to_string())); @@ -276,7 +287,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("namespace", "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let error = Error::new(ErrorKind::NotFound, "resource not found"); @@ -303,7 +316,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Namespace", "test", "0.1.0"); // Test Ok status let span = tracer.start_span("test_span_ok", SpanKind::Server, vec![]); @@ -335,7 +350,9 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider.unwrap().get_tracer("test", "0.1.0"); + let tracer = tracer_provider + .unwrap() + .get_tracer("Namespace", "test", "0.1.0"); let future = async { let context = Context::current(); @@ -371,13 +388,14 @@ mod tests { let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 1); for span in &spans { + trace!("Span: {:?}", span); assert_eq!(span.name, "test_span"); assert_eq!(span.events.len(), 1); - assert_eq!(span.attributes.len(), 1); + assert_eq!(span.attributes.len(), 2); assert_eq!(span.attributes[0].key, "test_key".into()); assert_eq!( format!("{:?}", span.attributes[0].value), - "String(Static(\"test_value\"))" + "String(Owned(\"test_value\"))" ); } } diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index 8231bf7739..9a48b08154 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -26,15 +26,18 @@ impl OpenTelemetryTracerProvider { impl TracerProvider for OpenTelemetryTracerProvider { fn get_tracer( &self, - name: &'static str, + namespace: &'static str, + package_name: &'static str, package_version: &'static str, ) -> Arc { - let scope = InstrumentationScope::builder(name) + let scope = InstrumentationScope::builder(package_name) .with_version(package_version) + .with_schema_url("https://opentelemetry.io/schemas/1.23.0") .build(); - Arc::new(OpenTelemetryTracer::new(BoxedTracer::new( - self.inner.boxed_tracer(scope), - ))) + Arc::new(OpenTelemetryTracer::new( + namespace, + BoxedTracer::new(self.inner.boxed_tracer(scope)), + )) } } diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index d4aa7afc69..80fd6f6716 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -15,17 +15,25 @@ use opentelemetry::{ use std::sync::Arc; pub struct OpenTelemetryTracer { + namespace: &'static str, inner: BoxedTracer, } impl OpenTelemetryTracer { /// Creates a new OpenTelemetry tracer with the given inner tracer. - pub(super) fn new(tracer: BoxedTracer) -> Self { - Self { inner: tracer } + pub(super) fn new(namespace: &'static str, tracer: BoxedTracer) -> Self { + Self { + namespace, + inner: tracer, + } } } impl Tracer for OpenTelemetryTracer { + fn namespace(&self) -> &'static str { + self.namespace + } + fn start_span( &self, name: &'static str, @@ -104,7 +112,7 @@ mod tests { fn test_create_tracer() { let noop_tracer = NoopTracerProvider::new(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); - let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer("name", "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); } @@ -113,14 +121,14 @@ mod tests { fn test_create_tracer_with_sdk_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); - let _tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); + let _tracer = otel_provider.get_tracer("My.Namespace", "test_tracer", "1.0.0"); } #[test] fn test_create_span_from_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); - let tracer = otel_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer("My.Namespace", "test_tracer", "1.0.0"); let _span = tracer.start_span("test_span", SpanKind::Internal, vec![]); } } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index b9cc00973b..af36fcd452 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -14,7 +14,7 @@ async fn test_span_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer("test_namespace", "test_tracer", "1.0.0"); // Create a span using the Azure tracer let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); @@ -42,7 +42,7 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer and verify it works - let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer("tes.namespace", "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); @@ -56,7 +56,7 @@ async fn test_span_attributes() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer("test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer("test.namespace", "test_tracer", "1.0.0"); // Create span with multiple attributes let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 911a50919e..c3a2ac8b8a 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -7,10 +7,10 @@ use azure_core::{ credentials::TokenCredential, fmt::SafeDebug, http::{ - policies::{BearerTokenCredentialPolicy, Policy}, ClientMethodOptions, ClientOptions, Pipeline, RawResponse, Request, RequestInstrumentationOptions, Url, }, + tracing::{Attribute, Tracer}, Result, }; use azure_core_opentelemetry::OpenTelemetryTracerProvider; @@ -27,6 +27,7 @@ pub struct TestServiceClient { endpoint: Url, api_version: String, pipeline: Pipeline, + tracer: Option>, } #[derive(Default, SafeDebug)] @@ -37,7 +38,7 @@ pub struct TestServiceClientGetMethodOptions<'a> { impl TestServiceClient { pub fn new( endpoint: &str, - credential: Arc, + _credential: Arc, options: Option, ) -> Result { let options = options.unwrap_or_default(); @@ -49,20 +50,23 @@ impl TestServiceClient { )); } endpoint.set_query(None); - let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( - credential, - vec!["https://vault.azure.net/.default"], - )); - let request_instrumentation_policy = Arc::new( - azure_core::http::policies::RequestInstrumentationPolicy::new( - option_env!("CARGO_PKG_NAME"), - option_env!("CARGO_PKG_VERSION"), - options - .azure_client_options - .request_instrumentation - .as_ref(), - ), - ); + + let tracer = + if let Some(tracer_options) = &options.azure_client_options.request_instrumentation { + tracer_options + .tracing_provider + .as_ref() + .map(|tracing_provider| { + tracing_provider.get_tracer( + "Az.TestServiceClient", + option_env!("CARGO_PKG_NAME").unwrap_or("test_service_client"), + option_env!("CARGO_PKG_VERSION").unwrap_or("0.1.0"), + ) + }) + } else { + None + }; + Ok(Self { endpoint, api_version: options.api_version, @@ -71,8 +75,9 @@ impl TestServiceClient { option_env!("CARGO_PKG_VERSION"), options.azure_client_options, Vec::default(), - vec![auth_policy, request_instrumentation_policy], + Vec::default(), ), + tracer, }) } @@ -81,6 +86,11 @@ impl TestServiceClient { &self.endpoint } + /// Returns the result of a Get verb against the configured endpoint with the specified path. + /// + /// This method demonstrates a service client which does not have per-method spans but which will create + /// HTTP client spans if the `RequestInstrumentationOptions` are configured in the client options. + /// pub async fn get( &self, path: &str, @@ -109,6 +119,60 @@ impl TestServiceClient { } Ok(response) } + + /// Returns the result of a Get verb against the configured endpoint with the specified path. + /// + /// This method demonstrates a service client which has per-method spans and uses the configured tracing + /// tracing provider to create per-api spans for the function. + /// + /// To configure per-api spans, your service implementation needs to do the following: + /// 1. If the client is configured with a [`Tracer`], it will create a span whose name matches the function. + /// 1. The span should be created with the `SpanKind::Internal` kind, and + /// 2. The span should have the `az.namespace` attribute set to the namespace of the service client. + /// 2. The function should add the span created in step 1 to the ClientMethodOptions context. + /// 3. The function should add the tracer to the ClientMethodOptions context so that the pipeline can use it to populate the `az.namespace` property in the request span. + /// 4. The function should then perform the normal client operations after setting up the context. + /// 5. After the client operation completes, if the function failed, it should add an `error.type` attribute to the span + /// with the error type. + /// + /// # Note + /// This applies to most HTTP client operations, but not all. CosmosDB has its own set of conventions as listed + /// [here](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/cosmosdb.md) + /// + pub async fn get_with_function_tracing( + &self, + path: &str, + options: Option>, + ) -> Result { + let mut options = options.unwrap_or_default(); + let mut ctx = options.method_options.context.clone(); + let span = if let Some(tracer) = &self.tracer { + let span = tracer.start_span( + "get_with_tracing", + azure_core::tracing::SpanKind::Internal, + vec![Attribute { + key: "az.namespace", + value: tracer.namespace().into(), + }], + ); + // We need to add the span to the context because the pipeline will use it as the parent span + // for the request span. + ctx = ctx.with_value(span.clone()); + // And we need to add the tracer to the context so that the pipeline can use it to populate the + // az.namespace property in the request span. + ctx = ctx.with_value(tracer.clone()); + Some(span) + } else { + None + }; + options.method_options.context = ctx; + let response = self.get(path, Some(options)).await; + if let Some(span) = span { + span.set_status(azure_core::tracing::SpanStatus::Ok); + span.end(); + }; + response + } } #[cfg(test)] @@ -116,6 +180,10 @@ mod tests { use super::*; use azure_core::Result; use azure_core_test::{recorded, TestContext}; + use opentelemetry::trace::{ + SpanKind as OpenTelemetrySpanKind, Status as OpenTelemetrySpanStatus, + }; + use opentelemetry::Value as OpenTelemetryAttributeValue; use tracing::{info, trace}; fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { @@ -127,6 +195,57 @@ mod tests { (otel_tracer_provider, otel_exporter) } + // Span verification utility functions. + + struct ExpectedSpan { + name: &'static str, + kind: OpenTelemetrySpanKind, + parent_span_id: Option, + status: OpenTelemetrySpanStatus, + attributes: Vec<(&'static str, OpenTelemetryAttributeValue)>, + } + + fn verify_span( + span: &opentelemetry_sdk::trace::SpanData, + expected: ExpectedSpan, + ) -> Result<()> { + assert_eq!(span.name, expected.name); + assert_eq!(span.span_kind, expected.kind); + assert_eq!(span.status, expected.status); + assert_eq!( + span.parent_span_id, + expected + .parent_span_id + .unwrap_or(opentelemetry::trace::SpanId::INVALID) + ); + + for attr in span.attributes.iter() { + println!("Attribute: {} = {:?}", attr.key, attr.value); + let mut found = false; + for (key, value) in expected.attributes.iter() { + if attr.key.as_str() == (*key) { + found = true; + // Skip checking the value for "" as it is a placeholder + if *value != OpenTelemetryAttributeValue::String("".into()) { + assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", *key); + } + break; + } + } + if !found { + panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); + } + } + for (key, value) in expected.attributes.iter() { + if !span.attributes.iter().any(|attr| attr.key == (*key).into()) { + panic!("Expected attribute not found: {} = {:?}", key, value); + } + } + + Ok(()) + } + + // Basic functionality tests. #[recorded::test()] async fn test_service_client_new(ctx: TestContext) -> Result<()> { let recording = ctx.recording(); @@ -144,17 +263,14 @@ mod tests { Ok(()) } + // Ensure that the the test client actually does what it's supposed to do without telemetry. #[recorded::test()] async fn test_service_client_get(ctx: TestContext) -> Result<()> { let recording = ctx.recording(); let endpoint = "https://example.com"; let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), - ..Default::default() - }; - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let client = TestServiceClient::new(endpoint, credential, None).unwrap(); let response = client.get("index.html", None).await; info!("Response: {:?}", response); assert!(response.is_ok()); @@ -192,8 +308,168 @@ mod tests { assert_eq!(spans.len(), 1); for span in &spans { trace!("Span: {:?}", span); + + verify_span( + span, + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + status: OpenTelemetrySpanStatus::Unset, + parent_span_id: None, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Unknown".into()), + ("az.schema.url", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "index.html?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend.count", 0.into()), + ("http.response.status_code", 200.into()), + ], + }, + )?; + } + + Ok(()) + } + + #[recorded::test()] + async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("failing_url", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + for span in &spans { + trace!("Span: {:?}", span); + + verify_span( + span, + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Error { + description: "HTTP request failed with status code 404: Not Found".into(), + }, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Unknown".into()), + ("az.schema.url", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend.count", 0.into()), + ("http.response.status_code", 404.into()), + ], + }, + )?; } Ok(()) } + + #[recorded::test()] + async fn test_service_client_get_with_full_tracing(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + api_version: "2023-10-01".to_string(), + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get_with_function_tracing("index.html", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + for span in &spans { + trace!("Span: {:?}", span); + } + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[1].span_context.span_id()), + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.schema.url", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "index.html?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend.count", 0.into()), + ("http.response.status_code", 200.into()), + ], + }, + )?; + verify_span( + &spans[1], + ExpectedSpan { + name: "get_with_tracing", + kind: OpenTelemetrySpanKind::Internal, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Ok, + attributes: vec![("az.namespace", "Az.TestServiceClient".into())], + }, + )?; + + Ok(()) + } } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index 23349e8a24..b3eadd93fb 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -29,10 +29,14 @@ pub trait TracerProvider: Send + Sync { /// Returns a tracer for the given name. /// /// Arguments: + /// - `namespace_name`: The namespace of the package for which the tracer is requested. See + /// [this page](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers) + /// for more information on namespace names. /// - `package_name`: The name of the package for which the tracer is requested. /// - `package_version`: The version of the package for which the tracer is requested. fn get_tracer( &self, + namespace_name: &'static str, package_name: &'static str, package_version: &'static str, ) -> Arc; @@ -96,6 +100,9 @@ pub trait Tracer: Send + Sync { attributes: Vec, parent: Arc, ) -> Arc; + + /// Returns the namespace the tracer was configured with. + fn namespace(&self) -> &'static str; } impl Debug for dyn Tracer { @@ -104,7 +111,7 @@ impl Debug for dyn Tracer { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum SpanStatus { Unset, Ok, From 22f4acc9c6b2f33a14fb8d617d4dc9082bfe28aa Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 09:09:04 -0700 Subject: [PATCH 29/84] Added test recording for opentelemetry tests --- Cargo.lock | 2 -- sdk/core/azure_core_opentelemetry/Cargo.toml | 2 -- sdk/core/azure_core_opentelemetry/assets.json | 6 ++++++ 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 sdk/core/azure_core_opentelemetry/assets.json diff --git a/Cargo.lock b/Cargo.lock index ff07b561ee..d0d44591ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,11 +206,9 @@ dependencies = [ name = "azure_core_opentelemetry" version = "0.1.0" dependencies = [ - "async-trait", "azure_core", "azure_core_test", "azure_core_test_macros", - "log", "opentelemetry 0.30.0", "opentelemetry_sdk 0.30.0", "tokio", diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index ee4ef6329a..2e4076e9e1 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -16,13 +16,11 @@ edition.workspace = true [dependencies] azure_core.workspace = true -log.workspace = true opentelemetry = { version = "0.30", features = ["trace"] } tracing.workspace = true typespec_client_core.workspace = true [dev-dependencies] -async-trait.workspace = true azure_core_test = { workspace = true, features = ["tracing"] } azure_core_test_macros.workspace = true opentelemetry_sdk = { version = "0.30", features = ["testing"] } diff --git a/sdk/core/azure_core_opentelemetry/assets.json b/sdk/core/azure_core_opentelemetry/assets.json new file mode 100644 index 0000000000..8f0a2844a2 --- /dev/null +++ b/sdk/core/azure_core_opentelemetry/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "rust", + "Tag": "", + "TagPrefix": "rust/azure_core_opentelemetry" +} \ No newline at end of file From 0630d0e16b209efdcf69df4d55bddf1eb0f96726 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 09:19:29 -0700 Subject: [PATCH 30/84] cargo publish fix --- .../src/http/policies/request_instrumentation.rs | 2 ++ sdk/core/azure_core_opentelemetry/src/span.rs | 2 ++ .../typespec_client_core/src/tracing/attributes.rs | 2 +- sdk/typespec/typespec_client_core/src/tracing/mod.rs | 7 ++++--- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 81a84dd6d8..7911028f9d 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -377,6 +377,8 @@ mod tests { { todo!() } + + fn propagate_headers(&self, request: &mut Request) {} } impl AsAny for MockSpan { diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index b356617a39..6f32d7f160 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -77,6 +77,8 @@ impl Span for OpenTelemetrySpan { self.context.span().set_status(otel_status); } + fn propagate_headers(&self, _request: &mut azure_core::http::Request) {} + fn set_current( &self, _context: &azure_core::http::Context, diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index f0321a941c..73e3a0fb73 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::fmt::SafeDebug; +use typespec_macros::SafeDebug; /// An array of homogeneous attribute values. #[derive(SafeDebug, PartialEq)] diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index b3eadd93fb..f889cd19a2 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -3,9 +3,8 @@ //! Distributed tracing trait definitions //! -use crate::http::Context; -use std::fmt::Debug; -use std::sync::Arc; +use crate::http::{Context, Request}; +use std::{fmt::Debug, sync::Arc}; /// Overall architecture for distributed tracing in the SDK. /// @@ -175,6 +174,8 @@ pub trait Span: AsAny + Send + Sync { /// enabling it to be used for tracing operations within that context. /// fn set_current(&self, context: &Context) -> crate::Result>; + + fn propagate_headers(&self, request: &mut Request); } /// A trait that allows an object to be downcast to a reference of type `Any`. From 635c218408d4474eb7ccb2ebd65570ceefc00e1a Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 09:30:39 -0700 Subject: [PATCH 31/84] cargo publish fix 2 --- sdk/core/azure_core_opentelemetry/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index 2e4076e9e1..674d4b1615 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -17,6 +17,7 @@ edition.workspace = true [dependencies] azure_core.workspace = true opentelemetry = { version = "0.30", features = ["trace"] } +opentelemetry-http = "0.30.0" tracing.workspace = true typespec_client_core.workspace = true From e043e1220bd696474ddc04a2ef3debde661ed746 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 09:34:45 -0700 Subject: [PATCH 32/84] Oops, commited too little stuff --- Cargo.lock | 31 +++++++++++++++++++ .../http/policies/request_instrumentation.rs | 2 +- .../src/tracing/attributes.rs | 12 ++++--- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0d44591ac..d3bcf2f9c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,7 @@ dependencies = [ "azure_core_test", "azure_core_test_macros", "opentelemetry 0.30.0", + "opentelemetry-http", "opentelemetry_sdk 0.30.0", "tokio", "tracing", @@ -1708,6 +1709,36 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry 0.30.0", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0da0d6b47a3dbc6e9c9e36a0520e25cf943e046843818faaa3f87365a548c82" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry 0.25.0", + "percent-encoding", + "rand 0.8.5", + "thiserror 1.0.69", +] + [[package]] name = "opentelemetry_sdk" version = "0.30.0" diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 7911028f9d..a38ef054f7 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -378,7 +378,7 @@ mod tests { todo!() } - fn propagate_headers(&self, request: &mut Request) {} + fn propagate_headers(&self, _request: &mut Request) {} } impl AsAny for MockSpan { diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 73e3a0fb73..00dc5c0442 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use typespec_macros::SafeDebug; +#[cfg(feature = "derive")] +use crate::fmt::SafeDebug; /// An array of homogeneous attribute values. -#[derive(SafeDebug, PartialEq)] +#[cfg_attr(feature = "derive", derive(SafeDebug))] +#[derive(PartialEq)] pub enum AttributeArray { /// An array of boolean values. Bool(Vec), @@ -17,7 +19,8 @@ pub enum AttributeArray { } /// Represents a single attribute value, which can be of various types -#[derive(SafeDebug, PartialEq)] +#[cfg_attr(feature = "derive", derive(SafeDebug))] +#[derive(PartialEq)] pub enum AttributeValue { /// A boolean attribute value. Bool(bool), @@ -30,7 +33,8 @@ pub enum AttributeValue { /// An array of attribute values. Array(AttributeArray), } -#[derive(SafeDebug)] + +#[cfg_attr(feature = "derive", derive(SafeDebug))] pub struct Attribute { /// A key-value pair attribute. pub key: &'static str, From 1db8a915666892f7517a1b9d4a1f9e60c1134e90 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 11:52:16 -0700 Subject: [PATCH 33/84] OTel feedback from Liudmila --- .../http/policies/request_instrumentation.rs | 68 +++++---- sdk/core/azure_core_opentelemetry/src/span.rs | 32 ++-- .../azure_core_opentelemetry/src/telemetry.rs | 2 +- .../azure_core_opentelemetry/src/tracer.rs | 12 +- .../tests/integration_test.rs | 6 +- .../tests/telemetry_service_implementation.rs | 144 +++++++++++++++--- .../typespec_client_core/src/tracing/mod.rs | 13 +- 7 files changed, 199 insertions(+), 78 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index a38ef054f7..0b412361e6 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -58,7 +58,7 @@ impl RequestInstrumentationPolicy { if let Some(tracing_provider) = &options.tracing_provider { Self { tracer: Some(tracing_provider.get_tracer( - azure_namespace.unwrap_or("Unknown"), + azure_namespace, crate_name.unwrap_or("unknown"), crate_version.unwrap_or("unknown"), )), @@ -94,16 +94,20 @@ impl Policy for RequestInstrumentationPolicy { key: HTTP_REQUEST_METHOD_ATTRIBUTE, value: request.method().to_string().into(), }, - Attribute { - key: AZ_NAMESPACE_ATTRIBUTE, - value: tracer.namespace().into(), - }, Attribute { key: AZ_SCHEMA_URL_ATTRIBUTE, value: request.url().scheme().into(), }, ]; + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + span_attributes.push(Attribute { + key: AZ_NAMESPACE_ATTRIBUTE, + value: namespace.into(), + }); + } + if !request.url().username().is_empty() || request.url().password().is_some() { // If the URL contains a password, we do not log it for security reasons. let full_url = format!( @@ -186,12 +190,17 @@ impl Policy for RequestInstrumentationPolicy { let result = next[0].send(ctx, request, &next[1..]).await; - if result.is_err() { + if let Some(err) = result.as_ref().err() { // If the request failed, set an error type attribute. - span.set_attribute( - ERROR_TYPE_ATTRIBUTE, - result.as_ref().err().unwrap().to_string().into(), - ); + let azure_error = err.downcast_ref::(); + if let Some(err_kind) = azure_error.map(|e| e.kind()) { + // If the error is an Azure core error, we set the error type. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, err_kind.to_string().into()); + } else { + // Otherwise, we set the error type to the error's text. This should never happen + // as the error should be an Azure core error. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, err.to_string().into()); + } } if let Ok(response) = result.as_ref() { // If the request was successful, set the HTTP response status code. @@ -202,13 +211,12 @@ impl Policy for RequestInstrumentationPolicy { if response.status().is_server_error() || response.status().is_client_error() { // If the response status indicates an error, set the span status to error. + // Since the reason can be inferred from the status code, description is left empty. span.set_status(crate::tracing::SpanStatus::Error { - description: format!( - "HTTP request failed with status code {}: {}", - response.status(), - response.status().canonical_reason() - ), + description: "".to_string(), }); + // Set the error type attribute for all HTTP 4XX or 5XX errors. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); } } @@ -250,7 +258,7 @@ mod tests { impl TracerProvider for MockTracingProvider { fn get_tracer( &self, - azure_namespace: &'static str, + azure_namespace: Option<&'static str>, crate_name: &'static str, crate_version: &'static str, ) -> Arc { @@ -269,14 +277,14 @@ mod tests { #[derive(Debug)] struct MockTracer { - namespace: &'static str, + namespace: Option<&'static str>, name: &'static str, version: &'static str, spans: Mutex>>, } impl Tracer for MockTracer { - fn namespace(&self) -> &'static str { + fn namespace(&self) -> Option<&'static str> { self.namespace } @@ -420,7 +428,7 @@ mod tests { } fn check_instrumentation_result( mock_tracer: Arc, - expected_namespace: &str, + expected_namespace: Option<&str>, expected_name: &str, expected_version: &str, expected_method: &str, @@ -494,7 +502,7 @@ mod tests { check_instrumentation_result( mock_tracer, - "test namespace", + Some("test namespace"), "test_crate", "1.0.0", "GET", @@ -530,7 +538,7 @@ mod tests { request.insert_header(headers::CLIENT_REQUEST_ID, "test-client-request-id"); let mock_tracer = run_instrumentation_test( - Some("test namespace"), + None, Some("test_crate"), Some("1.0.0"), &mut request, @@ -550,16 +558,12 @@ mod tests { check_instrumentation_result( mock_tracer.clone(), - "test namespace", + None, "test_crate", "1.0.0", "GET", SpanStatus::Unset, vec![ - ( - AZ_NAMESPACE_ATTRIBUTE, - AttributeValue::from("test namespace"), - ), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), ( AZ_CLIENT_REQUEST_ID_ATTRIBUTE, @@ -604,13 +608,12 @@ mod tests { check_instrumentation_result( mock_tracer_provider, - "Unknown", + None, "unknown", "unknown", "GET", SpanStatus::Unset, vec![ - (AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("Unknown")), (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, @@ -654,14 +657,19 @@ mod tests { check_instrumentation_result( mock_tracer.clone(), - "test namespace", + Some("test namespace"), "test_crate", "1.0.0", "PUT", SpanStatus::Error { - description: "HTTP request failed with status code 404: Not Found".to_string(), + description: "".to_string(), }, vec![ + (ERROR_TYPE_ATTRIBUTE, AttributeValue::from("404")), + ( + AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-service-request-id"), + ), ( AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("test namespace"), diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 6f32d7f160..b3abef4118 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -71,7 +71,6 @@ impl Span for OpenTelemetrySpan { fn set_status(&self, status: SpanStatus) { let otel_status = match status { SpanStatus::Unset => opentelemetry::trace::Status::Unset, - SpanStatus::Ok => opentelemetry::trace::Status::Ok, SpanStatus::Error { description } => opentelemetry::trace::Status::error(description), }; self.context.span().set_status(otel_status); @@ -142,9 +141,10 @@ mod tests { let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer("Microsoft.SpecialCase", "test", "0.1.0"); + let tracer = + tracer_provider + .unwrap() + .get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); span.end(); @@ -165,7 +165,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("Special Name", "test", "0.1.0"); + .get_tracer(Some("Special Name"), "test", "0.1.0"); let parent_span = tracer.start_span("parent_span", SpanKind::Server, vec![]); let child_span = tracer.start_span_with_parent( "child_span", @@ -199,7 +199,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("MyNamespace", "test", "0.1.0"); + .get_tracer(Some("MyNamespace"), "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let child_span = @@ -231,7 +231,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("Namespace", "test", "0.1.0"); + .get_tracer(Some("Namespace"), "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let _span_guard = span2 @@ -265,7 +265,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("ThisNamespace", "test", "0.1.0"); + .get_tracer(Some("ThisNamespace"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.set_attribute("test_key", AttributeValue::String("test_value".to_string())); @@ -291,7 +291,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("namespace", "test", "0.1.0"); + .get_tracer(Some("namespace"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let error = Error::new(ErrorKind::NotFound, "resource not found"); @@ -320,11 +320,10 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("Namespace", "test", "0.1.0"); + .get_tracer(Some("Namespace"), "test", "0.1.0"); - // Test Ok status - let span = tracer.start_span("test_span_ok", SpanKind::Server, vec![]); - span.set_status(SpanStatus::Ok); + // Test Unset status + let span = tracer.start_span("test_span_unset", SpanKind::Server, vec![]); span.end(); // Test Error status @@ -337,14 +336,13 @@ mod tests { let spans = otel_exporter.get_finished_spans().unwrap(); assert_eq!(spans.len(), 2); - let ok_span = spans.iter().find(|s| s.name == "test_span_ok").unwrap(); - assert_eq!(ok_span.status, opentelemetry::trace::Status::Ok); - let error_span = spans.iter().find(|s| s.name == "test_span_error").unwrap(); assert_eq!( error_span.status, opentelemetry::trace::Status::error("test error") ); + let unset_span = spans.iter().find(|s| s.name == "test_span_unset").unwrap(); + assert_eq!(unset_span.status, opentelemetry::trace::Status::Unset); } #[tokio::test] @@ -354,7 +352,7 @@ mod tests { assert!(tracer_provider.is_ok()); let tracer = tracer_provider .unwrap() - .get_tracer("Namespace", "test", "0.1.0"); + .get_tracer(Some("Namespace"), "test", "0.1.0"); let future = async { let context = Context::current(); diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index 9a48b08154..36dec069c9 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -26,7 +26,7 @@ impl OpenTelemetryTracerProvider { impl TracerProvider for OpenTelemetryTracerProvider { fn get_tracer( &self, - namespace: &'static str, + namespace: Option<&'static str>, package_name: &'static str, package_version: &'static str, ) -> Arc { diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 80fd6f6716..6282f5cdb5 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -15,13 +15,13 @@ use opentelemetry::{ use std::sync::Arc; pub struct OpenTelemetryTracer { - namespace: &'static str, + namespace: Option<&'static str>, inner: BoxedTracer, } impl OpenTelemetryTracer { /// Creates a new OpenTelemetry tracer with the given inner tracer. - pub(super) fn new(namespace: &'static str, tracer: BoxedTracer) -> Self { + pub(super) fn new(namespace: Option<&'static str>, tracer: BoxedTracer) -> Self { Self { namespace, inner: tracer, @@ -30,7 +30,7 @@ impl OpenTelemetryTracer { } impl Tracer for OpenTelemetryTracer { - fn namespace(&self) -> &'static str { + fn namespace(&self) -> Option<&'static str> { self.namespace } @@ -112,7 +112,7 @@ mod tests { fn test_create_tracer() { let noop_tracer = NoopTracerProvider::new(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); - let tracer = otel_provider.get_tracer("name", "test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer(Some("name"), "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); } @@ -121,14 +121,14 @@ mod tests { fn test_create_tracer_with_sdk_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); - let _tracer = otel_provider.get_tracer("My.Namespace", "test_tracer", "1.0.0"); + let _tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); } #[test] fn test_create_span_from_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); - let tracer = otel_provider.get_tracer("My.Namespace", "test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); let _span = tracer.start_span("test_span", SpanKind::Internal, vec![]); } } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index af36fcd452..e6165d45a0 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -14,7 +14,7 @@ async fn test_span_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer("test_namespace", "test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer(Some("test_namespace"), "test_tracer", "1.0.0"); // Create a span using the Azure tracer let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); @@ -42,7 +42,7 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer and verify it works - let tracer = azure_provider.get_tracer("tes.namespace", "test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); @@ -56,7 +56,7 @@ async fn test_span_attributes() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer("test.namespace", "test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); // Create span with multiple attributes let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index c3a2ac8b8a..edd8e85128 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -17,10 +17,19 @@ use azure_core_opentelemetry::OpenTelemetryTracerProvider; use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider}; use std::sync::Arc; -#[derive(Default, Clone, SafeDebug)] +#[derive(Clone, SafeDebug)] pub struct TestServiceClientOptions { pub azure_client_options: ClientOptions, - pub api_version: String, + pub api_version: Option, +} + +impl Default for TestServiceClientOptions { + fn default() -> Self { + Self { + azure_client_options: ClientOptions::default(), + api_version: Some("2023-10-01".to_string()), + } + } } pub struct TestServiceClient { @@ -58,7 +67,7 @@ impl TestServiceClient { .as_ref() .map(|tracing_provider| { tracing_provider.get_tracer( - "Az.TestServiceClient", + Some("Az.TestServiceClient"), option_env!("CARGO_PKG_NAME").unwrap_or("test_service_client"), option_env!("CARGO_PKG_VERSION").unwrap_or("0.1.0"), ) @@ -69,7 +78,7 @@ impl TestServiceClient { Ok(Self { endpoint, - api_version: options.api_version, + api_version: options.api_version.unwrap_or_default(), pipeline: Pipeline::new( option_env!("CARGO_PKG_NAME"), option_env!("CARGO_PKG_VERSION"), @@ -147,13 +156,18 @@ impl TestServiceClient { let mut options = options.unwrap_or_default(); let mut ctx = options.method_options.context.clone(); let span = if let Some(tracer) = &self.tracer { + let mut attributes = Vec::new(); + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + attributes.push(Attribute { + key: "az.namespace", + value: namespace.into(), + }); + } let span = tracer.start_span( "get_with_tracing", azure_core::tracing::SpanKind::Internal, - vec![Attribute { - key: "az.namespace", - value: tracer.namespace().into(), - }], + attributes, ); // We need to add the span to the context because the pipeline will use it as the parent span // for the request span. @@ -168,7 +182,26 @@ impl TestServiceClient { options.method_options.context = ctx; let response = self.get(path, Some(options)).await; if let Some(span) = span { - span.set_status(azure_core::tracing::SpanStatus::Ok); + if let Err(e) = &response { + // If the request failed, we set the error type on the span. + match e.kind() { + azure_core::error::ErrorKind::HttpResponse { status, .. } => { + span.set_attribute("error.type", status.to_string().into()); + if status.is_server_error() || status.is_client_error() { + span.set_status(azure_core::tracing::SpanStatus::Error { + description: "".to_string(), + }); + } + } + _ => { + span.set_attribute("error.type", e.kind().to_string().into()); + span.set_status(azure_core::tracing::SpanStatus::Error { + description: e.kind().to_string(), + }); + } + } + } + span.end(); }; response @@ -252,7 +285,6 @@ mod tests { let endpoint = "https://example.com"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), ..Default::default() }; @@ -288,13 +320,13 @@ mod tests { let endpoint = "https://example.com"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), }), ..Default::default() }, + ..Default::default() }; let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); @@ -318,7 +350,6 @@ mod tests { parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("az.namespace", "Unknown".into()), ("az.schema.url", "https".into()), ("az.client.request.id", "".into()), ( @@ -351,13 +382,13 @@ mod tests { let endpoint = "https://example.com"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), }), ..Default::default() }, + ..Default::default() }; let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); @@ -376,11 +407,10 @@ mod tests { kind: OpenTelemetrySpanKind::Client, parent_span_id: None, status: OpenTelemetrySpanStatus::Error { - description: "HTTP request failed with status code 404: Not Found".into(), + description: "".into(), }, attributes: vec![ ("http.request.method", "GET".into()), - ("az.namespace", "Unknown".into()), ("az.schema.url", "https".into()), ("az.client.request.id", "".into()), ( @@ -394,6 +424,7 @@ mod tests { ), ("server.address", "example.com".into()), ("server.port", 443.into()), + ("error.type", "404".into()), ("http.request.resend.count", 0.into()), ("http.response.status_code", 404.into()), ], @@ -405,7 +436,7 @@ mod tests { } #[recorded::test()] - async fn test_service_client_get_with_full_tracing(ctx: TestContext) -> Result<()> { + async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); @@ -413,13 +444,13 @@ mod tests { let endpoint = "https://example.com"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { - api_version: "2023-10-01".to_string(), azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), }), ..Default::default() }, + ..Default::default() }; let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); @@ -465,11 +496,88 @@ mod tests { name: "get_with_tracing", kind: OpenTelemetrySpanKind::Internal, parent_span_id: None, - status: OpenTelemetrySpanStatus::Ok, + status: OpenTelemetrySpanStatus::Unset, attributes: vec![("az.namespace", "Az.TestServiceClient".into())], }, )?; Ok(()) } + + #[recorded::test()] + async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get_with_function_tracing("failing_url", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + for span in &spans { + trace!("Span: {:?}", span); + } + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[1].span_context.span_id()), + status: OpenTelemetrySpanStatus::Error { + description: "".into(), + }, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.schema.url", "https".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend.count", 0.into()), + ("http.response.status_code", 404.into()), + ("error.type", "404".into()), + ], + }, + )?; + verify_span( + &spans[1], + ExpectedSpan { + name: "get_with_tracing", + kind: OpenTelemetrySpanKind::Internal, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Error { + description: "".into(), + }, + attributes: vec![ + ("az.namespace", "Az.TestServiceClient".into()), + ("error.type", "404".into()), + ], + }, + )?; + + Ok(()) + } } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index f889cd19a2..bbb202dcd4 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -35,7 +35,7 @@ pub trait TracerProvider: Send + Sync { /// - `package_version`: The version of the package for which the tracer is requested. fn get_tracer( &self, - namespace_name: &'static str, + namespace_name: Option<&'static str>, package_name: &'static str, package_version: &'static str, ) -> Arc; @@ -101,7 +101,7 @@ pub trait Tracer: Send + Sync { ) -> Arc; /// Returns the namespace the tracer was configured with. - fn namespace(&self) -> &'static str; + fn namespace(&self) -> Option<&'static str>; } impl Debug for dyn Tracer { @@ -110,10 +110,17 @@ impl Debug for dyn Tracer { } } +/// The status of a span. +/// +/// This enum represents the possible statuses of a span in distributed tracing. +/// It can be either `Unset`, indicating that the span has not been set to any specific status, +/// or `Error`, which contains a description of the error that occurred during the span's execution +/// +/// Note that OpenTelemetry defines an `Ok` status but that status is reserved for application and service developers, +/// so libraries should never set it. #[derive(Debug, PartialEq)] pub enum SpanStatus { Unset, - Ok, Error { description: String }, } From 15aa29c56f952fbeaaf05b557cfd51dcd91220c8 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 15:17:45 -0700 Subject: [PATCH 34/84] Propagate OpenTelemetry trace headers into request headers --- Cargo.lock | 1 + .../http/policies/request_instrumentation.rs | 19 +++++- sdk/core/azure_core_opentelemetry/Cargo.toml | 3 + sdk/core/azure_core_opentelemetry/src/span.rs | 64 ++++++++++++++++++- 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3bcf2f9c8..c31e5e0511 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,7 @@ dependencies = [ "opentelemetry 0.30.0", "opentelemetry-http", "opentelemetry_sdk 0.30.0", + "reqwest", "tokio", "tracing", "tracing-subscriber", diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 0b412361e6..c27cd2e88c 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -188,6 +188,9 @@ impl Policy for RequestInstrumentationPolicy { span.set_attribute(HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, retry_count.0.into()); } + // Propagate the headers for distributed tracing into the request. + span.propagate_headers(request); + let result = next[0].send(ctx, request, &next[1..]).await; if let Some(err) = result.as_ref().err() { @@ -242,6 +245,7 @@ mod tests { use azure_core_test::http::MockHttpClient; use futures::future::BoxFuture; use std::sync::{Arc, Mutex}; + use typespec_client_core::http::headers::HeaderName; #[derive(Debug)] struct MockTracingProvider { @@ -386,7 +390,15 @@ mod tests { todo!() } - fn propagate_headers(&self, _request: &mut Request) {} + /// Insert two dummy headers for distributed tracing. + // cspell: ignore traceparent tracestate + fn propagate_headers(&self, request: &mut Request) { + request.insert_header( + HeaderName::from_static("traceparent"), + "00---01", + ); + request.insert_header(HeaderName::from_static("tracestate"), "="); + } } impl AsAny for MockSpan { @@ -546,6 +558,11 @@ mod tests { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); assert_eq!(req.method(), &Method::Get); + assert_eq!( + req.headers() + .get_optional_str(HeaderName::from_static("traceparent")), + Some("00---01") + ); Ok(RawResponse::from_bytes( StatusCode::Ok, Headers::new(), diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index 674d4b1615..59a177fcf0 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -18,9 +18,12 @@ edition.workspace = true azure_core.workspace = true opentelemetry = { version = "0.30", features = ["trace"] } opentelemetry-http = "0.30.0" +opentelemetry_sdk = "0.30" +reqwest.workspace = true tracing.workspace = true typespec_client_core.workspace = true + [dev-dependencies] azure_core_test = { workspace = true, features = ["tracing"] } azure_core_test_macros.workspace = true diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index b3abef4118..728f56f011 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -8,7 +8,10 @@ use azure_core::{ tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}, Result, }; -use opentelemetry::trace::TraceContextExt; +use opentelemetry::{propagation::TextMapPropagator, trace::TraceContextExt}; +use opentelemetry_http::HeaderInjector; +use opentelemetry_sdk::propagation::TraceContextPropagator; +use reqwest::header::HeaderMap; use std::{error::Error as StdError, sync::Arc}; /// newtype for Azure Core SpanKind to enable conversion to OpenTelemetry SpanKind @@ -76,7 +79,34 @@ impl Span for OpenTelemetrySpan { self.context.span().set_status(otel_status); } - fn propagate_headers(&self, _request: &mut azure_core::http::Request) {} + fn propagate_headers(&self, request: &mut azure_core::http::Request) { + // A TraceContextPropagator is used to inject trace context information into HTTP headers. + let trace_propagator = TraceContextPropagator::new(); + // We need to map between a reqwest header map (which is what the OpenTelemetry SDK requires) + // and the Azure Core request headers. + // + // We start with an empty header map and inject the OpenTelemetry headers into it. + let mut header_map = HeaderMap::new(); + trace_propagator.inject_context(&self.context, &mut HeaderInjector(&mut header_map)); + + // We then insert each of the headers from the OpenTelemetry header map into the + // Request's header map. + for (key, value) in header_map.into_iter() { + // Note: The OpenTelemetry HeaderInjector will always produce unique header names, so we don't need to + // handle the multiple headers case here. + if let Some(key) = key { + // Convert HeaderName to &str for insertion. + let value_str = value.to_str().unwrap().to_string(); + request.insert_header( + azure_core::http::headers::HeaderName::from(key.to_string()), + azure_core::http::headers::HeaderValue::from(value_str), + ); + } else { + // If the key is invalid, we skip it + tracing::warn!("Invalid header key: {:?}", key); + } + } + } fn set_current( &self, @@ -117,7 +147,7 @@ impl Drop for OpenTelemetrySpanGuard { #[cfg(test)] mod tests { use crate::telemetry::OpenTelemetryTracerProvider; - use azure_core::http::Context as AzureContext; + use azure_core::http::{Context as AzureContext, Url}; use azure_core::tracing::{Attribute, AttributeValue, SpanKind, SpanStatus, TracerProvider}; use opentelemetry::trace::TraceContextExt; use opentelemetry::{Context, Key, KeyValue, Value}; @@ -158,6 +188,34 @@ mod tests { } } + // cspell: ignore traceparent tracestate + #[test] + fn test_open_telemetry_span_propagate() { + let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); + + let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); + assert!(tracer_provider.is_ok()); + let tracer = + tracer_provider + .unwrap() + .get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); + let span = tracer.start_span("test_span", SpanKind::Client, vec![]); + let mut request = azure_core::http::Request::new( + Url::parse("http://example.com").unwrap(), + azure_core::http::Method::Get, + ); + span.propagate_headers(&mut request); + trace!("Request headers after propagation: {:?}", request.headers()); + let traceparent = azure_core::http::headers::HeaderName::from("traceparent"); + let tracestate = azure_core::http::headers::HeaderName::from("tracestate"); + request.headers().get_as::(&traceparent).unwrap(); + request.headers().get_as::(&tracestate).unwrap(); + span.end(); + + let finished_spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(finished_spans.len(), 1); + } + #[test] fn test_open_telemetry_span_hierarchy() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); From d7af2bb9ae2680f0721bff0e05198498c831aeb0 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 15:33:11 -0700 Subject: [PATCH 35/84] More documentation --- .../http/policies/request_instrumentation.rs | 2 +- sdk/core/azure_core_opentelemetry/src/span.rs | 8 ++----- .../typespec_client_core/src/tracing/mod.rs | 21 ++++++++++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index c27cd2e88c..cd42881eb9 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -560,7 +560,7 @@ mod tests { assert_eq!(req.method(), &Method::Get); assert_eq!( req.headers() - .get_optional_str(HeaderName::from_static("traceparent")), + .get_optional_str(&HeaderName::from_static("traceparent")), Some("00---01") ); Ok(RawResponse::from_bytes( diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 728f56f011..beca6068d5 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -4,10 +4,7 @@ //! OpenTelemetry implementation of typespec_client_core tracing traits. use crate::attributes::AttributeValue as ConversionAttributeValue; -use azure_core::{ - tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}, - Result, -}; +use azure_core::tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}; use opentelemetry::{propagation::TextMapPropagator, trace::TraceContextExt}; use opentelemetry_http::HeaderInjector; use opentelemetry_sdk::propagation::TraceContextPropagator; @@ -132,9 +129,8 @@ struct OpenTelemetrySpanGuard { } impl SpanGuard for OpenTelemetrySpanGuard { - fn end(self) -> Result<()> { + fn end(self) { // The span is ended when the guard is dropped, so no action needed here. - Ok(()) } } diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index bbb202dcd4..c6de90a87f 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -136,9 +136,13 @@ pub enum SpanKind { pub trait SpanGuard { /// Ends the span when dropped. - fn end(self) -> crate::Result<()>; + fn end(self); } +/// A trait that represents a span in distributed tracing. +/// +/// This trait defines the methods that a span must implement to be used in distributed tracing. +/// It includes methods for setting attributes, recording errors, and managing the span's lifecycle. pub trait Span: AsAny + Send + Sync { fn is_recording(&self) -> bool; @@ -158,6 +162,11 @@ pub trait Span: AsAny + Send + Sync { fn set_status(&self, status: SpanStatus); /// Sets an attribute on the current span. + /// + /// # Arguments + /// - `key`: The key of the attribute to set. + /// - `value`: The value of the attribute to set. + /// fn set_attribute(&self, key: &'static str, value: attributes::AttributeValue); /// Records a Rust standard error on the current span. @@ -171,6 +180,7 @@ pub trait Span: AsAny + Send + Sync { fn record_error(&self, error: &dyn std::error::Error); /// Temporarily sets the span as the current active span in the context. + /// /// # Arguments /// - `context`: The context in which to set the current span. /// @@ -182,6 +192,15 @@ pub trait Span: AsAny + Send + Sync { /// fn set_current(&self, context: &Context) -> crate::Result>; + /// Adds telemetry headers to the request for distributed tracing. + /// + /// # Arguments + /// - `request`: A mutable reference to the request to which headers will be added. + /// + /// This method should be called before sending the request to ensure that the tracing information + /// is included in the request headers. It typically adds the [W3C Distributed Tracing](https://www.w3.org/TR/trace-context/) + /// headers to the request. + /// fn propagate_headers(&self, request: &mut Request); } From c965604007451765cbcf6d5ec60f56735895e340 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 15:56:09 -0700 Subject: [PATCH 36/84] Small cleanup on propagate_headers --- sdk/core/azure_core_opentelemetry/src/span.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index beca6068d5..b7b4aa3b3d 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -4,11 +4,13 @@ //! OpenTelemetry implementation of typespec_client_core tracing traits. use crate::attributes::AttributeValue as ConversionAttributeValue; -use azure_core::tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}; +use azure_core::{ + http::headers::{HeaderName, HeaderValue}, + tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus}, +}; use opentelemetry::{propagation::TextMapPropagator, trace::TraceContextExt}; use opentelemetry_http::HeaderInjector; use opentelemetry_sdk::propagation::TraceContextPropagator; -use reqwest::header::HeaderMap; use std::{error::Error as StdError, sync::Arc}; /// newtype for Azure Core SpanKind to enable conversion to OpenTelemetry SpanKind @@ -83,7 +85,7 @@ impl Span for OpenTelemetrySpan { // and the Azure Core request headers. // // We start with an empty header map and inject the OpenTelemetry headers into it. - let mut header_map = HeaderMap::new(); + let mut header_map = reqwest::header::HeaderMap::new(); trace_propagator.inject_context(&self.context, &mut HeaderInjector(&mut header_map)); // We then insert each of the headers from the OpenTelemetry header map into the @@ -92,11 +94,9 @@ impl Span for OpenTelemetrySpan { // Note: The OpenTelemetry HeaderInjector will always produce unique header names, so we don't need to // handle the multiple headers case here. if let Some(key) = key { - // Convert HeaderName to &str for insertion. - let value_str = value.to_str().unwrap().to_string(); request.insert_header( - azure_core::http::headers::HeaderName::from(key.to_string()), - azure_core::http::headers::HeaderValue::from(value_str), + HeaderName::from(key.as_str().to_owned()), + HeaderValue::from(value.to_str().unwrap().to_owned()), ); } else { // If the key is invalid, we skip it From 9a4956c2abb195abdbffe5510c6bc946f891967b Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 30 Jun 2025 16:58:37 -0700 Subject: [PATCH 37/84] Started filling out OpenTelemetry readme.md --- Cargo.lock | 1 + sdk/core/azure_core_opentelemetry/Cargo.toml | 1 + sdk/core/azure_core_opentelemetry/README.md | 43 ++++++++++++++++++- sdk/core/azure_core_opentelemetry/src/lib.rs | 3 ++ sdk/core/azure_core_opentelemetry/src/span.rs | 3 ++ .../azure_core_opentelemetry/src/telemetry.rs | 4 +- .../tests/telemetry_service_implementation.rs | 8 ++-- 7 files changed, 56 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c31e5e0511..4fd711e205 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,7 @@ dependencies = [ "azure_core", "azure_core_test", "azure_core_test_macros", + "azure_identity", "opentelemetry 0.30.0", "opentelemetry-http", "opentelemetry_sdk 0.30.0", diff --git a/sdk/core/azure_core_opentelemetry/Cargo.toml b/sdk/core/azure_core_opentelemetry/Cargo.toml index 59a177fcf0..729bfdb6fd 100644 --- a/sdk/core/azure_core_opentelemetry/Cargo.toml +++ b/sdk/core/azure_core_opentelemetry/Cargo.toml @@ -27,6 +27,7 @@ typespec_client_core.workspace = true [dev-dependencies] azure_core_test = { workspace = true, features = ["tracing"] } azure_core_test_macros.workspace = true +azure_identity.workspace = true opentelemetry_sdk = { version = "0.30", features = ["testing"] } tokio.workspace = true tracing-subscriber.workspace = true diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index 10f0460e09..8c3deaee7c 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -1,3 +1,44 @@ # Azure Core OpenTelemetry Tracing -This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. It enables automatic span creation, context propagation, and telemetry collection for Azure service operations. +This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. + +It allows Rust applications which use the [OpenTelemetry](https://opentelemetry.io/) APIs to generated OpenTelemetry spans for Azure SDK for Rust Clients. + +It implements the [Rust OpenTelemetry](https://opentelemetry.io/docs/languages/rust/) APIs for the Azure SDK distributed tracing traits. + +## OpenTelemetry integration with the Azure SDK for Rust + +To integrate the OpenTelemetry APIs with the Azure SDK for Rust, you create a [`OpenTelemetryTracerProvider`] and pass it into your SDK ClientOptions. + +```rust no_run +# use azure_identity::DefaultAzureCredential; +# use azure_core::{http::{ClientOptions, RequestInstrumentationOptions}}; +# #[derive(Default)] +# struct ServiceClientOptions { +# azure_client_options: ClientOptions, +# } +use azure_core_opentelemetry::OpenTelemetryTracerProvider; +use opentelemetry_sdk::trace::SdkTracerProvider; +use std::sync::Arc; + +# fn test_fn() -> azure_core::Result<()> { +// Create an OpenTelemetry tracer provider adapter from an OpenTelemetry TracerProvider +let otel_tracer_provider = Arc::new(SdkTracerProvider::builder().build()); + +let azure_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider)?; + +let options = ServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + +# Ok(()) +# } +``` + +Once the `OpenTelemetryTracerProvider` is integrated with the Azure Service ClientOptions, the Azure SDK will be configured to capture per-API and per-HTTP operation tracing options, and the HTTP requests will be annotated with [W3C Trace Context headers](https://www.w3.org/TR/trace-context/). diff --git a/sdk/core/azure_core_opentelemetry/src/lib.rs b/sdk/core/azure_core_opentelemetry/src/lib.rs index a192707bd3..4f25c7ef33 100644 --- a/sdk/core/azure_core_opentelemetry/src/lib.rs +++ b/sdk/core/azure_core_opentelemetry/src/lib.rs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + //! Azure Core OpenTelemetry tracing integration. //! //! This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index b7b4aa3b3d..2bffc32968 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -93,9 +93,12 @@ impl Span for OpenTelemetrySpan { for (key, value) in header_map.into_iter() { // Note: The OpenTelemetry HeaderInjector will always produce unique header names, so we don't need to // handle the multiple headers case here. + if let Some(key) = key { request.insert_header( HeaderName::from(key.as_str().to_owned()), + // The value is guaranteed to be a valid UTF-8 string by the OpenTelemetry SDK, + // so we can safely unwrap it. HeaderValue::from(value.to_str().unwrap().to_owned()), ); } else { diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index 36dec069c9..d347413d83 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -18,8 +18,8 @@ pub struct OpenTelemetryTracerProvider { impl OpenTelemetryTracerProvider { /// Creates a new Azure telemetry provider with the given SDK tracer provider. #[allow(dead_code)] - pub fn new(provider: Arc) -> Result { - Ok(Self { inner: provider }) + pub fn new(provider: Arc) -> Result> { + Ok(Arc::new(Self { inner: provider })) } } diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index edd8e85128..7ed16566a2 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -314,7 +314,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -376,7 +376,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -438,7 +438,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -507,7 +507,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = Arc::new(OpenTelemetryTracerProvider::new(sdk_provider)?); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; let recording = ctx.recording(); let endpoint = "https://example.com"; From ee192ba0842ab6081550153993ff52c8b3f47bfe Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 12:03:31 -0700 Subject: [PATCH 38/84] PR feedback from Liudmila --- .../http/policies/request_instrumentation.rs | 30 ++++++++++++++----- .../tests/telemetry_service_implementation.rs | 16 +++++----- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index cd42881eb9..4e217e226f 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -12,15 +12,15 @@ use typespec_client_core::{ }; const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; -const AZ_SCHEMA_URL_ATTRIBUTE: &str = "az.schema.url"; const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client.request.id"; const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; -const AZ_SERVICE_REQUEST_ID_ATTRIBUTE: &str = "az.service.request.id"; -const HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE: &str = "http.request.resend.count"; +const AZ_SERVICE_REQUEST_ID_ATTRIBUTE: &str = "az.service_request.id"; +const HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE: &str = "http.request.resend_count"; const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE: &str = "http.response.status_code"; const HTTP_REQUEST_METHOD_ATTRIBUTE: &str = "http.request.method"; const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; const SERVER_PORT_ATTRIBUTE: &str = "server.port"; +const URL_SCHEME_ATTRIBUTE: &str = "url.scheme"; const URL_FULL_ATTRIBUTE: &str = "url.full"; /// Sets distributed tracing information for HTTP requests. @@ -88,6 +88,15 @@ impl Policy for RequestInstrumentationPolicy { self.tracer.as_ref() }; + // If there is a span in the context, if it's not recording, just forward the request + // without instrumentation. + if let Some(span) = ctx.value::>() { + if !span.is_recording() { + // If the span is not recording, we skip instrumentation. + return next[0].send(ctx, request, &next[1..]).await; + } + } + if let Some(tracer) = tracer { let mut span_attributes = vec![ Attribute { @@ -95,7 +104,7 @@ impl Policy for RequestInstrumentationPolicy { value: request.method().to_string().into(), }, Attribute { - key: AZ_SCHEMA_URL_ATTRIBUTE, + key: URL_SCHEME_ATTRIBUTE, value: request.url().scheme().into(), }, ]; @@ -171,6 +180,11 @@ impl Policy for RequestInstrumentationPolicy { tracer.start_span_with_current(method_str, SpanKind::Client, span_attributes) }; + if !span.is_recording() { + // If the span is not recording, we skip instrumentation. + return next[0].send(ctx, request, &next[1..]).await; + } + if let Some(client_request_id) = request .headers() .get_optional_str(&headers::CLIENT_REQUEST_ID) @@ -524,7 +538,7 @@ mod tests { AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("test namespace"), ), - (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("http")), + (URL_SCHEME_ATTRIBUTE, AttributeValue::from("http")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, AttributeValue::from(200), @@ -581,7 +595,7 @@ mod tests { "GET", SpanStatus::Unset, vec![ - (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + (URL_SCHEME_ATTRIBUTE, AttributeValue::from("https")), ( AZ_CLIENT_REQUEST_ID_ATTRIBUTE, AttributeValue::from("test-client-request-id"), @@ -631,7 +645,7 @@ mod tests { "GET", SpanStatus::Unset, vec![ - (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + (URL_SCHEME_ATTRIBUTE, AttributeValue::from("https")), ( HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, AttributeValue::from(200), @@ -691,7 +705,7 @@ mod tests { AZ_NAMESPACE_ATTRIBUTE, AttributeValue::from("test namespace"), ), - (AZ_SCHEMA_URL_ATTRIBUTE, AttributeValue::from("https")), + (URL_SCHEME_ATTRIBUTE, AttributeValue::from("https")), ( AZ_SERVICE_REQUEST_ID_ATTRIBUTE, AttributeValue::from("test-service-request-id"), diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 7ed16566a2..d2fa3534c3 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -350,7 +350,7 @@ mod tests { parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("az.schema.url", "https".into()), + ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -363,7 +363,7 @@ mod tests { ), ("server.address", "example.com".into()), ("server.port", 443.into()), - ("http.request.resend.count", 0.into()), + ("http.request.resend_count", 0.into()), ("http.response.status_code", 200.into()), ], }, @@ -411,7 +411,7 @@ mod tests { }, attributes: vec![ ("http.request.method", "GET".into()), - ("az.schema.url", "https".into()), + ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -425,7 +425,7 @@ mod tests { ("server.address", "example.com".into()), ("server.port", 443.into()), ("error.type", "404".into()), - ("http.request.resend.count", 0.into()), + ("http.request.resend_count", 0.into()), ("http.response.status_code", 404.into()), ], }, @@ -472,7 +472,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.schema.url", "https".into()), + ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -485,7 +485,7 @@ mod tests { ), ("server.address", "example.com".into()), ("server.port", 443.into()), - ("http.request.resend.count", 0.into()), + ("http.request.resend_count", 0.into()), ("http.response.status_code", 200.into()), ], }, @@ -543,7 +543,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.schema.url", "https".into()), + ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -556,7 +556,7 @@ mod tests { ), ("server.address", "example.com".into()), ("server.port", 443.into()), - ("http.request.resend.count", 0.into()), + ("http.request.resend_count", 0.into()), ("http.response.status_code", 404.into()), ("error.type", "404".into()), ], From 25789b11126e76dd8b24bc00f99b2fd678bf58ac Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 12:14:31 -0700 Subject: [PATCH 39/84] Cargo fmt fixes --- sdk/core/azure_core/src/http/pipeline.rs | 3 +-- sdk/typespec/typespec_client_core/src/http/request/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sdk/core/azure_core/src/http/pipeline.rs b/sdk/core/azure_core/src/http/pipeline.rs index 7358e5d65b..c53dc02b81 100644 --- a/sdk/core/azure_core/src/http/pipeline.rs +++ b/sdk/core/azure_core/src/http/pipeline.rs @@ -53,10 +53,9 @@ impl Pipeline { let (core_client_options, options) = options.deconstruct(); let user_agent_policy = - UserAgentPolicy::new(crate_name, crate_version, &core_client_options.user_agent); + UserAgentPolicy::new(crate_name, crate_version, &core_client_options.user_agent); push_unique(&mut per_call_policies, user_agent_policy); - let mut per_try_policies = per_try_policies.clone(); if core_client_options .request_instrumentation diff --git a/sdk/typespec/typespec_client_core/src/http/request/mod.rs b/sdk/typespec/typespec_client_core/src/http/request/mod.rs index ab2549d0e0..9f82e66564 100644 --- a/sdk/typespec/typespec_client_core/src/http/request/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/request/mod.rs @@ -160,8 +160,8 @@ impl Request { } } - /// Inserts zero or more headers from a type that implements [`AsHeaders`]. - pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { + /// Inserts zero or more headers from a type that implements [`AsHeaders`]. + pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { for (name, value) in headers.as_headers()? { self.insert_header(name, value); } From 154ce9a8f4346d70924844858e1f0e70f60df793 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 15:24:36 -0700 Subject: [PATCH 40/84] Removed unused public function --- .../typespec_client_core/src/http/request/mod.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/sdk/typespec/typespec_client_core/src/http/request/mod.rs b/sdk/typespec/typespec_client_core/src/http/request/mod.rs index 9f82e66564..dc38355600 100644 --- a/sdk/typespec/typespec_client_core/src/http/request/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/request/mod.rs @@ -145,21 +145,6 @@ impl Request { &self.method } - /// Returns the HTTP method as a static string. - /// - /// This is not generally useful and should be avoided in favor of using the ['Request::method()'] method. - #[doc(hidden)] - pub fn method_as_str(&self) -> &'static str { - match self.method { - Method::Delete => "DELETE", - Method::Get => "GET", - Method::Head => "HEAD", - Method::Patch => "PATCH", - Method::Post => "POST", - Method::Put => "PUT", - } - } - /// Inserts zero or more headers from a type that implements [`AsHeaders`]. pub fn insert_headers(&mut self, headers: &T) -> Result<(), T::Error> { for (name, value) in headers.as_headers()? { From 342b28d3c297a61c8407870cf8d39d790a5075c4 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 16:12:06 -0700 Subject: [PATCH 41/84] Added the ability to configure an OpenTelemetryProvider on the global OpenTelemetry provider --- .../http/policies/request_instrumentation.rs | 3 +- sdk/core/azure_core_opentelemetry/README.md | 33 ++++++++- sdk/core/azure_core_opentelemetry/src/span.rs | 62 ++++------------- .../azure_core_opentelemetry/src/telemetry.rs | 69 +++++++++++++++---- .../azure_core_opentelemetry/src/tracer.rs | 6 +- .../tests/integration_test.rs | 6 +- .../tests/telemetry_service_implementation.rs | 8 +-- .../typespec_client_core/src/tracing/mod.rs | 8 +-- .../src/tracing/with_context.rs | 4 +- 9 files changed, 119 insertions(+), 80 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 4e217e226f..8cb22c03a4 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -399,8 +399,7 @@ mod tests { fn set_current( &self, _context: &Context, - ) -> typespec_client_core::Result> - { + ) -> Box { todo!() } diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index 8c3deaee7c..2db3f51aa9 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -25,7 +25,38 @@ use std::sync::Arc; // Create an OpenTelemetry tracer provider adapter from an OpenTelemetry TracerProvider let otel_tracer_provider = Arc::new(SdkTracerProvider::builder().build()); -let azure_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider)?; +let azure_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); + +let options = ServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + +# Ok(()) +# } +``` + +If it is more convenient to use the global OpenTelemetry provider, then the [`OpenTelemetryTracerProvider::new_from_global_provider`] method will configure the OpenTelemetry support to use the global provider instead of a custom configured provider. + +```rust no_run +# use azure_identity::DefaultAzureCredential; +# use azure_core::{http::{ClientOptions, RequestInstrumentationOptions}}; +# #[derive(Default)] +# struct ServiceClientOptions { +# azure_client_options: ClientOptions, +# } +use azure_core_opentelemetry::OpenTelemetryTracerProvider; +use opentelemetry_sdk::trace::SdkTracerProvider; +use std::sync::Arc; + +# fn test_fn() -> azure_core::Result<()> { + +let azure_provider = OpenTelemetryTracerProvider::new_from_global_provider(); let options = ServiceClientOptions { azure_client_options: ClientOptions { diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 2bffc32968..a864f615f6 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -108,16 +108,13 @@ impl Span for OpenTelemetrySpan { } } - fn set_current( - &self, - _context: &azure_core::http::Context, - ) -> typespec_client_core::Result> { + fn set_current(&self, _context: &azure_core::http::Context) -> Box { // Create a context with the current span let context_guard = self.context.clone().attach(); - Ok(Box::new(OpenTelemetrySpanGuard { + Box::new(OpenTelemetrySpanGuard { _inner: context_guard, - })) + }) } } @@ -169,11 +166,7 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = - tracer_provider - .unwrap() - .get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); span.end(); @@ -193,11 +186,7 @@ mod tests { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = - tracer_provider - .unwrap() - .get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let mut request = azure_core::http::Request::new( Url::parse("http://example.com").unwrap(), @@ -219,10 +208,7 @@ mod tests { fn test_open_telemetry_span_hierarchy() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("Special Name"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Special Name"), "test", "0.1.0"); let parent_span = tracer.start_span("parent_span", SpanKind::Server, vec![]); let child_span = tracer.start_span_with_parent( "child_span", @@ -253,10 +239,7 @@ mod tests { fn test_open_telemetry_span_start_with_parent() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("MyNamespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("MyNamespace"), "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let child_span = @@ -285,15 +268,10 @@ mod tests { fn test_open_telemetry_span_start_with_current() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("Namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Namespace"), "test", "0.1.0"); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); - let _span_guard = span2 - .set_current(&azure_core::http::Context::new()) - .unwrap(); + let _span_guard = span2.set_current(&azure_core::http::Context::new()); let child_span = tracer.start_span_with_current("child_span", SpanKind::Client, vec![]); child_span.end(); @@ -319,10 +297,7 @@ mod tests { fn test_open_telemetry_span_set_attribute() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("ThisNamespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("ThisNamespace"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.set_attribute("test_key", AttributeValue::String("test_value".to_string())); @@ -345,10 +320,7 @@ mod tests { fn test_open_telemetry_span_record_error() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("namespace"), "test", "0.1.0"); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let error = Error::new(ErrorKind::NotFound, "resource not found"); @@ -374,10 +346,7 @@ mod tests { fn test_open_telemetry_span_set_status() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("Namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Namespace"), "test", "0.1.0"); // Test Unset status let span = tracer.start_span("test_span_unset", SpanKind::Server, vec![]); @@ -406,10 +375,7 @@ mod tests { async fn test_open_telemetry_span_futures() { let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider(); let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); - assert!(tracer_provider.is_ok()); - let tracer = tracer_provider - .unwrap() - .get_tracer(Some("Namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Namespace"), "test", "0.1.0"); let future = async { let context = Context::current(); @@ -435,7 +401,7 @@ mod tests { let azure_context = AzureContext::new(); let azure_context = azure_context.with_value(span.clone()); - let _guard = span.set_current(&azure_context).unwrap(); + let _guard = span.set_current(&azure_context); let result = future.await; diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index d347413d83..12127a43ee 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -3,7 +3,6 @@ use crate::tracer::OpenTelemetryTracer; use azure_core::tracing::TracerProvider; -use azure_core::Result; use opentelemetry::{ global::{BoxedTracer, ObjectSafeTracerProvider}, InstrumentationScope, @@ -12,14 +11,36 @@ use std::sync::Arc; /// Enum to hold different OpenTelemetry tracer provider implementations. pub struct OpenTelemetryTracerProvider { - inner: Arc, + inner: Option>, } impl OpenTelemetryTracerProvider { /// Creates a new Azure telemetry provider with the given SDK tracer provider. - #[allow(dead_code)] - pub fn new(provider: Arc) -> Result> { - Ok(Arc::new(Self { inner: provider })) + /// + /// # Arguments + /// - `provider`: An `Arc` to an object-safe tracer provider that implements the + /// `ObjectSafeTracerProvider` trait. + /// + /// # Returns + /// An `Arc` to the newly created `OpenTelemetryTracerProvider`. + /// + /// + pub fn new(provider: Arc) -> Arc { + Arc::new(Self { + inner: Some(provider), + }) + } + + /// Creates a new Azure telemetry provider that uses the global OpenTelemetry tracer provider. + /// + /// This is useful when you want to use the global OpenTelemetry provider without + /// explicitly instantiating a specific provider. + /// + /// # Returns + /// An `Arc` to the newly created `OpenTelemetryTracerProvider` that uses the global provider. + /// + pub fn new_from_global_provider() -> Arc { + Arc::new(Self { inner: None }) } } @@ -34,10 +55,19 @@ impl TracerProvider for OpenTelemetryTracerProvider { .with_version(package_version) .with_schema_url("https://opentelemetry.io/schemas/1.23.0") .build(); - Arc::new(OpenTelemetryTracer::new( - namespace, - BoxedTracer::new(self.inner.boxed_tracer(scope)), - )) + if let Some(provider) = &self.inner { + // If we have a specific provider set, use it to create the tracer. + Arc::new(OpenTelemetryTracer::new( + namespace, + BoxedTracer::new(provider.boxed_tracer(scope)), + )) + } else { + // Use the global tracer if no specific provider has been set. + Arc::new(OpenTelemetryTracer::new( + namespace, + opentelemetry::global::tracer_with_scope(scope), + )) + } } } @@ -50,14 +80,27 @@ mod tests { #[test] fn test_create_tracer_provider_sdk_tracer() { let provider = Arc::new(SdkTracerProvider::builder().build()); - let tracer_provider = OpenTelemetryTracerProvider::new(provider); - assert!(tracer_provider.is_ok()); + let _tracer_provider = OpenTelemetryTracerProvider::new(provider); } #[test] fn test_create_tracer_provider_noop_tracer() { let provider = Arc::new(NoopTracerProvider::new()); - let tracer_provider = OpenTelemetryTracerProvider::new(provider); - assert!(tracer_provider.is_ok()); + let _tracer_provider = OpenTelemetryTracerProvider::new(provider); + } + + #[test] + fn test_create_tracer_provider_from_global() { + let tracer_provider = OpenTelemetryTracerProvider::new_from_global_provider(); + let _tracer = tracer_provider.get_tracer(Some("My Namespace"), "test", "0.1.0"); + } + + #[test] + fn test_create_tracer_provider_from_global_provider_set() { + let provider = SdkTracerProvider::builder().build(); + opentelemetry::global::set_tracer_provider(provider); + + let tracer_provider = OpenTelemetryTracerProvider::new_from_global_provider(); + let _tracer = tracer_provider.get_tracer(Some("My Namespace"), "test", "0.1.0"); } } diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 6282f5cdb5..9051643534 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -111,7 +111,7 @@ mod tests { #[test] fn test_create_tracer() { let noop_tracer = NoopTracerProvider::new(); - let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)).unwrap(); + let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)); let tracer = otel_provider.get_tracer(Some("name"), "test_tracer", "1.0.0"); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); @@ -120,14 +120,14 @@ mod tests { #[test] fn test_create_tracer_with_sdk_tracer() { let provider = SdkTracerProvider::builder().build(); - let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); + let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)); let _tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); } #[test] fn test_create_span_from_tracer() { let provider = SdkTracerProvider::builder().build(); - let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)).unwrap(); + let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)); let tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); let _span = tracer.start_span("test_span", SpanKind::Internal, vec![]); } diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs index e6165d45a0..bd8911fcaa 100644 --- a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs +++ b/sdk/core/azure_core_opentelemetry/tests/integration_test.rs @@ -11,7 +11,7 @@ use std::sync::Arc; async fn test_span_creation() -> Result<(), Box> { // Set up a tracer provider for testing let sdk_provider = Arc::new(SdkTracerProvider::builder().build()); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); // Get a tracer from the Azure provider let tracer = azure_provider.get_tracer(Some("test_namespace"), "test_tracer", "1.0.0"); @@ -39,7 +39,7 @@ async fn test_span_creation() -> Result<(), Box> { async fn test_tracer_provider_creation() -> Result<(), Box> { // Create multiple tracer provider instances to test initialization let sdk_provider = Arc::new(SdkTracerProvider::builder().build()); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); // Get a tracer and verify it works let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); @@ -53,7 +53,7 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { async fn test_span_attributes() -> Result<(), Box> { // Set up a tracer provider for testing let sdk_provider = Arc::new(SdkTracerProvider::builder().build()); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); // Get a tracer from the Azure provider let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index d2fa3534c3..57e3b9aade 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -314,7 +314,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -376,7 +376,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -438,7 +438,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); let endpoint = "https://example.com"; @@ -507,7 +507,7 @@ mod tests { #[recorded::test()] async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider)?; + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); let endpoint = "https://example.com"; diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index c6de90a87f..b096b9ee27 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -55,7 +55,7 @@ pub trait Tracer: Send + Sync { /// - `kind`: The type of the span to start. /// /// # Returns - /// An `Arc` representing the started span. + /// An `Arc` representing the started span. /// fn start_span( &self, @@ -71,7 +71,7 @@ pub trait Tracer: Send + Sync { /// - `kind`: The type of the span to start. /// /// # Returns - /// An `Arc` representing the started span. + /// An `Arc` representing the started span. /// fn start_span_with_current( &self, @@ -88,7 +88,7 @@ pub trait Tracer: Send + Sync { /// - `parent`: The parent span to use for the new span. /// /// # Returns - /// An `Arc` representing the started span + /// An `Arc` representing the started span /// /// Note: This method may panic if the parent span cannot be downcasted to the expected type. /// @@ -190,7 +190,7 @@ pub trait Span: AsAny + Send + Sync { /// This method allows the span to be set as the current span in the context, /// enabling it to be used for tracing operations within that context. /// - fn set_current(&self, context: &Context) -> crate::Result>; + fn set_current(&self, context: &Context) -> Box; /// Adds telemetry headers to the request for distributed tracing. /// diff --git a/sdk/typespec/typespec_client_core/src/tracing/with_context.rs b/sdk/typespec/typespec_client_core/src/tracing/with_context.rs index 14ef2808bf..8dfcf050b6 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/with_context.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/with_context.rs @@ -20,8 +20,8 @@ impl std::future::Future for WithContext<'_, T> { fn poll(self: Pin<&mut Self>, task_cx: &mut TaskContext<'_>) -> Poll { let this = self.project(); - if let Some(span) = this.context.value::>() { - let _guard = span.set_current(this.context).unwrap(); + if let Some(span) = this.context.value::>() { + let _guard = span.set_current(this.context); this.inner.poll(task_cx) } else { From dd5866ed39247441922d69430e6de0a74586898e Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 1 Jul 2025 16:21:26 -0700 Subject: [PATCH 42/84] Documentation updates --- sdk/core/azure_core_opentelemetry/README.md | 6 +++--- sdk/core/azure_core_opentelemetry/src/lib.rs | 6 ------ .../typespec_client_core/src/tracing/mod.rs | 15 +++++++++++++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index 2db3f51aa9..a87a8db7f9 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -1,10 +1,10 @@ # Azure Core OpenTelemetry Tracing This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. +It bridges the standardized azure_core tracing traits with OpenTelemetry implementation, +enabling automatic span creation, context propagation, and telemetry collection for Azure services. -It allows Rust applications which use the [OpenTelemetry](https://opentelemetry.io/) APIs to generated OpenTelemetry spans for Azure SDK for Rust Clients. - -It implements the [Rust OpenTelemetry](https://opentelemetry.io/docs/languages/rust/) APIs for the Azure SDK distributed tracing traits. +It allows Rust applications which use the [OpenTelemetry](https://opentelemetry.io/) APIs to generate OpenTelemetry spans for Azure SDK for Rust Clients. ## OpenTelemetry integration with the Azure SDK for Rust diff --git a/sdk/core/azure_core_opentelemetry/src/lib.rs b/sdk/core/azure_core_opentelemetry/src/lib.rs index 4f25c7ef33..37bed17edb 100644 --- a/sdk/core/azure_core_opentelemetry/src/lib.rs +++ b/sdk/core/azure_core_opentelemetry/src/lib.rs @@ -4,12 +4,6 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -//! Azure Core OpenTelemetry tracing integration. -//! -//! This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. -//! It bridges the standardized typespec_client_core tracing traits with OpenTelemetry implementation, -//! enabling automatic span creation, context propagation, and telemetry collection for Azure services. - mod attributes; mod span; mod telemetry; diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index b096b9ee27..2b49bfd2b5 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -50,9 +50,12 @@ impl Debug for dyn TracerProvider { pub trait Tracer: Send + Sync { /// Starts a new span with the given name and type. /// + /// The newly created span will not have a parent span. + /// /// # Arguments /// - `name`: The name of the span to start. /// - `kind`: The type of the span to start. + /// - `attributes`: A vector of attributes to associate with the span. /// /// # Returns /// An `Arc` representing the started span. @@ -64,11 +67,15 @@ pub trait Tracer: Send + Sync { attributes: Vec, ) -> Arc; - /// Starts a new span with the given type, using the current span as the parent span. + /// Starts a new span with the given name and type. + /// + /// The parent span of the newly created span will be the current span (if one + /// exists). /// /// # Arguments /// - `name`: The name of the span to start. /// - `kind`: The type of the span to start. + /// - `attributes`: A vector of attributes to associate with the span. /// /// # Returns /// An `Arc` representing the started span. @@ -85,6 +92,7 @@ pub trait Tracer: Send + Sync { /// # Arguments /// - `name`: The name of the span to start. /// - `kind`: The type of the span to start. + /// - `attributes`: A vector of attributes to associate with the span. /// - `parent`: The parent span to use for the new span. /// /// # Returns @@ -100,7 +108,10 @@ pub trait Tracer: Send + Sync { parent: Arc, ) -> Arc; - /// Returns the namespace the tracer was configured with. + /// Returns the namespace the tracer was configured with (if any). + /// + /// # Returns + /// An `Option<&'static str>` representing the namespace of the tracer, fn namespace(&self) -> Option<&'static str>; } From 32ffbe750b4f50431dd7e5122b7a66a448777db5 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 9 Jul 2025 12:22:21 -0700 Subject: [PATCH 43/84] Created PublicApiInstrumentationPolicy to instrument public APIs --- Cargo.lock | 8 +- sdk/core/azure_core/src/http/pipeline.rs | 44 +- sdk/core/azure_core/src/http/policies/mod.rs | 5 +- .../policies/public_api_instrumentation.rs | 422 ++++++++++++++++++ .../http/policies/request_instrumentation.rs | 389 ++++++++-------- sdk/core/azure_core/src/lib.rs | 5 +- .../tests/telemetry_service_implementation.rs | 69 +-- .../src/tracing/attributes.rs | 11 +- .../typespec_client_core/src/tracing/mod.rs | 2 +- 9 files changed, 683 insertions(+), 272 deletions(-) create mode 100644 sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs diff --git a/Cargo.lock b/Cargo.lock index 4fd711e205..3a6f2f366a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -210,9 +210,9 @@ dependencies = [ "azure_core_test", "azure_core_test_macros", "azure_identity", - "opentelemetry 0.30.0", + "opentelemetry", "opentelemetry-http", - "opentelemetry_sdk 0.30.0", + "opentelemetry_sdk", "reqwest", "tokio", "tracing", @@ -1720,7 +1720,7 @@ dependencies = [ "async-trait", "bytes", "http", - "opentelemetry 0.30.0", + "opentelemetry", ] [[package]] diff --git a/sdk/core/azure_core/src/http/pipeline.rs b/sdk/core/azure_core/src/http/pipeline.rs index c53dc02b81..96a63510d6 100644 --- a/sdk/core/azure_core/src/http/pipeline.rs +++ b/sdk/core/azure_core/src/http/pipeline.rs @@ -3,7 +3,9 @@ use super::policies::ClientRequestIdPolicy; use crate::http::{ - policies::{Policy, RequestInstrumentationPolicy, UserAgentPolicy}, + policies::{ + Policy, PublicApiInstrumentationPolicy, RequestInstrumentationPolicy, UserAgentPolicy, + }, ClientOptions, }; use std::{ @@ -48,20 +50,41 @@ impl Pipeline { per_call_policies: Vec>, per_try_policies: Vec>, ) -> Self { + let (core_client_options, options) = options.deconstruct(); + + let install_instrumentation_policies = core_client_options + .request_instrumentation + .tracing_provider + .is_some(); + + let tracer = if install_instrumentation_policies { + core_client_options + .request_instrumentation + .tracing_provider + .as_ref() + .map(|tracing_provider| { + tracing_provider.get_tracer( + None, + crate_name.unwrap_or("unknown"), + crate_version.unwrap_or("unknown"), + ) + }) + } else { + None + }; let mut per_call_policies = per_call_policies.clone(); push_unique(&mut per_call_policies, ClientRequestIdPolicy::default()); + if install_instrumentation_policies { + let public_api_policy = PublicApiInstrumentationPolicy::new(tracer.clone()); + push_unique(&mut per_call_policies, public_api_policy); + } - let (core_client_options, options) = options.deconstruct(); let user_agent_policy = UserAgentPolicy::new(crate_name, crate_version, &core_client_options.user_agent); push_unique(&mut per_call_policies, user_agent_policy); let mut per_try_policies = per_try_policies.clone(); - if core_client_options - .request_instrumentation - .tracing_provider - .is_some() - { + if install_instrumentation_policies { // Note that the choice to use "None" as the namespace here // is intentional. // The `azure_namespace` parameter is used to populate the `az.namespace` @@ -74,12 +97,7 @@ impl Pipeline { // This information can only come from the package owner. It doesn't make sense // to burden all users of the azure_core pipeline with determining this // information, so we use `None` here. - let request_instrumentation_policy = RequestInstrumentationPolicy::new( - None, - crate_name, - crate_version, - &core_client_options.request_instrumentation, - ); + let request_instrumentation_policy = RequestInstrumentationPolicy::new(tracer); push_unique(&mut per_try_policies, request_instrumentation_policy); } diff --git a/sdk/core/azure_core/src/http/policies/mod.rs b/sdk/core/azure_core/src/http/policies/mod.rs index 2d07bf36f7..0d4439d285 100644 --- a/sdk/core/azure_core/src/http/policies/mod.rs +++ b/sdk/core/azure_core/src/http/policies/mod.rs @@ -5,11 +5,14 @@ mod bearer_token_policy; mod client_request_id; +mod public_api_instrumentation; mod request_instrumentation; mod user_agent; pub use bearer_token_policy::BearerTokenCredentialPolicy; pub use client_request_id::*; -pub use request_instrumentation::*; +pub use public_api_instrumentation::PublicApiInstrumentationInformation; +pub(crate) use public_api_instrumentation::PublicApiInstrumentationPolicy; +pub(crate) use request_instrumentation::*; pub use typespec_client_core::http::policies::*; pub use user_agent::*; diff --git a/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs new file mode 100644 index 0000000000..4caccf6d72 --- /dev/null +++ b/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs @@ -0,0 +1,422 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use crate::{ + http::{Context, Request}, + tracing::{Span, SpanKind, Tracer}, +}; +use std::sync::Arc; +use typespec_client_core::{ + http::policies::{Policy, PolicyResult}, + tracing::Attribute, +}; + +#[derive(Debug)] +pub struct PublicApiInstrumentationInformation { + pub api_name: &'static str, +} + +const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; +const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; + +/// Sets distributed tracing information for HTTP requests. +#[derive(Clone, Debug)] +pub(crate) struct PublicApiInstrumentationPolicy { + tracer: Option>, +} + +impl PublicApiInstrumentationPolicy { + /// Creates a new `PublicApiInstrumentationPolicy`. + /// + /// + /// # Returns + /// A new instance of `PublicApiInstrumentationPolicy`. + /// + /// # Note + /// This policy will only create a tracer if a tracing provider is provided in the options. + /// + /// This policy will create a tracer that can be used to instrument HTTP requests. + /// However this tracer is only used when the client method is NOT instrumented. + /// A part of the client method instrumentation sets a client-specific tracer into the + /// request `[Context]` which will be used instead of the tracer from this policy. + /// + pub fn new(tracer: Option>) -> Self { + Self { tracer } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Policy for PublicApiInstrumentationPolicy { + async fn send( + &self, + ctx: &Context, + request: &mut Request, + next: &[Arc], + ) -> PolicyResult { + // If there is a span in the context, we're a nested call, so we just want to forward the request. + if ctx.value::>().is_some() { + return next[0].send(ctx, request, &next[1..]).await; + } + + // We're not a nested call, so we can proceed with instrumentation. + // We first check if the context has public API instrumentation information. + // If it does, we use that information to create a span for the request. + // If it doesn't, we skip instrumentation. + let public_api_information = ctx.value::(); + let span: Option> = if let Some(info) = public_api_information { + // Use the public API information for instrumentation. + // If the context has a tracer (which happens when called from an instrumented method), + // we prefer the tracer from the context. + // Otherwise, we use the tracer from the policy itself. + // This allows for flexibility in using different tracers in different contexts. + let mut span_attributes = vec![]; + + if let Some(tracer) = ctx.value::>() { + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + span_attributes.push(Attribute { + key: AZ_NAMESPACE_ATTRIBUTE, + value: namespace.into(), + }); + } + // Create a span with the public API information. + Some(tracer.start_span_with_current( + info.api_name, + SpanKind::Internal, + span_attributes, + )) + } else if let Some(tracer) = &self.tracer { + // We didn't have a span from the context, but we do have a Tracer from the + // pipeline construction, so use that. + Some(tracer.start_span_with_current( + info.api_name, + SpanKind::Internal, + span_attributes, + )) + } else { + // If no tracer is available, we skip instrumentation. + None + } + } else { + None + }; + + // Now add the span to the context, so that it can be used by the next policies. + let mut ctx = ctx.clone(); + if let Some(span) = &span { + // If we have a span, we set it in the context. + ctx = ctx.with_value(span.clone()); + } + + let result = next[0].send(&ctx, request, &next[1..]).await; + + if let Some(span) = span { + if let Err(e) = &result { + // If the request failed, we set the error type on the span. + match e.kind() { + crate::error::ErrorKind::HttpResponse { status, .. } => { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, status.to_string().into()); + + // 5xx status codes SHOULD set status to Error. + // The description should not be set because it can be inferred from "http.response.status_code". + if status.is_server_error() { + span.set_status(crate::tracing::SpanStatus::Error { + description: "".to_string(), + }); + } + } + _ => { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, e.kind().to_string().into()); + span.set_status(crate::tracing::SpanStatus::Error { + description: e.kind().to_string(), + }); + } + } + } else if let Ok(response) = &result { + // 5xx status codes SHOULD set status to Error. + // The description should not be set because it can be inferred from "http.response.status_code". + if response.status().is_server_error() { + span.set_status(crate::tracing::SpanStatus::Error { + description: "".to_string(), + }); + } + if response.status().is_client_error() || response.status().is_server_error() { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); + } + } + + span.end(); + } + result + } +} + +#[cfg(test)] +mod tests { + // cspell: ignore traceparent + use super::super::request_instrumentation::tests::{ + check_request_instrumentation_result, InstrumentationExpectation, MockTracingProvider, + }; + use super::*; + use crate::{ + http::{ + headers::Headers, + policies::{RequestInstrumentationPolicy, TransportPolicy}, + Method, RawResponse, StatusCode, TransportOptions, + }, + tracing::{AttributeValue, SpanStatus, TracerProvider}, + Result, + }; + use azure_core_test::http::MockHttpClient; + use futures::future::BoxFuture; + use std::sync::Arc; + + // Test just the public API instrumentation policy without request instrumentation. + async fn run_public_api_instrumentation_test( + api_name: Option<&'static str>, + request: &mut Request, + callback: C, + ) -> Arc + where + C: FnMut(&Request) -> BoxFuture<'_, Result> + Send + Sync + 'static, + { + let public_api_information = PublicApiInstrumentationInformation { + api_name: api_name.unwrap_or("unknown"), + }; + // Add the public API information and tracer to the context so that it can be used by the policy. + let mock_tracer_provider = Arc::new(MockTracingProvider::new()); + let tracer = mock_tracer_provider.get_tracer(Some("test namespace"), "test_crate", "1.0.0"); + + let public_api_policy = Arc::new(PublicApiInstrumentationPolicy::new(Some(tracer.clone()))); + + let transport = TransportPolicy::new(TransportOptions::new(Arc::new(MockHttpClient::new( + callback, + )))); + + let next: Vec> = vec![Arc::new(transport)]; + let ctx = Context::default() + .with_value(public_api_information) + .with_value(tracer.clone()); + let _result = public_api_policy.send(&ctx, request, &next).await; + + mock_tracer_provider + } + + async fn run_public_api_instrumentation_test_with_request_instrumentation( + api_name: Option<&'static str>, + namespace: Option<&'static str>, + crate_name: Option<&'static str>, + version: Option<&'static str>, + request: &mut Request, + callback: C, + ) -> Arc + where + C: FnMut(&Request) -> BoxFuture<'_, Result> + Send + Sync + 'static, + { + let mock_tracer_provider = Arc::new(MockTracingProvider::new()); + let mock_tracer = mock_tracer_provider.get_tracer( + namespace, + crate_name.unwrap_or("unknown"), + version.unwrap_or("unknown"), + ); + + let public_api_policy = Arc::new(PublicApiInstrumentationPolicy::new(Some( + mock_tracer.clone(), + ))); + + let transport = TransportPolicy::new(TransportOptions::new(Arc::new(MockHttpClient::new( + callback, + )))); + + let request_instrumentation_policy = + RequestInstrumentationPolicy::new(Some(mock_tracer.clone())); + + let next: Vec> = vec![ + Arc::new(request_instrumentation_policy), + Arc::new(transport), + ]; + let public_api_information = PublicApiInstrumentationInformation { + api_name: api_name.unwrap_or("unknown"), + }; + + // Add the public API information and tracer to the context so that it can be used by the policy. + let ctx = Context::default() + .with_value(public_api_information) + .with_value(mock_tracer.clone()); + let _result = public_api_policy.send(&ctx, request, &next).await; + + mock_tracer_provider + } + + fn check_public_api_instrumentation_result( + mock_tracer: Arc, + span_count: usize, + span_index: usize, + expected_api_name: Option<&str>, + expected_kind: SpanKind, + expected_status: SpanStatus, + expected_attributes: Vec<(&str, AttributeValue)>, + ) { + assert_eq!( + mock_tracer.tracers.lock().unwrap().len(), + 1, + "Expected one tracer to be created", + ); + let tracers = mock_tracer.tracers.lock().unwrap(); + let tracer = tracers.first().unwrap(); + let spans = tracer.spans.lock().unwrap(); + assert_eq!(spans.len(), span_count, "Expected one span to be created"); + println!("Spans: {:?}", spans); + let span = spans[span_index].as_ref(); + assert_eq!(span.name, expected_api_name.unwrap_or("unknown")); + assert_eq!(span.kind, expected_kind); + assert_eq!(*span.state.lock().unwrap(), expected_status); + let attributes = span.attributes.lock().unwrap(); + for attr in attributes.iter() { + println!("Attribute: {} = {:?}", attr.key, attr.value); + let mut found = false; + for (key, value) in &expected_attributes { + if attr.key == *key { + assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); + found = true; + break; + } + } + if !found { + panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); + } + } + for (key, value) in &expected_attributes { + if !attributes + .iter() + .any(|attr| attr.key == *key && attr.value == *value) + { + panic!("Expected attribute not found: {} = {:?}", key, value); + } + } + } + + #[tokio::test] + async fn simple_public_api_instrumentation_policy() { + let url = "http://example.com/path"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer = + run_public_api_instrumentation_test(Some("MyClient.MyApi"), &mut request, |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }) + .await; + + check_public_api_instrumentation_result( + mock_tracer, + 1, + 0, + Some("MyClient.MyApi"), + SpanKind::Internal, + SpanStatus::Unset, + vec![(AZ_NAMESPACE_ATTRIBUTE, "test namespace".into())], + ); + } + + #[tokio::test] + async fn public_api_instrumentation_policy_with_error() { + let url = "http://example.com/path"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer = + run_public_api_instrumentation_test(Some("MyClient.MyApi"), &mut request, |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::InternalServerError, + Headers::new(), + vec![], + )) + }) + }) + .await; + + check_public_api_instrumentation_result( + mock_tracer, + 1, + 0, + Some("MyClient.MyApi"), + SpanKind::Internal, + SpanStatus::Error { + description: "".to_string(), + }, + vec![ + (AZ_NAMESPACE_ATTRIBUTE, "test namespace".into()), + (ERROR_TYPE_ATTRIBUTE, "500".into()), + ], + ); + } + + #[tokio::test] + async fn public_api_instrumentation_policy_with_request_instrumentation() { + let url = "http://example.com/path_with_request"; + let mut request = Request::new(url.parse().unwrap(), Method::Put); + + let mock_tracer = run_public_api_instrumentation_test_with_request_instrumentation( + Some("MyClient.MyApi"), + Some("test.namespace"), + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Put); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; + + check_public_api_instrumentation_result( + mock_tracer.clone(), + 2, + 0, + Some("MyClient.MyApi"), + SpanKind::Internal, + SpanStatus::Unset, + vec![(AZ_NAMESPACE_ATTRIBUTE, "test.namespace".into())], + ); + + check_request_instrumentation_result( + mock_tracer.clone(), + 2, + 1, + InstrumentationExpectation { + namespace: Some("test.namespace"), + name: "test_crate", + version: "1.0.0", + span_name: "PUT", + kind: SpanKind::Client, + status: SpanStatus::Unset, + attributes: vec![ + (AZ_NAMESPACE_ATTRIBUTE, "test.namespace".into()), + ("http.request.method", "PUT".into()), + ("url.full", "http://example.com/path_with_request".into()), + ("server.address", "example.com".into()), + ("server.port", 80.into()), + ("http.response.status_code", 200.into()), + ], + }, + ); + } +} diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 8cb22c03a4..10796aa277 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use crate::{ - http::{headers, options::RequestInstrumentationOptions, Context, Request}, + http::{headers, Context, Request}, tracing::{Span, SpanKind}, }; use std::sync::Arc; @@ -20,12 +20,11 @@ const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE: &str = "http.response.status_code"; const HTTP_REQUEST_METHOD_ATTRIBUTE: &str = "http.request.method"; const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; const SERVER_PORT_ATTRIBUTE: &str = "server.port"; -const URL_SCHEME_ATTRIBUTE: &str = "url.scheme"; const URL_FULL_ATTRIBUTE: &str = "url.full"; /// Sets distributed tracing information for HTTP requests. #[derive(Clone, Debug)] -pub struct RequestInstrumentationPolicy { +pub(crate) struct RequestInstrumentationPolicy { tracer: Option>, } @@ -50,22 +49,24 @@ impl RequestInstrumentationPolicy { /// request `[Context]` which will be used instead of the tracer from this policy. /// pub fn new( - azure_namespace: Option<&'static str>, - crate_name: Option<&'static str>, - crate_version: Option<&'static str>, - options: &RequestInstrumentationOptions, + tracer: Option>, + // azure_namespace: Option<&'static str>, + // crate_name: Option<&'static str>, + // crate_version: Option<&'static str>, + // options: &RequestInstrumentationOptions, ) -> Self { - if let Some(tracing_provider) = &options.tracing_provider { - Self { - tracer: Some(tracing_provider.get_tracer( - azure_namespace, - crate_name.unwrap_or("unknown"), - crate_version.unwrap_or("unknown"), - )), - } - } else { - Self { tracer: None } - } + Self { tracer } + // if let Some(tracing_provider) = &options.tracing_provider { + // Self { + // tracer: Some(tracing_provider.get_tracer( + // azure_namespace, + // crate_name.unwrap_or("unknown"), + // crate_version.unwrap_or("unknown"), + // )), + // } + // } else { + // Self { tracer: None } + // } } } @@ -98,16 +99,10 @@ impl Policy for RequestInstrumentationPolicy { } if let Some(tracer) = tracer { - let mut span_attributes = vec![ - Attribute { - key: HTTP_REQUEST_METHOD_ATTRIBUTE, - value: request.method().to_string().into(), - }, - Attribute { - key: URL_SCHEME_ATTRIBUTE, - value: request.url().scheme().into(), - }, - ]; + let mut span_attributes = vec![Attribute { + key: HTTP_REQUEST_METHOD_ATTRIBUTE, + value: request.method().to_string().into(), + }]; if let Some(namespace) = tracer.namespace() { // If the tracer has a namespace, we set it as an attribute. @@ -246,7 +241,7 @@ impl Policy for RequestInstrumentationPolicy { } } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::{ http::{ @@ -262,12 +257,12 @@ mod tests { use typespec_client_core::http::headers::HeaderName; #[derive(Debug)] - struct MockTracingProvider { - tracers: Mutex>>, + pub(crate) struct MockTracingProvider { + pub(crate) tracers: Mutex>>, } impl MockTracingProvider { - fn new() -> Self { + pub(crate) fn new() -> Self { Self { tracers: Mutex::new(Vec::new()), } @@ -283,8 +278,8 @@ mod tests { let mut tracers = self.tracers.lock().unwrap(); let tracer = Arc::new(MockTracer { namespace: azure_namespace, - name: crate_name, - version: crate_version, + package_name: crate_name, + package_version: crate_version, spans: Mutex::new(Vec::new()), }); @@ -294,11 +289,11 @@ mod tests { } #[derive(Debug)] - struct MockTracer { - namespace: Option<&'static str>, - name: &'static str, - version: &'static str, - spans: Mutex>>, + pub(crate) struct MockTracer { + pub(crate) namespace: Option<&'static str>, + pub(crate) package_name: &'static str, + pub(crate) package_version: &'static str, + pub(crate) spans: Mutex>>, } impl Tracer for MockTracer { @@ -342,14 +337,12 @@ mod tests { } #[derive(Debug)] - struct MockSpan { - name: String, - #[allow(dead_code)] - kind: SpanKind, - #[allow(dead_code)] - attributes: Mutex>, - state: Mutex, - is_open: Mutex, + pub(crate) struct MockSpan { + pub(crate) name: String, + pub(crate) kind: SpanKind, + pub(crate) attributes: Mutex>, + pub(crate) state: Mutex, + pub(crate) is_open: Mutex, } impl MockSpan { fn new(name: &str, kind: SpanKind, attributes: Vec) -> Self { @@ -430,16 +423,13 @@ mod tests { where C: FnMut(&Request) -> BoxFuture<'_, Result> + Send + Sync + 'static, { - let mock_tracer = Arc::new(MockTracingProvider::new()); - let options = RequestInstrumentationOptions { - tracing_provider: Some(mock_tracer.clone()), - }; - let policy = Arc::new(RequestInstrumentationPolicy::new( + let mock_tracer_provider = Arc::new(MockTracingProvider::new()); + let tracer = mock_tracer_provider.get_tracer( test_namespace, - crate_name, - version, - &options, - )); + crate_name.unwrap_or("unknown"), + version.unwrap_or("unknown"), + ); + let policy = Arc::new(RequestInstrumentationPolicy::new(Some(tracer.clone()))); let transport = TransportPolicy::new(TransportOptions::new(Arc::new(MockHttpClient::new( callback, @@ -449,16 +439,22 @@ mod tests { let next: Vec> = vec![Arc::new(transport)]; let _result = policy.send(&ctx, request, &next).await; - mock_tracer + mock_tracer_provider + } + pub(crate) struct InstrumentationExpectation<'a> { + pub(crate) namespace: Option<&'a str>, + pub(crate) name: &'a str, + pub(crate) version: &'a str, + pub(crate) span_name: &'a str, + pub(crate) status: SpanStatus, + pub(crate) kind: SpanKind, + pub(crate) attributes: Vec<(&'a str, AttributeValue)>, } - fn check_instrumentation_result( + pub(crate) fn check_request_instrumentation_result( mock_tracer: Arc, - expected_namespace: Option<&str>, - expected_name: &str, - expected_version: &str, - expected_method: &str, - expected_status: SpanStatus, - expected_attributes: Vec<(&str, AttributeValue)>, + expected_span_count: usize, + span_index: usize, + expectation: InstrumentationExpectation, ) { assert_eq!( mock_tracer.tracers.lock().unwrap().len(), @@ -467,20 +463,25 @@ mod tests { ); let tracers = mock_tracer.tracers.lock().unwrap(); let tracer = tracers.first().unwrap(); - assert_eq!(tracer.name, expected_name); - assert_eq!(tracer.version, expected_version); - assert_eq!(tracer.namespace, expected_namespace); + assert_eq!(tracer.package_name, expectation.name); + assert_eq!(tracer.package_version, expectation.version); + assert_eq!(tracer.namespace, expectation.namespace); let spans = tracer.spans.lock().unwrap(); - assert_eq!(spans.len(), 1, "Expected one span to be created"); + assert_eq!( + spans.len(), + expected_span_count, + "Expected one span to be created" + ); println!("Spans: {:?}", spans); - let span = spans.first().unwrap(); - assert_eq!(span.name, expected_method); - assert_eq!(*span.state.lock().unwrap(), expected_status); + let span = spans[span_index].as_ref(); + assert_eq!(span.name, expectation.span_name); + assert_eq!(span.kind, expectation.kind); + assert_eq!(*span.state.lock().unwrap(), expectation.status); let attributes = span.attributes.lock().unwrap(); for attr in attributes.iter() { println!("Attribute: {} = {:?}", attr.key, attr.value); let mut found = false; - for (key, value) in &expected_attributes { + for (key, value) in &expectation.attributes { if attr.key == *key { assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); found = true; @@ -491,7 +492,7 @@ mod tests { panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); } } - for (key, value) in &expected_attributes { + for (key, value) in &expectation.attributes { if !attributes .iter() .any(|attr| attr.key == *key && attr.value == *value) @@ -525,34 +526,38 @@ mod tests { ) .await; - check_instrumentation_result( + check_request_instrumentation_result( mock_tracer, - Some("test namespace"), - "test_crate", - "1.0.0", - "GET", - SpanStatus::Unset, - vec![ - ( - AZ_NAMESPACE_ATTRIBUTE, - AttributeValue::from("test namespace"), - ), - (URL_SCHEME_ATTRIBUTE, AttributeValue::from("http")), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(200), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), - ( - SERVER_ADDRESS_ATTRIBUTE, - AttributeValue::from("example.com"), - ), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(80)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("http://example.com/path"), - ), - ], + 1, + 0, + InstrumentationExpectation { + namespace: Some("test namespace"), + name: "test_crate", + version: "1.0.0", + span_name: "GET", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(80)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("http://example.com/path"), + ), + ], + }, ); } @@ -586,34 +591,38 @@ mod tests { ) .await; - check_instrumentation_result( + check_request_instrumentation_result( mock_tracer.clone(), - None, - "test_crate", - "1.0.0", - "GET", - SpanStatus::Unset, - vec![ - (URL_SCHEME_ATTRIBUTE, AttributeValue::from("https")), - ( - AZ_CLIENT_REQUEST_ID_ATTRIBUTE, - AttributeValue::from("test-client-request-id"), - ), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(200), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), - ( - SERVER_ADDRESS_ATTRIBUTE, - AttributeValue::from("example.com"), - ), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("https://example.com/client_request_id"), - ), - ], + 1, + 0, + InstrumentationExpectation { + namespace: None, + name: "test_crate", + version: "1.0.0", + span_name: "GET", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + ( + AZ_CLIENT_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-client-request-id"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://example.com/client_request_id"), + ), + ], + }, ); } @@ -635,28 +644,31 @@ mod tests { }) }) .await; - - check_instrumentation_result( + check_request_instrumentation_result( mock_tracer_provider, - None, - "unknown", - "unknown", - "GET", - SpanStatus::Unset, - vec![ - (URL_SCHEME_ATTRIBUTE, AttributeValue::from("https")), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(200), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), - (SERVER_ADDRESS_ATTRIBUTE, AttributeValue::from("host")), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(8080)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("https://host:8080/path?query=value#fragment"), - ), - ], + 1, + 0, + InstrumentationExpectation { + namespace: None, + name: "unknown", + version: "unknown", + span_name: "GET", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + (SERVER_ADDRESS_ATTRIBUTE, AttributeValue::from("host")), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(8080)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://host:8080/path?query=value#fragment"), + ), + ], + }, ); } @@ -684,46 +696,49 @@ mod tests { }, ) .await; - - check_instrumentation_result( - mock_tracer.clone(), - Some("test namespace"), - "test_crate", - "1.0.0", - "PUT", - SpanStatus::Error { - description: "".to_string(), + check_request_instrumentation_result( + mock_tracer, + 1, + 0, + InstrumentationExpectation { + namespace: Some("test namespace"), + name: "test_crate", + version: "1.0.0", + span_name: "PUT", + status: SpanStatus::Error { + description: "".to_string(), + }, + kind: SpanKind::Client, + attributes: vec![ + (ERROR_TYPE_ATTRIBUTE, AttributeValue::from("404")), + ( + AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-service-request-id"), + ), + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), + ( + AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-service-request-id"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(404), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("PUT")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("microsoft.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://microsoft.com/request_failed.htm"), + ), + ], }, - vec![ - (ERROR_TYPE_ATTRIBUTE, AttributeValue::from("404")), - ( - AZ_SERVICE_REQUEST_ID_ATTRIBUTE, - AttributeValue::from("test-service-request-id"), - ), - ( - AZ_NAMESPACE_ATTRIBUTE, - AttributeValue::from("test namespace"), - ), - (URL_SCHEME_ATTRIBUTE, AttributeValue::from("https")), - ( - AZ_SERVICE_REQUEST_ID_ATTRIBUTE, - AttributeValue::from("test-service-request-id"), - ), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(404), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("PUT")), - ( - SERVER_ADDRESS_ATTRIBUTE, - AttributeValue::from("microsoft.com"), - ), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("https://microsoft.com/request_failed.htm"), - ), - ], ); } } diff --git a/sdk/core/azure_core/src/lib.rs b/sdk/core/azure_core/src/lib.rs index edf21fa808..6f68543bf2 100644 --- a/sdk/core/azure_core/src/lib.rs +++ b/sdk/core/azure_core/src/lib.rs @@ -27,7 +27,10 @@ pub use typespec_client_core::{ fmt, json, sleep, stream, time, Bytes, Uuid, }; -pub use typespec_client_core::tracing; +pub mod tracing { + pub use crate::http::policies::PublicApiInstrumentationInformation; + pub use typespec_client_core::tracing::*; +} #[cfg(feature = "xml")] pub use typespec_client_core::xml; diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 57e3b9aade..37047a7f23 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -10,7 +10,7 @@ use azure_core::{ ClientMethodOptions, ClientOptions, Pipeline, RawResponse, Request, RequestInstrumentationOptions, Url, }, - tracing::{Attribute, Tracer}, + tracing::{PublicApiInstrumentationInformation, Tracer}, Result, }; use azure_core_opentelemetry::OpenTelemetryTracerProvider; @@ -154,57 +154,18 @@ impl TestServiceClient { options: Option>, ) -> Result { let mut options = options.unwrap_or_default(); - let mut ctx = options.method_options.context.clone(); - let span = if let Some(tracer) = &self.tracer { - let mut attributes = Vec::new(); - if let Some(namespace) = tracer.namespace() { - // If the tracer has a namespace, we set it as an attribute. - attributes.push(Attribute { - key: "az.namespace", - value: namespace.into(), - }); - } - let span = tracer.start_span( - "get_with_tracing", - azure_core::tracing::SpanKind::Internal, - attributes, - ); - // We need to add the span to the context because the pipeline will use it as the parent span - // for the request span. - ctx = ctx.with_value(span.clone()); - // And we need to add the tracer to the context so that the pipeline can use it to populate the - // az.namespace property in the request span. - ctx = ctx.with_value(tracer.clone()); - Some(span) - } else { - None - }; - options.method_options.context = ctx; - let response = self.get(path, Some(options)).await; - if let Some(span) = span { - if let Err(e) = &response { - // If the request failed, we set the error type on the span. - match e.kind() { - azure_core::error::ErrorKind::HttpResponse { status, .. } => { - span.set_attribute("error.type", status.to_string().into()); - if status.is_server_error() || status.is_client_error() { - span.set_status(azure_core::tracing::SpanStatus::Error { - description: "".to_string(), - }); - } - } - _ => { - span.set_attribute("error.type", e.kind().to_string().into()); - span.set_status(azure_core::tracing::SpanStatus::Error { - description: e.kind().to_string(), - }); - } - } - } - span.end(); + let public_api_info = PublicApiInstrumentationInformation { + api_name: "get_with_tracing", }; - response + // Add the span to the tracer. + let mut ctx = options.method_options.context.with_value(public_api_info); + // If the service has a tracer, we add it to the context. + if let Some(tracer) = &self.tracer { + ctx = ctx.with_value(tracer.clone()); + } + options.method_options.context = ctx; + self.get(path, Some(options)).await } } @@ -350,7 +311,6 @@ mod tests { parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -411,7 +371,6 @@ mod tests { }, attributes: vec![ ("http.request.method", "GET".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -472,7 +431,6 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -543,7 +501,6 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -568,9 +525,7 @@ mod tests { name: "get_with_tracing", kind: OpenTelemetrySpanKind::Internal, parent_span_id: None, - status: OpenTelemetrySpanStatus::Error { - description: "".into(), - }, + status: OpenTelemetrySpanStatus::Unset, attributes: vec![ ("az.namespace", "Az.TestServiceClient".into()), ("error.type", "404".into()), diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 00dc5c0442..0f54349d52 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,12 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#[cfg(feature = "derive")] -use crate::fmt::SafeDebug; - /// An array of homogeneous attribute values. -#[cfg_attr(feature = "derive", derive(SafeDebug))] -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub enum AttributeArray { /// An array of boolean values. Bool(Vec), @@ -19,8 +15,7 @@ pub enum AttributeArray { } /// Represents a single attribute value, which can be of various types -#[cfg_attr(feature = "derive", derive(SafeDebug))] -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub enum AttributeValue { /// A boolean attribute value. Bool(bool), @@ -34,7 +29,7 @@ pub enum AttributeValue { Array(AttributeArray), } -#[cfg_attr(feature = "derive", derive(SafeDebug))] +#[derive(Debug, PartialEq)] pub struct Attribute { /// A key-value pair attribute. pub key: &'static str, diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index 2b49bfd2b5..0ff30140e1 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -135,7 +135,7 @@ pub enum SpanStatus { Error { description: String }, } -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub enum SpanKind { #[default] Internal, From 43fe44a032d53e6b70307f057bd717b53788d152 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 9 Jul 2025 12:36:33 -0700 Subject: [PATCH 44/84] Added attributes to the PublicApiInstrumentationInformation structure --- .../policies/public_api_instrumentation.rs | 44 ++++++++++++++++++- .../tests/telemetry_service_implementation.rs | 1 + .../src/tracing/attributes.rs | 4 +- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs index 4caccf6d72..6e71dbb6e7 100644 --- a/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs @@ -11,9 +11,34 @@ use typespec_client_core::{ tracing::Attribute, }; +/// Information about the public API being instrumented. +/// +/// This struct is used to pass information about the public API being instrumented +/// to the `PublicApiInstrumentationPolicy`. +/// +/// It contains the name of the API, which is used to create a span for distributed tracing +/// and any additional per-API attributes that might be needed for instrumentation. +/// +/// If the `PublicApiInstrumentationPolicy` policy detects a `PublicApiInstrumentationInformation` in the context, +/// it will create a span with the API name and any additional attributes. #[derive(Debug)] pub struct PublicApiInstrumentationInformation { + /// The name of the API being instrumented. + /// + /// The API name should be in the form of ., where + /// `` is the name of the service client and `` is the name of the API. + /// + /// For example, if the service client is `MyClient` and the API is `my_api`, + /// the API name should be `MyClient.my_api`. pub api_name: &'static str, + + /// Additional attributes to be added to the span for this API. + /// + /// These attributes can provide additional information about the API being instrumented. + /// See [Library-specific attributes](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.md#library-specific-attributes) + /// for more information. + /// + pub attributes: Vec, } const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; @@ -72,6 +97,14 @@ impl Policy for PublicApiInstrumentationPolicy { // This allows for flexibility in using different tracers in different contexts. let mut span_attributes = vec![]; + for attr in &info.attributes { + // Add the attributes from the public API information to the span. + span_attributes.push(Attribute { + key: attr.key, + value: attr.value.clone(), + }); + } + if let Some(tracer) = ctx.value::>() { if let Some(namespace) = tracer.namespace() { // If the tracer has a namespace, we set it as an attribute. @@ -183,6 +216,7 @@ mod tests { { let public_api_information = PublicApiInstrumentationInformation { api_name: api_name.unwrap_or("unknown"), + attributes: Vec::new(), }; // Add the public API information and tracer to the context so that it can be used by the policy. let mock_tracer_provider = Arc::new(MockTracingProvider::new()); @@ -238,6 +272,10 @@ mod tests { ]; let public_api_information = PublicApiInstrumentationInformation { api_name: api_name.unwrap_or("unknown"), + attributes: vec![Attribute { + key: "az.fake_attribute", + value: "attribute value".into(), + }], }; // Add the public API information and tracer to the context so that it can be used by the policy. @@ -394,7 +432,11 @@ mod tests { Some("MyClient.MyApi"), SpanKind::Internal, SpanStatus::Unset, - vec![(AZ_NAMESPACE_ATTRIBUTE, "test.namespace".into())], + vec![ + (AZ_NAMESPACE_ATTRIBUTE, "test.namespace".into()), + // Attribute comes from the public API information. + ("az.fake_attribute", "attribute value".into()), + ], ); check_request_instrumentation_result( diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 37047a7f23..e9576d455a 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -157,6 +157,7 @@ impl TestServiceClient { let public_api_info = PublicApiInstrumentationInformation { api_name: "get_with_tracing", + attributes: vec![], }; // Add the span to the tracer. let mut ctx = options.method_options.context.with_value(public_api_info); diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 0f54349d52..7ae0108038 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. /// An array of homogeneous attribute values. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum AttributeArray { /// An array of boolean values. Bool(Vec), @@ -15,7 +15,7 @@ pub enum AttributeArray { } /// Represents a single attribute value, which can be of various types -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum AttributeValue { /// A boolean attribute value. Bool(bool), From 1eb9d20e3d1dcb2efbb6c46dadc89b52bf03ebe4 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 9 Jul 2025 12:46:30 -0700 Subject: [PATCH 45/84] Updated cargo.lock --- Cargo.lock | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a6f2f366a..de47513978 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1723,24 +1723,6 @@ dependencies = [ "opentelemetry", ] -[[package]] -name = "opentelemetry_sdk" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0da0d6b47a3dbc6e9c9e36a0520e25cf943e046843818faaa3f87365a548c82" -dependencies = [ - "async-trait", - "futures-channel", - "futures-executor", - "futures-util", - "glob", - "once_cell", - "opentelemetry 0.25.0", - "percent-encoding", - "rand 0.8.5", - "thiserror 1.0.69", -] - [[package]] name = "opentelemetry_sdk" version = "0.30.0" From 6e50363cf739ee6b7c69f771c02f11071fff0cf1 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 9 Jul 2025 13:24:22 -0700 Subject: [PATCH 46/84] CI pipeline fixes --- .../azure_core/src/http/policies/public_api_instrumentation.rs | 2 +- sdk/typespec/typespec_client_core/src/error/http_error.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs index 6e71dbb6e7..3a40546ef5 100644 --- a/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs @@ -25,7 +25,7 @@ use typespec_client_core::{ pub struct PublicApiInstrumentationInformation { /// The name of the API being instrumented. /// - /// The API name should be in the form of ., where + /// The API name should be in the form of `.`, where /// `` is the name of the service client and `` is the name of the API. /// /// For example, if the service client is `MyClient` and the API is `my_api`, diff --git a/sdk/typespec/typespec_client_core/src/error/http_error.rs b/sdk/typespec/typespec_client_core/src/error/http_error.rs index 7dcc478f5e..4e04c2436e 100644 --- a/sdk/typespec/typespec_client_core/src/error/http_error.rs +++ b/sdk/typespec/typespec_client_core/src/error/http_error.rs @@ -50,7 +50,7 @@ impl HttpError { /// This searches the entire ["source" chain](https://doc.rust-lang.org/std/error/trait.Error.html#method.source) /// looking for an `HttpError`. pub fn try_from(error: &Error) -> Option<&Self> { - let mut error = error.get_ref()? as &(dyn std::error::Error); + let mut error = error.get_ref()? as &dyn std::error::Error; loop { match error.downcast_ref::() { Some(e) => return Some(e), From a7c72451edddb0e10cc98ec20760c7aa34815cd8 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 9 Jul 2025 14:59:50 -0700 Subject: [PATCH 47/84] Post merge fixes --- Cargo.lock | 12 - .../policies/public_api_instrumentation.rs | 8 + .../http/policies/request_instrumentation.rs | 5 +- sdk/core/azure_core/src/lib.rs | 1 + sdk/core/azure_core_macros/src/tracing.rs | 3 +- sdk/core/azure_core_opentelemetry/README.md | 5 +- .../tests/telemetry_service_implementation.rs | 4 - .../tests/telemetry_service_macros.rs | 219 ++++++------------ 8 files changed, 92 insertions(+), 165 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6265445529..3195cbda95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1728,18 +1728,6 @@ name = "opentelemetry-http" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry 0.30.0", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0da0d6b47a3dbc6e9c9e36a0520e25cf943e046843818faaa3f87365a548c82" dependencies = [ "async-trait", "bytes", diff --git a/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs index 3a40546ef5..a30ef502e1 100644 --- a/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs @@ -5,6 +5,7 @@ use crate::{ http::{Context, Request}, tracing::{Span, SpanKind, Tracer}, }; +use ::tracing::trace; use std::sync::Arc; use typespec_client_core::{ http::policies::{Policy, PolicyResult}, @@ -81,6 +82,7 @@ impl Policy for PublicApiInstrumentationPolicy { ) -> PolicyResult { // If there is a span in the context, we're a nested call, so we just want to forward the request. if ctx.value::>().is_some() { + trace!("PublicApiPolicy: Nested call detected, forwarding request without instrumentation."); return next[0].send(ctx, request, &next[1..]).await; } @@ -129,9 +131,12 @@ impl Policy for PublicApiInstrumentationPolicy { )) } else { // If no tracer is available, we skip instrumentation. + trace!("PublicApiPolicy: No tracer available, skipping instrumentation."); None } } else { + // If there is no public API information, we skip instrumentation. + trace!("PublicApiPolicy: No public API information found, skipping instrumentation."); None }; @@ -146,9 +151,11 @@ impl Policy for PublicApiInstrumentationPolicy { if let Some(span) = span { if let Err(e) = &result { + trace!("Request failed: {}: {:?}", e, e.kind()); // If the request failed, we set the error type on the span. match e.kind() { crate::error::ErrorKind::HttpResponse { status, .. } => { + trace!("Adding error type to span"); span.set_attribute(ERROR_TYPE_ATTRIBUTE, status.to_string().into()); // 5xx status codes SHOULD set status to Error. @@ -167,6 +174,7 @@ impl Policy for PublicApiInstrumentationPolicy { } } } else if let Ok(response) = &result { + trace!("Request succeeded: {}", response.status()); // 5xx status codes SHOULD set status to Error. // The description should not be set because it can be inferred from "http.response.status_code". if response.status().is_server_error() { diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs index 40480dc531..10796aa277 100644 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs @@ -20,7 +20,6 @@ const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE: &str = "http.response.status_code"; const HTTP_REQUEST_METHOD_ATTRIBUTE: &str = "http.request.method"; const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; const SERVER_PORT_ATTRIBUTE: &str = "server.port"; -const URL_SCHEME_ATTRIBUTE: &str = "url.scheme"; const URL_FULL_ATTRIBUTE: &str = "url.full"; /// Sets distributed tracing information for HTTP requests. @@ -279,8 +278,8 @@ pub(crate) mod tests { let mut tracers = self.tracers.lock().unwrap(); let tracer = Arc::new(MockTracer { namespace: azure_namespace, - name: crate_name, - version: crate_version, + package_name: crate_name, + package_version: crate_version, spans: Mutex::new(Vec::new()), }); diff --git a/sdk/core/azure_core/src/lib.rs b/sdk/core/azure_core/src/lib.rs index 6f68543bf2..589a5a0ef5 100644 --- a/sdk/core/azure_core/src/lib.rs +++ b/sdk/core/azure_core/src/lib.rs @@ -29,6 +29,7 @@ pub use typespec_client_core::{ pub mod tracing { pub use crate::http::policies::PublicApiInstrumentationInformation; + pub use azure_core_macros::*; pub use typespec_client_core::tracing::*; } diff --git a/sdk/core/azure_core_macros/src/tracing.rs b/sdk/core/azure_core_macros/src/tracing.rs index 72d4d124a8..0d68be3903 100644 --- a/sdk/core/azure_core_macros/src/tracing.rs +++ b/sdk/core/azure_core_macros/src/tracing.rs @@ -5,8 +5,6 @@ pub(crate) mod tests { use proc_macro2::{TokenStream, TokenTree}; - use super::*; - // cspell: ignore punct pub(crate) fn compare_token_tree(token: &TokenTree, expected_token: &TokenTree) -> bool { @@ -90,6 +88,7 @@ pub(crate) mod tests { #[derive(Default)] pub struct MyServiceClientOptions { + #[allow(dead_code)] pub client_options: ClientOptions, } diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index 4ef96760a3..4d33ec0f07 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -46,6 +46,7 @@ If it is more convenient to use the global OpenTelemetry provider, then the [`Op ```rust no_run # use azure_identity::DefaultAzureCredential; # use azure_core::{http::{ClientOptions, RequestInstrumentationOptions}}; + # #[derive(Default)] # struct ServiceClientOptions { # client_options: ClientOptions, @@ -59,13 +60,13 @@ use std::sync::Arc; let azure_provider = OpenTelemetryTracerProvider::new_from_global_provider(); let options = ServiceClientOptions { + client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), }), ..Default::default() }, - ..Default::default() - }; +}; # Ok(()) # } diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 61be094d1b..70b0817237 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -180,7 +180,6 @@ mod tests { SpanKind as OpenTelemetrySpanKind, Status as OpenTelemetrySpanStatus, }; use opentelemetry::Value as OpenTelemetryAttributeValue; - use tracing::{info, trace}; fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { let otel_exporter = InMemorySpanExporter::default(); @@ -313,7 +312,6 @@ mod tests { parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -374,7 +372,6 @@ mod tests { }, attributes: vec![ ("http.request.method", "GET".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -435,7 +432,6 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs index e0dc9b4db4..55e595b7a9 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs @@ -11,7 +11,7 @@ use azure_core::{ RequestInstrumentationOptions, Url, }, tracing, - tracing::Attribute, + tracing::PublicApiInstrumentationInformation, Result, }; use azure_core_opentelemetry::OpenTelemetryTracerProvider; @@ -19,12 +19,12 @@ use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider}; use std::sync::Arc; #[derive(Clone, SafeDebug)] -pub struct TestServiceClientOptions { +pub struct TestServiceClientWithMacrosOptions { pub client_options: ClientOptions, pub api_version: Option, } -impl Default for TestServiceClientOptions { +impl Default for TestServiceClientWithMacrosOptions { fn default() -> Self { Self { client_options: ClientOptions::default(), @@ -36,18 +36,18 @@ impl Default for TestServiceClientOptions { /// Define a TestServiceClient which is a fake service client for testing purposes. /// This client demonstrates how to implement a service client using the tracing convenience proc macros. #[tracing::client] -pub struct TestServiceClient { +pub struct TestServiceClientWithMacros { endpoint: Url, api_version: String, pipeline: Pipeline, } #[derive(Default, SafeDebug)] -pub struct TestServiceClientGetMethodOptions<'a> { +pub struct TestServiceClientWithMacrosGetMethodOptions<'a> { pub method_options: ClientMethodOptions<'a>, } -impl TestServiceClient { +impl TestServiceClientWithMacros { /// Creates a new instance of the TestServiceClient. /// /// This function demonstrates how to create a service client using the tracing convenience proc macros. @@ -61,7 +61,7 @@ impl TestServiceClient { pub fn new( endpoint: &str, _credential: Arc, - options: Option, + options: Option, ) -> Result { let options = options.unwrap_or_default(); let mut endpoint = Url::parse(endpoint)?; @@ -91,15 +91,10 @@ impl TestServiceClient { &self.endpoint } - /// Returns the result of a Get verb against the configured endpoint with the specified path. - /// - /// This method demonstrates a service client which does not have per-method spans but which will create - /// HTTP client spans if the `RequestInstrumentationOptions` are configured in the client options. - /// pub async fn get( &self, path: &str, - options: Option>, + options: Option>, ) -> Result { let options = options.unwrap_or_default(); let mut url = self.endpoint.clone(); @@ -144,68 +139,43 @@ impl TestServiceClient { /// This applies to most HTTP client operations, but not all. CosmosDB has its own set of conventions as listed /// [here](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/cosmosdb.md) /// - #[tracing::function] + // #[tracing::function] pub async fn get_with_function_tracing( &self, path: &str, - options: Option>, + options: Option>, ) -> Result { - let mut options = options.unwrap_or_default(); - let mut ctx = options.method_options.context.clone(); - let span = if ctx.value::>().is_none() { - if let Some(tracer) = &self.tracer { - let mut attributes = Vec::new(); - if let Some(namespace) = tracer.namespace() { - // If the tracer has a namespace, we set it as an attribute. - attributes.push(Attribute { - key: "az.namespace", - value: namespace.into(), - }); - } - let span = tracer.start_span( - "get_with_tracing", - azure_core::tracing::SpanKind::Internal, - attributes, - ); - // We need to add the span to the context because the pipeline will use it as the parent span - // for the request span. - ctx = ctx.with_value(span.clone()); - // And we need to add the tracer to the context so that the pipeline can use it to populate the - // az.namespace property in the request span. - ctx = ctx.with_value(tracer.clone()); - Some(span) - } else { - None - } - } else { - None - }; - options.method_options.context = ctx; - let response = self.get(path, Some(options)).await; - if let Some(span) = span { - if let Err(e) = &response { - // If the request failed, we set the error type on the span. - match e.kind() { - azure_core::error::ErrorKind::HttpResponse { status, .. } => { - span.set_attribute("error.type", status.to_string().into()); - if status.is_server_error() || status.is_client_error() { - span.set_status(azure_core::tracing::SpanStatus::Error { - description: "".to_string(), - }); - } - } - _ => { - span.set_attribute("error.type", e.kind().to_string().into()); - span.set_status(azure_core::tracing::SpanStatus::Error { - description: e.kind().to_string(), - }); - } - } - } + let options = options.unwrap_or_default(); - span.end(); + let public_api_info = PublicApiInstrumentationInformation { + api_name: "macros_get_with_tracing", + attributes: vec![], }; - response + // Add the span to the tracer. + let mut ctx = options.method_options.context.with_value(public_api_info); + // If the service has a tracer, we add it to the context. + if let Some(tracer) = &self.tracer { + ctx = ctx.with_value(tracer.clone()); + } + + let mut url = self.endpoint.clone(); + url.set_path(path); + url.query_pairs_mut() + .append_pair("api-version", &self.api_version); + + let mut request = Request::new(url, azure_core::http::Method::Get); + + let response = self.pipeline.send(&ctx, &mut request).await?; + if !response.status().is_success() { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::HttpResponse { + status: response.status(), + error_code: None, + }, + format!("Failed to GET {}: {}", request.url(), response.status()), + )); + } + Ok(response) } } @@ -213,6 +183,7 @@ impl TestServiceClient { mod tests { use super::*; use ::tracing::{info, trace}; + use azure_core::tracing::TracerProvider; use azure_core::Result; use azure_core_test::{recorded, TestContext}; use opentelemetry::trace::{ @@ -229,6 +200,26 @@ mod tests { (otel_tracer_provider, otel_exporter) } + fn create_service_client( + ctx: TestContext, + azure_provider: Arc, + ) -> TestServiceClientWithMacros { + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientWithMacrosOptions { + client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + + TestServiceClientWithMacros::new(endpoint, credential, Some(options)).unwrap() + } + // Span verification utility functions. struct ExpectedSpan { @@ -281,56 +272,28 @@ mod tests { // Basic functionality tests. #[recorded::test()] - async fn test_service_client_new(ctx: TestContext) -> Result<()> { + async fn test_macro_service_client_new(ctx: TestContext) -> Result<()> { let recording = ctx.recording(); let endpoint = "https://example.com"; let credential = recording.credential().clone(); - let options = TestServiceClientOptions { + let options = TestServiceClientWithMacrosOptions { ..Default::default() }; - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let client = TestServiceClientWithMacros::new(endpoint, credential, Some(options)).unwrap(); assert_eq!(client.endpoint().as_str(), "https://example.com/"); assert_eq!(client.api_version, "2023-10-01"); Ok(()) } - // Ensure that the the test client actually does what it's supposed to do without telemetry. #[recorded::test()] - async fn test_service_client_get(ctx: TestContext) -> Result<()> { - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - - let client = TestServiceClient::new(endpoint, credential, None).unwrap(); - let response = client.get("index.html", None).await; - info!("Response: {:?}", response); - assert!(response.is_ok()); - let response = response.unwrap(); - assert_eq!(response.status(), azure_core::http::StatusCode::Ok); - Ok(()) - } - - #[recorded::test()] - async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { + async fn test_macro_service_client_get(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - client_options: ClientOptions { - request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), - }), - ..Default::default() - }, - ..Default::default() - }; + let client = create_service_client(ctx, azure_provider.clone()); - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); let response = client.get("index.html", None).await; info!("Response: {:?}", response); assert!(response.is_ok()); @@ -351,7 +314,6 @@ mod tests { parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -375,24 +337,12 @@ mod tests { } #[recorded::test()] - async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { + async fn test_macro_service_client_get_with_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - client_options: ClientOptions { - request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), - }), - ..Default::default() - }, - ..Default::default() - }; + let client = create_service_client(ctx, azure_provider.clone()); - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); let response = client.get("failing_url", None).await; info!("Response: {:?}", response); @@ -412,7 +362,6 @@ mod tests { }, attributes: vec![ ("http.request.method", "GET".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -437,24 +386,12 @@ mod tests { } #[recorded::test()] - async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { + async fn test_macro_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - client_options: ClientOptions { - request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), - }), - ..Default::default() - }, - ..Default::default() - }; + let client = create_service_client(ctx, azure_provider.clone()); - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); let response = client.get_with_function_tracing("index.html", None).await; info!("Response: {:?}", response); @@ -473,7 +410,6 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -494,7 +430,7 @@ mod tests { verify_span( &spans[1], ExpectedSpan { - name: "get_with_tracing", + name: "macros_get_with_tracing", kind: OpenTelemetrySpanKind::Internal, parent_span_id: None, status: OpenTelemetrySpanStatus::Unset, @@ -506,14 +442,16 @@ mod tests { } #[recorded::test()] - async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { + async fn test_macro_service_client_get_with_function_tracing_error( + ctx: TestContext, + ) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); let endpoint = "https://example.com"; let credential = recording.credential().clone(); - let options = TestServiceClientOptions { + let options = TestServiceClientWithMacrosOptions { client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { tracing_provider: Some(azure_provider), @@ -523,7 +461,7 @@ mod tests { ..Default::default() }; - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let client = TestServiceClientWithMacros::new(endpoint, credential, Some(options)).unwrap(); let response = client.get_with_function_tracing("failing_url", None).await; info!("Response: {:?}", response); @@ -544,7 +482,6 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("url.scheme", "https".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -566,12 +503,10 @@ mod tests { verify_span( &spans[1], ExpectedSpan { - name: "get_with_tracing", + name: "macros_get_with_tracing", kind: OpenTelemetrySpanKind::Internal, parent_span_id: None, - status: OpenTelemetrySpanStatus::Error { - description: "".into(), - }, + status: OpenTelemetrySpanStatus::Unset, attributes: vec![ ("az.namespace", "Az.TestServiceClient".into()), ("error.type", "404".into()), From 7ec16b9245ae74563aed312965a9bcae516aeb89 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Fri, 11 Jul 2025 10:26:25 -0700 Subject: [PATCH 48/84] tracing::function now works as expected --- Cargo.lock | 1 + sdk/core/azure_core_macros/Cargo.toml | 1 + sdk/core/azure_core_macros/src/lib.rs | 4 +- .../azure_core_macros/src/tracing_client.rs | 4 +- .../azure_core_macros/src/tracing_function.rs | 344 ++++++++++++++++-- .../tests/telemetry_service_macros.rs | 32 +- 6 files changed, 328 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3195cbda95..cc354ca0e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,7 @@ dependencies = [ "quote", "syn", "tokio", + "typespec_client_core", ] [[package]] diff --git a/sdk/core/azure_core_macros/Cargo.toml b/sdk/core/azure_core_macros/Cargo.toml index 88f3db6092..7632fbffd9 100644 --- a/sdk/core/azure_core_macros/Cargo.toml +++ b/sdk/core/azure_core_macros/Cargo.toml @@ -20,6 +20,7 @@ proc-macro = true proc-macro2.workspace = true quote.workspace = true syn.workspace = true +typespec_client_core.workspace = true [dev-dependencies] azure_core.workspace = true diff --git a/sdk/core/azure_core_macros/src/lib.rs b/sdk/core/azure_core_macros/src/lib.rs index 0e12335b8c..4df16c8892 100644 --- a/sdk/core/azure_core_macros/src/lib.rs +++ b/sdk/core/azure_core_macros/src/lib.rs @@ -104,8 +104,8 @@ pub fn new(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// impl MyServiceClient { /// -/// #[tracing::function] -/// pub fn public_function(param: &str, options: Option) -> Result<()> { +/// #[tracing::function("MyServiceClient.PublicFunction")] +/// pub async fn public_function(&self, param: &str, options: Option) -> Result<()> { /// let options = options.unwrap_or_default(); /// Ok(()) /// } diff --git a/sdk/core/azure_core_macros/src/tracing_client.rs b/sdk/core/azure_core_macros/src/tracing_client.rs index bde8f8350e..d05ecd5b43 100644 --- a/sdk/core/azure_core_macros/src/tracing_client.rs +++ b/sdk/core/azure_core_macros/src/tracing_client.rs @@ -27,7 +27,7 @@ pub fn parse_client(_attr: TokenStream, item: TokenStream) -> Result>, + tracer: Option>, } }) } @@ -65,7 +65,7 @@ mod tests { pub struct ServiceClient { name: &'static str, endpoint: Url, - tracer: Option>, + tracer: Option>, } }; // println!("Parsed tokens: {:?}", tokens); diff --git a/sdk/core/azure_core_macros/src/tracing_function.rs b/sdk/core/azure_core_macros/src/tracing_function.rs index 71c4051300..7d630491b7 100644 --- a/sdk/core/azure_core_macros/src/tracing_function.rs +++ b/sdk/core/azure_core_macros/src/tracing_function.rs @@ -2,8 +2,8 @@ // Licensed under the MIT License. use proc_macro2::TokenStream; -use quote::quote; -use syn::{spanned::Spanned, ItemFn, Result}; +use quote::{quote, ToTokens}; +use syn::{parse::Parse, spanned::Spanned, ItemFn, Member, Result, Token}; const INVALID_PUBLIC_FUNCTION_MESSAGE: &str = "function attribute must be applied to a public function returning a Result."; @@ -21,57 +21,153 @@ const INVALID_PUBLIC_FUNCTION_MESSAGE: &str = /// 1) `Result` /// 1) `Result, E>` /// -pub fn parse_function(_attr: TokenStream, item: TokenStream) -> Result { +pub fn parse_function(attr: TokenStream, item: TokenStream) -> Result { if !is_function_declaration(&item) { + println!("Not a function declaration: {item}"); return Err(syn::Error::new( item.span(), INVALID_PUBLIC_FUNCTION_MESSAGE, )); } - let client_fn: ItemFn = syn::parse2(item.clone())?; + let function_name_and_attributes: FunctionNameAndAttributes = syn::parse2(attr)?; - let vis = &client_fn.vis; - let asyncness = &client_fn.sig.asyncness; - let ident = &client_fn.sig.ident; - let inputs = client_fn.sig.inputs.iter(); - let body = client_fn.block.stmts.iter(); - let output = &client_fn.sig.output; + let api_name = function_name_and_attributes.function_name; - Ok(quote! { - #vis #asyncness - fn #ident(#(#inputs),*) #output { + let ItemFn { + attrs, + vis, + sig, + block, + } = syn::parse2(item)?; + + let attributes: TokenStream = if function_name_and_attributes.arguments.is_empty() { + quote! {Vec::new()} + } else { + let attribute_vec = function_name_and_attributes + .arguments + .into_iter() + .map(|(name, value)| { + quote! { + ::typespec_client_core::tracing::Attribute{key: #name, value: #value.into()} + } + }) + .collect::>(); + quote! { vec![#(#attribute_vec),*] } + }; + + let preamble = quote! { + let options = { let mut options = options.unwrap_or_default(); - let mut ctx = options.method_options.context.clone(); - let span = if ctx.value::>().is_none() { + + let public_api_info = azure_core::tracing::PublicApiInstrumentationInformation { + api_name: #api_name, + attributes: #attributes, + }; + // Add the span to the tracer. + let mut ctx = options.method_options.context.with_value(public_api_info); + // If the service has a tracer, we add it to the context. if let Some(tracer) = &self.tracer { - let mut attributes = Vec::new(); - if let Some(namespace) = tracer.namespace() { - // If the tracer has a namespace, we set it as an attribute. - attributes.push(azure_core::tracing::Attribute { - key: "az.namespace", - value: namespace.into(), - }); - } - let span = tracer.start_span( - stringify!(#ident), - azure_core::tracing::SpanKind::Internal, - attributes, - ); - ctx = ctx.with_value(span.clone()); ctx = ctx.with_value(tracer.clone()); - Some(span) + } + options.method_options.context = ctx; + Some(options) + }; + }; + + // Clear the actual test method parameters. + Ok(quote! { + #(#attrs)* + #vis #sig { + #preamble + #block + } + }) +} + +#[derive(Debug)] +struct FunctionNameAndAttributes { + function_name: String, + arguments: Vec<(String, syn::Expr)>, +} + +fn name_from_expr(expr: &syn::Expr) -> Result { + match expr { + syn::Expr::Lit(lit) => match &lit.lit { + syn::Lit::Str(lit_str) => Ok(lit_str.value()), + _ => Err(syn::Error::new(lit.span(), "Unsupported literal type")), + }, + syn::Expr::Path(expr_path) => expr_path + .path + .get_ident() + .ok_or_else(|| { + syn::Error::new( + expr_path.span(), + "Expected an identifier in path expression", + ) + }) + .map(|ident| ident.to_string()), + syn::Expr::Field(expr_field) => { + // If it's a field, we can extract the base and path + // This assumes the field is a path like `az.foo.bar.namespace` + // and we want to extract `az.foo.bar.namespace` + let base = name_from_expr(expr_field.base.as_ref())?; + let member = match &expr_field.member { + Member::Named(ident) => ident.to_string(), + Member::Unnamed(_) => { + println!("Anonymous member"); + // If it's an unnamed member, we can use the index or some other identifier + // Here we assume it's a named member for simplicity + format!("{:?}", expr_field.member.to_token_stream()) + } + }; + Ok(format!("{base}.{member}")) + } + _ => Err(syn::Error::new(expr.span(), "Unsupported expression type")), + } +} + +impl Parse for FunctionNameAndAttributes { + fn parse(input: syn::parse::ParseStream) -> Result { + let function_name = input.parse::()?.value(); + // If the next character is a comma, we expect a list of attributes. + if input.peek(Token!(,)) { + input.parse::()?; + if input.peek(syn::token::Paren) { + let content; + let _ = syn::parenthesized!(content in input); + let mut arguments: syn::punctuated::Punctuated = + syn::punctuated::Punctuated::new(); + if !content.is_empty() { + arguments = content.parse_terminated(syn::ExprAssign::parse, syn::Token![,])?; + } + let arguments_result = arguments + .into_iter() + .map(|arg| { + let syn::ExprAssign { left, right, .. } = arg; + let (left, right) = (left, right); + let name = name_from_expr(left.as_ref())?; + Ok((name, *right)) + }) + .collect::>>()?; + + Ok(FunctionNameAndAttributes { + function_name, + arguments: arguments_result, + }) } else { - None + Err(syn::Error::new( + input.span(), + "Expected parentheses after function name.", + )) } } else { - None - }; - options.method_options.context = ctx; - let options = Some(options); - #(#body)* + Ok(FunctionNameAndAttributes { + function_name, + arguments: vec![], + }) + } } - }) } fn is_function_declaration(item: &TokenStream) -> bool { @@ -85,6 +181,11 @@ fn is_function_declaration(item: &TokenStream) -> bool { return false; } + // Function must be public. + if item_fn.sig.asyncness.is_none() { + return false; + } + // Function must return a Result type. if let syn::ReturnType::Type(_, ty) = &item_fn.sig.output { if !matches!(ty.as_ref(), syn::Type::Path(_)) { @@ -96,3 +197,172 @@ fn is_function_declaration(item: &TokenStream) -> bool { true } + +#[cfg(test)] +mod tests { + use syn::parse_quote; + + use super::*; + + #[test] + fn test_parse_function_name_and_attributes() { + let types = [ + quote! { "Text String" }, + quote! { "Text String", (a = 1, b = 2) }, + ]; + + for stream in types.iter() { + let parsed: FunctionNameAndAttributes = + syn::parse2(stream.clone()).expect("Failed to parse"); + assert_eq!(parsed.function_name, "Text String"); + if !parsed.arguments.is_empty() { + assert_eq!(parsed.arguments.len(), 2); + assert_eq!(parsed.arguments[0].0, "a"); + assert_eq!(parsed.arguments[1].0, "b"); + } + } + + { + let test_stream = quote! { "Test Function", (arg1 = 42, arg2 = "value") }; + let parsed: FunctionNameAndAttributes = + syn::parse2(test_stream).expect("Failed to parse"); + assert_eq!(parsed.function_name, "Test Function"); + assert_eq!(parsed.arguments.len(), 2); + assert_eq!(parsed.arguments[0].0, "arg1"); + assert_eq!(parsed.arguments[0].1, parse_quote!(42)); + assert_eq!(parsed.arguments[1].0, "arg2"); + assert_eq!(parsed.arguments[1].1, parse_quote!("value")); + } + { + let test_stream = quote! { "Test Function", ("az.namespace" = "my namespace", az.test_value = "value") }; + let parsed: FunctionNameAndAttributes = + syn::parse2(test_stream).expect("Failed to parse"); + assert_eq!(parsed.function_name, "Test Function"); + assert_eq!(parsed.arguments.len(), 2); + assert_eq!(parsed.arguments[0].0, "az.namespace"); + assert_eq!(parsed.arguments[0].1, parse_quote!("my namespace")); + assert_eq!(parsed.arguments[1].0, "az.test_value"); + assert_eq!(parsed.arguments[1].1, parse_quote!("value")); + } + { + let test_stream = quote! { "Test Function", (az.namespace = "my namespace", az.test_value = "value") }; + let parsed: FunctionNameAndAttributes = + syn::parse2(test_stream).expect("Failed to parse"); + assert_eq!(parsed.function_name, "Test Function"); + assert_eq!(parsed.arguments.len(), 2); + assert_eq!(parsed.arguments[0].0, "az.namespace"); + assert_eq!(parsed.arguments[0].1, parse_quote!("my namespace")); + assert_eq!(parsed.arguments[1].0, "az.test_value"); + assert_eq!(parsed.arguments[1].1, parse_quote!("value")); + } + { + let test_stream = quote! {"macros_get_with_tracing", (az.path = path, az.info = "Test", az.number = 42)}; + let parsed: FunctionNameAndAttributes = + syn::parse2(test_stream).expect("Failed to parse"); + assert_eq!(parsed.function_name, "macros_get_with_tracing"); + assert_eq!(parsed.arguments.len(), 3); + assert_eq!(parsed.arguments[0].0, "az.path"); + assert_eq!(parsed.arguments[0].1, parse_quote!(path)); + + assert_eq!(parsed.arguments[1].0, "az.info"); + assert_eq!(parsed.arguments[1].1, parse_quote!("Test")); + + assert_eq!(parsed.arguments[2].0, "az.number"); + assert_eq!(parsed.arguments[2].1, parse_quote!(42)); + } + { + let test_stream = quote! { "Test Function", (az.foo.bar.namespace = "my namespace", az.test_value = "value") }; + let parsed: FunctionNameAndAttributes = + syn::parse2(test_stream).expect("Failed to parse"); + assert_eq!(parsed.function_name, "Test Function"); + assert_eq!(parsed.arguments.len(), 2); + assert_eq!(parsed.arguments[0].0, "az.foo.bar.namespace"); + assert_eq!(parsed.arguments[0].1, parse_quote!("my namespace")); + assert_eq!(parsed.arguments[1].0, "az.test_value"); + assert_eq!(parsed.arguments[1].1, parse_quote!("value")); + } + + { + let test_stream = quote! { "Test Function", }; + + syn::parse2::(test_stream) + .expect_err("Should fail to parse."); + } + { + let test_stream = quote! { "Test Function",(23.5= "value") }; + + syn::parse2::(test_stream) + .expect_err("Should fail to parse."); + } + { + let test_stream = quote! { "Test Function", ()}; + + syn::parse2::(test_stream).expect("No attributes are ok."); + } + } + + #[test] + fn test_is_function_declaration() { + let valid_fn = quote! { + pub async fn my_function() -> Result<(), Box> { + } + }; + let invalid_fn = quote! { + pub fn my_function() -> Result<(), Box> { + } + }; + + assert!(is_function_declaration(&valid_fn)); + assert!(!is_function_declaration(&invalid_fn)); + } + + #[test] + fn test_parse_function() -> std::result::Result<(), syn::Error> { + let attr = quote! { "TestFunction" }; + let item = quote! { + pub async fn my_function(&self, path: &str) -> Result<(), Box> { + let options = options.unwrap_or_default(); + + let mut url = self.endpoint.clone(); + url.set_path(path); + url.query_pairs_mut() + .append_pair("api-version", &self.api_version); + + let mut request = Request::new(url, azure_core::http::Method::Get); + + let response = self + .pipeline + .send(&options.method_options.context, &mut request) + .await?; + if !response.status().is_success() { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::HttpResponse { + status: response.status(), + error_code: None, + }, + format!("Failed to GET {}: {}", request.url(), response.status()), + )); + } + Ok(response) + } + }; + + let actual = parse_function(attr, item)?; + let expected = quote! { + pub async fn my_function() -> Result<(), Box> { + let mut options = None; + let result = tracing::function("TestFunction", options); + Ok(()) + } + }; + + println!("Parsed tokens: {:?}", actual.to_string()); + println!("Expected tokens: {:?}", expected.to_string()); + + assert!( + crate::tracing::tests::compare_token_stream(actual, expected), + "Parsed tokens do not match expected tokens" + ); + Ok(()) + } +} diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs index 55e595b7a9..d586def81c 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs @@ -10,9 +10,7 @@ use azure_core::{ ClientMethodOptions, ClientOptions, Pipeline, RawResponse, Request, RequestInstrumentationOptions, Url, }, - tracing, - tracing::PublicApiInstrumentationInformation, - Result, + tracing, Result, }; use azure_core_opentelemetry::OpenTelemetryTracerProvider; use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider}; @@ -139,7 +137,7 @@ impl TestServiceClientWithMacros { /// This applies to most HTTP client operations, but not all. CosmosDB has its own set of conventions as listed /// [here](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/cosmosdb.md) /// - // #[tracing::function] + #[tracing::function("macros_get_with_tracing",(a.b=1,az.telemetry="Abc","string attribute"=path))] pub async fn get_with_function_tracing( &self, path: &str, @@ -147,17 +145,6 @@ impl TestServiceClientWithMacros { ) -> Result { let options = options.unwrap_or_default(); - let public_api_info = PublicApiInstrumentationInformation { - api_name: "macros_get_with_tracing", - attributes: vec![], - }; - // Add the span to the tracer. - let mut ctx = options.method_options.context.with_value(public_api_info); - // If the service has a tracer, we add it to the context. - if let Some(tracer) = &self.tracer { - ctx = ctx.with_value(tracer.clone()); - } - let mut url = self.endpoint.clone(); url.set_path(path); url.query_pairs_mut() @@ -165,7 +152,10 @@ impl TestServiceClientWithMacros { let mut request = Request::new(url, azure_core::http::Method::Get); - let response = self.pipeline.send(&ctx, &mut request).await?; + let response = self + .pipeline + .send(&options.method_options.context, &mut request) + .await?; if !response.status().is_success() { return Err(azure_core::Error::message( azure_core::error::ErrorKind::HttpResponse { @@ -434,7 +424,12 @@ mod tests { kind: OpenTelemetrySpanKind::Internal, parent_span_id: None, status: OpenTelemetrySpanStatus::Unset, - attributes: vec![("az.namespace", "Az.TestServiceClient".into())], + attributes: vec![ + ("az.namespace", "Az.TestServiceClient".into()), + ("a.b", 1.into()), // added by tracing macro. + ("az.telemetry", "Abc".into()), // added by tracing macro + ("string attribute", "index.html".into()), // added by tracing macro. + ], }, )?; @@ -510,6 +505,9 @@ mod tests { attributes: vec![ ("az.namespace", "Az.TestServiceClient".into()), ("error.type", "404".into()), + ("a.b", 1.into()), // added by tracing macro. + ("az.telemetry", "Abc".into()), // added by tracing macro + ("string attribute", "failing_url".into()), // added by tracing macro. ], }, )?; From 4d54207f25d276a26d1480d10329e1ebb25ba7f8 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Fri, 11 Jul 2025 11:49:03 -0700 Subject: [PATCH 49/84] PR feedback --- sdk/core/azure_core/src/http/options/mod.rs | 4 + sdk/core/azure_core/src/http/pipeline.rs | 4 +- .../src/http/policies/instrumentation/mod.rs | 255 ++++++ .../public_api_instrumentation.rs | 147 ++-- .../request_instrumentation.rs | 501 ++++++++++++ sdk/core/azure_core/src/http/policies/mod.rs | 7 +- .../http/policies/request_instrumentation.rs | 744 ------------------ sdk/core/azure_core/src/lib.rs | 1 + sdk/core/azure_core_opentelemetry/README.md | 39 +- sdk/core/azure_core_opentelemetry/src/span.rs | 2 +- .../azure_core_opentelemetry/src/telemetry.rs | 17 +- .../azure_core_opentelemetry/src/tracer.rs | 22 +- ...integration_test.rs => otel_span_tests.rs} | 0 .../typespec_client_core/src/http/method.rs | 2 +- .../src/http/policies/retry/mod.rs | 16 +- .../src/tracing/attributes.rs | 9 +- .../typespec_client_core/src/tracing/mod.rs | 26 +- 17 files changed, 931 insertions(+), 865 deletions(-) create mode 100644 sdk/core/azure_core/src/http/policies/instrumentation/mod.rs rename sdk/core/azure_core/src/http/policies/{ => instrumentation}/public_api_instrumentation.rs (79%) create mode 100644 sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs delete mode 100644 sdk/core/azure_core/src/http/policies/request_instrumentation.rs rename sdk/core/azure_core_opentelemetry/tests/{integration_test.rs => otel_span_tests.rs} (100%) diff --git a/sdk/core/azure_core/src/http/options/mod.rs b/sdk/core/azure_core/src/http/options/mod.rs index fd2e0e835e..e337fa1873 100644 --- a/sdk/core/azure_core/src/http/options/mod.rs +++ b/sdk/core/azure_core/src/http/options/mod.rs @@ -30,6 +30,10 @@ pub struct ClientOptions { /// User-Agent telemetry options. pub user_agent: Option, + /// Options for request instrumentation, such as distributed tracing. + /// + /// If not specified, defaults to no instrumentation. + /// pub request_instrumentation: Option, } diff --git a/sdk/core/azure_core/src/http/pipeline.rs b/sdk/core/azure_core/src/http/pipeline.rs index 96a63510d6..9fa7dec26d 100644 --- a/sdk/core/azure_core/src/http/pipeline.rs +++ b/sdk/core/azure_core/src/http/pipeline.rs @@ -65,8 +65,8 @@ impl Pipeline { .map(|tracing_provider| { tracing_provider.get_tracer( None, - crate_name.unwrap_or("unknown"), - crate_version.unwrap_or("unknown"), + crate_name.unwrap_or("Unknown"), + crate_version.unwrap_or("0.1.0"), ) }) } else { diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/mod.rs b/sdk/core/azure_core/src/http/policies/instrumentation/mod.rs new file mode 100644 index 0000000000..f9e02a03a7 --- /dev/null +++ b/sdk/core/azure_core/src/http/policies/instrumentation/mod.rs @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//! Instrumentation pipeline policies. + +mod public_api_instrumentation; +mod request_instrumentation; + +// Distributed tracing span attribute names. Defined in +// [OpenTelemetrySpans](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md) +// and [Azure conventions for open telemetry spans](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.md) +const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; +const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client.request.id"; +const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; +const AZ_SERVICE_REQUEST_ID_ATTRIBUTE: &str = "az.service_request.id"; +const HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE: &str = "http.request.resend_count"; +const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE: &str = "http.response.status_code"; +const HTTP_REQUEST_METHOD_ATTRIBUTE: &str = "http.request.method"; +const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; +const SERVER_PORT_ATTRIBUTE: &str = "server.port"; +const URL_FULL_ATTRIBUTE: &str = "url.full"; + +pub use public_api_instrumentation::PublicApiInstrumentationInformation; +pub(crate) use public_api_instrumentation::PublicApiInstrumentationPolicy; +pub(crate) use request_instrumentation::*; + +#[cfg(test)] +mod tests { + // cspell: ignore traceparent + use std::sync::{Arc, Mutex}; + use typespec_client_core::{ + http::{headers::HeaderName, Context, Request}, + tracing::{ + AsAny, Attribute, AttributeValue, Span, SpanKind, SpanStatus, Tracer, TracerProvider, + }, + }; + + #[derive(Debug)] + pub(super) struct MockTracingProvider { + pub(super) tracers: Mutex>>, + } + + impl MockTracingProvider { + pub(super) fn new() -> Self { + Self { + tracers: Mutex::new(Vec::new()), + } + } + } + impl TracerProvider for MockTracingProvider { + fn get_tracer( + &self, + azure_namespace: Option<&'static str>, + crate_name: &'static str, + crate_version: &'static str, + ) -> Arc { + let mut tracers = self.tracers.lock().unwrap(); + let tracer = Arc::new(MockTracer { + namespace: azure_namespace, + package_name: crate_name, + package_version: crate_version, + spans: Mutex::new(Vec::new()), + }); + + tracers.push(tracer.clone()); + tracer + } + } + + #[derive(Debug)] + pub(super) struct MockTracer { + pub(super) namespace: Option<&'static str>, + pub(super) package_name: &'static str, + pub(super) package_version: &'static str, + pub(super) spans: Mutex>>, + } + + impl Tracer for MockTracer { + fn namespace(&self) -> Option<&'static str> { + self.namespace + } + + fn start_span_with_current( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + + fn start_span_with_parent( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + _parent: Arc, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + + fn start_span( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + } + + #[derive(Debug)] + pub(super) struct MockSpan { + pub(super) name: String, + pub(super) kind: SpanKind, + pub(super) attributes: Mutex>, + pub(super) state: Mutex, + pub(super) is_open: Mutex, + } + impl MockSpan { + fn new(name: &str, kind: SpanKind, attributes: Vec) -> Self { + println!("Creating MockSpan: {}", name); + println!("Attributes: {:?}", attributes); + Self { + name: name.to_string(), + kind, + attributes: Mutex::new(attributes), + state: Mutex::new(SpanStatus::Unset), + is_open: Mutex::new(true), + } + } + } + + impl Span for MockSpan { + fn set_attribute(&self, key: &'static str, value: AttributeValue) { + println!("{}: Setting attribute {}: {:?}", self.name, key, value); + let mut attributes = self.attributes.lock().unwrap(); + attributes.push(Attribute { key, value }); + } + + fn set_status(&self, status: crate::tracing::SpanStatus) { + println!("{}: Setting span status: {:?}", self.name, status); + let mut state = self.state.lock().unwrap(); + *state = status; + } + + fn end(&self) { + println!("Ending span: {}", self.name); + let mut is_open = self.is_open.lock().unwrap(); + *is_open = false; + } + + fn is_recording(&self) -> bool { + true + } + + fn span_id(&self) -> [u8; 8] { + [0; 8] // Mock span ID + } + + fn record_error(&self, _error: &dyn std::error::Error) { + todo!() + } + + fn set_current( + &self, + _context: &Context, + ) -> Box { + todo!() + } + + /// Insert two dummy headers for distributed tracing. + // cspell: ignore traceparent tracestate + fn propagate_headers(&self, request: &mut Request) { + request.insert_header( + HeaderName::from_static("traceparent"), + "00---01", + ); + request.insert_header(HeaderName::from_static("tracestate"), "="); + } + } + + impl AsAny for MockSpan { + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + pub(super) struct InstrumentationExpectation<'a> { + pub(super) namespace: Option<&'a str>, + pub(super) name: &'a str, + pub(super) version: &'a str, + pub(super) span_name: &'a str, + pub(super) status: SpanStatus, + pub(super) kind: SpanKind, + pub(super) attributes: Vec<(&'a str, AttributeValue)>, + } + pub(super) fn check_request_instrumentation_result( + mock_tracer: Arc, + expected_span_count: usize, + span_index: usize, + expectation: InstrumentationExpectation, + ) { + assert_eq!( + mock_tracer.tracers.lock().unwrap().len(), + 1, + "Expected one tracer to be created", + ); + let tracers = mock_tracer.tracers.lock().unwrap(); + let tracer = tracers.first().unwrap(); + assert_eq!(tracer.package_name, expectation.name); + assert_eq!(tracer.package_version, expectation.version); + assert_eq!(tracer.namespace, expectation.namespace); + let spans = tracer.spans.lock().unwrap(); + assert_eq!( + spans.len(), + expected_span_count, + "Expected one span to be created" + ); + println!("Spans: {:?}", spans); + let span = spans[span_index].as_ref(); + assert_eq!(span.name, expectation.span_name); + assert_eq!(span.kind, expectation.kind); + assert_eq!(*span.state.lock().unwrap(), expectation.status); + let attributes = span.attributes.lock().unwrap(); + for attr in attributes.iter() { + println!("Attribute: {} = {:?}", attr.key, attr.value); + let mut found = false; + for (key, value) in &expectation.attributes { + if attr.key == *key { + assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); + found = true; + break; + } + } + if !found { + panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); + } + } + for (key, value) in &expectation.attributes { + if !attributes + .iter() + .any(|attr| attr.key == *key && attr.value == *value) + { + panic!("Expected attribute not found: {} = {:?}", key, value); + } + } + } +} diff --git a/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs similarity index 79% rename from sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs rename to sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs index 3a40546ef5..cd3bc882a4 100644 --- a/sdk/core/azure_core/src/http/policies/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use super::{AZ_NAMESPACE_ATTRIBUTE, ERROR_TYPE_ATTRIBUTE}; use crate::{ http::{Context, Request}, tracing::{Span, SpanKind, Tracer}, }; use std::sync::Arc; use typespec_client_core::{ + fmt::SafeDebug, http::policies::{Policy, PolicyResult}, tracing::Attribute, }; @@ -21,7 +23,7 @@ use typespec_client_core::{ /// /// If the `PublicApiInstrumentationPolicy` policy detects a `PublicApiInstrumentationInformation` in the context, /// it will create a span with the API name and any additional attributes. -#[derive(Debug)] +#[derive(SafeDebug)] pub struct PublicApiInstrumentationInformation { /// The name of the API being instrumented. /// @@ -30,6 +32,7 @@ pub struct PublicApiInstrumentationInformation { /// /// For example, if the service client is `MyClient` and the API is `my_api`, /// the API name should be `MyClient.my_api`. + #[safe(true)] pub api_name: &'static str, /// Additional attributes to be added to the span for this API. @@ -41,9 +44,6 @@ pub struct PublicApiInstrumentationInformation { pub attributes: Vec, } -const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; -const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; - /// Sets distributed tracing information for HTTP requests. #[derive(Clone, Debug)] pub(crate) struct PublicApiInstrumentationPolicy { @@ -89,51 +89,48 @@ impl Policy for PublicApiInstrumentationPolicy { // If it does, we use that information to create a span for the request. // If it doesn't, we skip instrumentation. let public_api_information = ctx.value::(); - let span: Option> = if let Some(info) = public_api_information { - // Use the public API information for instrumentation. - // If the context has a tracer (which happens when called from an instrumented method), - // we prefer the tracer from the context. - // Otherwise, we use the tracer from the policy itself. - // This allows for flexibility in using different tracers in different contexts. - let mut span_attributes = vec![]; - - for attr in &info.attributes { - // Add the attributes from the public API information to the span. - span_attributes.push(Attribute { - key: attr.key, - value: attr.value.clone(), - }); - } - - if let Some(tracer) = ctx.value::>() { - if let Some(namespace) = tracer.namespace() { - // If the tracer has a namespace, we set it as an attribute. + let span: Option> = public_api_information + .map(|info| { + // Use the public API information for instrumentation. + // If the context has a tracer (which happens when called from an instrumented method), + // we prefer the tracer from the context. + // Otherwise, we use the tracer from the policy itself. + // This allows for flexibility in using different tracers in different contexts. + let mut span_attributes = vec![]; + + for attr in &info.attributes { + // Add the attributes from the public API information to the span. span_attributes.push(Attribute { - key: AZ_NAMESPACE_ATTRIBUTE, - value: namespace.into(), + key: attr.key, + value: attr.value.clone(), }); } - // Create a span with the public API information. - Some(tracer.start_span_with_current( - info.api_name, - SpanKind::Internal, - span_attributes, - )) - } else if let Some(tracer) = &self.tracer { - // We didn't have a span from the context, but we do have a Tracer from the - // pipeline construction, so use that. - Some(tracer.start_span_with_current( - info.api_name, - SpanKind::Internal, - span_attributes, - )) - } else { - // If no tracer is available, we skip instrumentation. - None - } - } else { - None - }; + + if let Some(tracer) = ctx.value::>() { + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + span_attributes.push(Attribute { + key: AZ_NAMESPACE_ATTRIBUTE, + value: namespace.into(), + }); + } + // Create a span with the public API information. + Some(tracer.start_span_with_current( + info.api_name, + SpanKind::Internal, + span_attributes, + )) + } else { + self.tracer.as_ref().map(|tracer| { + tracer.start_span_with_current( + info.api_name, + SpanKind::Internal, + span_attributes, + ) + }) + } + }) + .unwrap(); // Now add the span to the context, so that it can be used by the next policies. let mut ctx = ctx.clone(); @@ -145,37 +142,43 @@ impl Policy for PublicApiInstrumentationPolicy { let result = next[0].send(&ctx, request, &next[1..]).await; if let Some(span) = span { - if let Err(e) = &result { - // If the request failed, we set the error type on the span. - match e.kind() { - crate::error::ErrorKind::HttpResponse { status, .. } => { - span.set_attribute(ERROR_TYPE_ATTRIBUTE, status.to_string().into()); - - // 5xx status codes SHOULD set status to Error. - // The description should not be set because it can be inferred from "http.response.status_code". - if status.is_server_error() { + match &result { + Err(e) => { + // If the request failed, we set the error type on the span. + match e.kind() { + crate::error::ErrorKind::HttpResponse { status, .. } => { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, status.to_string().into()); + + // 5xx status codes SHOULD set status to Error. + // The description should not be set because it can be inferred from "http.response.status_code". + if status.is_server_error() { + span.set_status(crate::tracing::SpanStatus::Error { + description: "".to_string(), + }); + } + } + _ => { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, e.kind().to_string().into()); span.set_status(crate::tracing::SpanStatus::Error { - description: "".to_string(), + description: e.kind().to_string(), }); } } - _ => { - span.set_attribute(ERROR_TYPE_ATTRIBUTE, e.kind().to_string().into()); + } + Ok(response) => { + // 5xx status codes SHOULD set status to Error. + // The description should not be set because it can be inferred from "http.response.status_code". + if response.status().is_server_error() { span.set_status(crate::tracing::SpanStatus::Error { - description: e.kind().to_string(), + description: "".to_string(), }); } - } - } else if let Ok(response) = &result { - // 5xx status codes SHOULD set status to Error. - // The description should not be set because it can be inferred from "http.response.status_code". - if response.status().is_server_error() { - span.set_status(crate::tracing::SpanStatus::Error { - description: "".to_string(), - }); - } - if response.status().is_client_error() || response.status().is_server_error() { - span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); + if response.status().is_client_error() || response.status().is_server_error() { + span.set_attribute( + ERROR_TYPE_ATTRIBUTE, + response.status().to_string().into(), + ); + } } } @@ -188,10 +191,10 @@ impl Policy for PublicApiInstrumentationPolicy { #[cfg(test)] mod tests { // cspell: ignore traceparent - use super::super::request_instrumentation::tests::{ + use super::*; + use crate::http::policies::instrumentation::tests::{ check_request_instrumentation_result, InstrumentationExpectation, MockTracingProvider, }; - use super::*; use crate::{ http::{ headers::Headers, diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs new file mode 100644 index 0000000000..03596c5925 --- /dev/null +++ b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs @@ -0,0 +1,501 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use super::{ + AZ_CLIENT_REQUEST_ID_ATTRIBUTE, AZ_NAMESPACE_ATTRIBUTE, AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + ERROR_TYPE_ATTRIBUTE, HTTP_REQUEST_METHOD_ATTRIBUTE, HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, SERVER_ADDRESS_ATTRIBUTE, SERVER_PORT_ATTRIBUTE, + URL_FULL_ATTRIBUTE, +}; +use crate::{ + http::{headers, Context, Request}, + tracing::{Span, SpanKind}, +}; +use std::sync::Arc; +use typespec_client_core::{ + http::policies::{Policy, PolicyResult, RetryPolicyCount}, + tracing::Attribute, +}; + +/// Sets distributed tracing information for HTTP requests. +#[derive(Clone, Debug)] +pub(crate) struct RequestInstrumentationPolicy { + tracer: Option>, +} + +impl RequestInstrumentationPolicy { + /// Creates a new `RequestInstrumentationPolicy`. + /// + /// # Arguments + /// - `azure_namespace`: The Azure namespace for the tracer. + /// - `crate_name`: The name of the crate for which the tracer is created. + /// - `crate_version`: The version of the crate for which the tracer is created. + /// - `options`: Options for request instrumentation, including the tracing provider. + /// + /// # Returns + /// A new instance of `RequestInstrumentationPolicy`. + /// + /// # Note + /// This policy will only create a tracer if a tracing provider is provided in the options. + /// + /// This policy will create a tracer that can be used to instrument HTTP requests. + /// However this tracer is only used when the client method is NOT instrumented. + /// A part of the client method instrumentation sets a client-specific tracer into the + /// request `[Context]` which will be used instead of the tracer from this policy. + /// + pub fn new( + tracer: Option>, + // azure_namespace: Option<&'static str>, + // crate_name: Option<&'static str>, + // crate_version: Option<&'static str>, + // options: &RequestInstrumentationOptions, + ) -> Self { + Self { tracer } + // if let Some(tracing_provider) = &options.tracing_provider { + // Self { + // tracer: Some(tracing_provider.get_tracer( + // azure_namespace, + // crate_name.unwrap_or("unknown"), + // crate_version.unwrap_or("unknown"), + // )), + // } + // } else { + // Self { tracer: None } + // } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Policy for RequestInstrumentationPolicy { + async fn send( + &self, + ctx: &Context, + request: &mut Request, + next: &[Arc], + ) -> PolicyResult { + // If the context has a tracer (which happens when called from an instrumented method), + // we prefer the tracer from the context. + // Otherwise, we use the tracer from the policy itself. + // This allows for flexibility in using different tracers in different contexts. + let tracer = if ctx.value::>().is_some() { + ctx.value::>() + } else { + self.tracer.as_ref() + }; + + // If there is a span in the context, if it's not recording, just forward the request + // without instrumentation. + if let Some(span) = ctx.value::>() { + if !span.is_recording() { + // If the span is not recording, we skip instrumentation. + return next[0].send(ctx, request, &next[1..]).await; + } + } + + let Some(tracer) = tracer else { + return next[0].send(ctx, request, &next[1..]).await; + }; + let mut span_attributes = vec![Attribute { + key: HTTP_REQUEST_METHOD_ATTRIBUTE, + value: request.method().to_string().into(), + }]; + + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + span_attributes.push(Attribute { + key: AZ_NAMESPACE_ATTRIBUTE, + value: namespace.into(), + }); + } + + // OpenTelemetry requires that we sanitize the URL if it contains a username or password. + // Since a valid Azure SDK endpoint should never contain a username or password, if + // the url contains a username or password, we simply omit the URL_FULL_ATTRIBUTE. + if request.url().username().is_empty() && request.url().password().is_none() { + span_attributes.push(Attribute { + key: URL_FULL_ATTRIBUTE, + value: request.url().to_string().into(), + }); + } + + if let Some(host) = request.url().host() { + span_attributes.push(Attribute { + key: SERVER_ADDRESS_ATTRIBUTE, + value: host.to_string().into(), + }); + } + if let Some(port) = request.url().port_or_known_default() { + span_attributes.push(Attribute { + key: SERVER_PORT_ATTRIBUTE, + value: port.into(), + }); + } + // Get the method as a string to avoid lifetime issues + // let method_str = request.method_as_str(); + let method_str = request.method().as_str(); + let span = if let Some(parent_span) = ctx.value::>() { + // If a parent span exists, start a new span with the parent. + tracer.start_span_with_parent( + method_str, + SpanKind::Client, + span_attributes, + parent_span.clone(), + ) + } else { + // If no parent span exists, start a new span without a parent. + tracer.start_span_with_current(method_str, SpanKind::Client, span_attributes) + }; + + if !span.is_recording() { + // If the span is not recording, we skip instrumentation. + return next[0].send(ctx, request, &next[1..]).await; + } + + if let Some(client_request_id) = request + .headers() + .get_optional_str(&headers::CLIENT_REQUEST_ID) + { + span.set_attribute(AZ_CLIENT_REQUEST_ID_ATTRIBUTE, client_request_id.into()); + } + + if let Some(service_request_id) = request.headers().get_optional_str(&headers::REQUEST_ID) { + span.set_attribute(AZ_SERVICE_REQUEST_ID_ATTRIBUTE, service_request_id.into()); + } + + if let Some(retry_count) = ctx.value::() { + span.set_attribute(HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, (**retry_count).into()); + } + + // Propagate the headers for distributed tracing into the request. + span.propagate_headers(request); + + let result = next[0].send(ctx, request, &next[1..]).await; + + if let Some(err) = result.as_ref().err() { + // If the request failed, set an error type attribute. + let azure_error = err.downcast_ref::(); + if let Some(err_kind) = azure_error.map(|e| e.kind()) { + // If the error is an Azure core error, we set the error type. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, err_kind.to_string().into()); + } else { + // Otherwise, we set the error type to the error's text. This should never happen + // as the error should be an Azure core error. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, err.to_string().into()); + } + } + if let Ok(response) = result.as_ref() { + // If the request was successful, set the HTTP response status code. + span.set_attribute( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + u16::from(response.status()).into(), + ); + + if response.status().is_server_error() || response.status().is_client_error() { + // If the response status indicates an error, set the span status to error. + // Since the reason can be inferred from the status code, description is left empty. + span.set_status(crate::tracing::SpanStatus::Error { + description: "".to_string(), + }); + // Set the error type attribute for all HTTP 4XX or 5XX errors. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); + } + } + + span.end(); + return result; + } +} +#[cfg(test)] +pub(crate) mod tests { + // cspell: ignore traceparent + use super::*; + use crate::{ + http::{ + headers::Headers, + policies::{ + instrumentation::tests::{ + check_request_instrumentation_result, InstrumentationExpectation, + MockTracingProvider, + }, + TransportPolicy, + }, + Method, RawResponse, StatusCode, TransportOptions, + }, + tracing::{AttributeValue, SpanStatus, TracerProvider}, + Result, + }; + use azure_core_test::http::MockHttpClient; + use futures::future::BoxFuture; + use std::sync::Arc; + use typespec_client_core::http::headers::HeaderName; + + async fn run_instrumentation_test( + test_namespace: Option<&'static str>, + crate_name: Option<&'static str>, + version: Option<&'static str>, + request: &mut Request, + callback: C, + ) -> Arc + where + C: FnMut(&Request) -> BoxFuture<'_, Result> + Send + Sync + 'static, + { + let mock_tracer_provider = Arc::new(MockTracingProvider::new()); + let tracer = mock_tracer_provider.get_tracer( + test_namespace, + crate_name.unwrap_or("unknown"), + version.unwrap_or("unknown"), + ); + let policy = Arc::new(RequestInstrumentationPolicy::new(Some(tracer.clone()))); + + let transport = TransportPolicy::new(TransportOptions::new(Arc::new(MockHttpClient::new( + callback, + )))); + + let ctx = Context::default(); + let next: Vec> = vec![Arc::new(transport)]; + let _result = policy.send(&ctx, request, &next).await; + + mock_tracer_provider + } + + #[tokio::test] + async fn simple_instrumentation_policy() { + let url = "http://example.com/path"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer = run_instrumentation_test( + Some("test namespace"), + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; + + check_request_instrumentation_result( + mock_tracer, + 1, + 0, + InstrumentationExpectation { + namespace: Some("test namespace"), + name: "test_crate", + version: "1.0.0", + span_name: "GET", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(80)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("http://example.com/path"), + ), + ], + }, + ); + } + + #[tokio::test] + async fn client_request_id() { + let url = "https://example.com/client_request_id"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + request.insert_header(headers::CLIENT_REQUEST_ID, "test-client-request-id"); + + let mock_tracer = run_instrumentation_test( + None, + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + assert_eq!( + req.headers() + .get_optional_str(&HeaderName::from_static("traceparent")), + Some("00---01") + ); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; + + check_request_instrumentation_result( + mock_tracer.clone(), + 1, + 0, + InstrumentationExpectation { + namespace: None, + name: "test_crate", + version: "1.0.0", + span_name: "GET", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + ( + AZ_CLIENT_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-client-request-id"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://example.com/client_request_id"), + ), + ], + }, + ); + } + + #[tokio::test] + async fn test_url_with_password() { + let url = "https://user:password@host:8080/path?query=value#fragment"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer_provider = + run_instrumentation_test(None, None, None, &mut request, |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("host")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }) + .await; + check_request_instrumentation_result( + mock_tracer_provider, + 1, + 0, + InstrumentationExpectation { + namespace: None, + name: "unknown", + version: "unknown", + span_name: "GET", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + (SERVER_ADDRESS_ATTRIBUTE, AttributeValue::from("host")), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(8080)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://host:8080/path?query=value#fragment"), + ), + ], + }, + ); + } + + #[tokio::test] + async fn request_failed() { + let url = "https://microsoft.com/request_failed.htm"; + let mut request = Request::new(url.parse().unwrap(), Method::Put); + request.insert_header(headers::REQUEST_ID, "test-service-request-id"); + + let mock_tracer = run_instrumentation_test( + Some("test namespace"), + Some("test_crate"), + Some("1.0.0"), + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("microsoft.com")); + assert_eq!(req.method(), &Method::Put); + Ok(RawResponse::from_bytes( + StatusCode::NotFound, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; + check_request_instrumentation_result( + mock_tracer, + 1, + 0, + InstrumentationExpectation { + namespace: Some("test namespace"), + name: "test_crate", + version: "1.0.0", + span_name: "PUT", + status: SpanStatus::Error { + description: "".to_string(), + }, + kind: SpanKind::Client, + attributes: vec![ + (ERROR_TYPE_ATTRIBUTE, AttributeValue::from("404")), + ( + AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-service-request-id"), + ), + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), + ( + AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-service-request-id"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(404), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("PUT")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("microsoft.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://microsoft.com/request_failed.htm"), + ), + ], + }, + ); + } +} diff --git a/sdk/core/azure_core/src/http/policies/mod.rs b/sdk/core/azure_core/src/http/policies/mod.rs index 0d4439d285..8625b69946 100644 --- a/sdk/core/azure_core/src/http/policies/mod.rs +++ b/sdk/core/azure_core/src/http/policies/mod.rs @@ -5,14 +5,11 @@ mod bearer_token_policy; mod client_request_id; -mod public_api_instrumentation; -mod request_instrumentation; +mod instrumentation; mod user_agent; pub use bearer_token_policy::BearerTokenCredentialPolicy; pub use client_request_id::*; -pub use public_api_instrumentation::PublicApiInstrumentationInformation; -pub(crate) use public_api_instrumentation::PublicApiInstrumentationPolicy; -pub(crate) use request_instrumentation::*; +pub use instrumentation::*; pub use typespec_client_core::http::policies::*; pub use user_agent::*; diff --git a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/request_instrumentation.rs deleted file mode 100644 index 10796aa277..0000000000 --- a/sdk/core/azure_core/src/http/policies/request_instrumentation.rs +++ /dev/null @@ -1,744 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -use crate::{ - http::{headers, Context, Request}, - tracing::{Span, SpanKind}, -}; -use std::sync::Arc; -use typespec_client_core::{ - http::policies::{Policy, PolicyResult, RetryPolicyCount}, - tracing::Attribute, -}; - -const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; -const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client.request.id"; -const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; -const AZ_SERVICE_REQUEST_ID_ATTRIBUTE: &str = "az.service_request.id"; -const HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE: &str = "http.request.resend_count"; -const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE: &str = "http.response.status_code"; -const HTTP_REQUEST_METHOD_ATTRIBUTE: &str = "http.request.method"; -const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; -const SERVER_PORT_ATTRIBUTE: &str = "server.port"; -const URL_FULL_ATTRIBUTE: &str = "url.full"; - -/// Sets distributed tracing information for HTTP requests. -#[derive(Clone, Debug)] -pub(crate) struct RequestInstrumentationPolicy { - tracer: Option>, -} - -impl RequestInstrumentationPolicy { - /// Creates a new `RequestInstrumentationPolicy`. - /// - /// # Arguments - /// - `azure_namespace`: The Azure namespace for the tracer. - /// - `crate_name`: The name of the crate for which the tracer is created. - /// - `crate_version`: The version of the crate for which the tracer is created. - /// - `options`: Options for request instrumentation, including the tracing provider. - /// - /// # Returns - /// A new instance of `RequestInstrumentationPolicy`. - /// - /// # Note - /// This policy will only create a tracer if a tracing provider is provided in the options. - /// - /// This policy will create a tracer that can be used to instrument HTTP requests. - /// However this tracer is only used when the client method is NOT instrumented. - /// A part of the client method instrumentation sets a client-specific tracer into the - /// request `[Context]` which will be used instead of the tracer from this policy. - /// - pub fn new( - tracer: Option>, - // azure_namespace: Option<&'static str>, - // crate_name: Option<&'static str>, - // crate_version: Option<&'static str>, - // options: &RequestInstrumentationOptions, - ) -> Self { - Self { tracer } - // if let Some(tracing_provider) = &options.tracing_provider { - // Self { - // tracer: Some(tracing_provider.get_tracer( - // azure_namespace, - // crate_name.unwrap_or("unknown"), - // crate_version.unwrap_or("unknown"), - // )), - // } - // } else { - // Self { tracer: None } - // } - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -impl Policy for RequestInstrumentationPolicy { - async fn send( - &self, - ctx: &Context, - request: &mut Request, - next: &[Arc], - ) -> PolicyResult { - // If the context has a tracer (which happens when called from an instrumented method), - // we prefer the tracer from the context. - // Otherwise, we use the tracer from the policy itself. - // This allows for flexibility in using different tracers in different contexts. - let tracer = if ctx.value::>().is_some() { - ctx.value::>() - } else { - self.tracer.as_ref() - }; - - // If there is a span in the context, if it's not recording, just forward the request - // without instrumentation. - if let Some(span) = ctx.value::>() { - if !span.is_recording() { - // If the span is not recording, we skip instrumentation. - return next[0].send(ctx, request, &next[1..]).await; - } - } - - if let Some(tracer) = tracer { - let mut span_attributes = vec![Attribute { - key: HTTP_REQUEST_METHOD_ATTRIBUTE, - value: request.method().to_string().into(), - }]; - - if let Some(namespace) = tracer.namespace() { - // If the tracer has a namespace, we set it as an attribute. - span_attributes.push(Attribute { - key: AZ_NAMESPACE_ATTRIBUTE, - value: namespace.into(), - }); - } - - if !request.url().username().is_empty() || request.url().password().is_some() { - // If the URL contains a password, we do not log it for security reasons. - let full_url = format!( - "{}://{}{}{}{}{}", - request.url().scheme(), - request - .url() - .host() - .map_or_else(|| "unknown_host".to_string(), |h| h.to_string()), - request - .url() - .port() - .map_or_else(String::new, |p| format!(":{}", p)), - request.url().path(), - request - .url() - .query() - .map_or_else(String::new, |q| format!("?{}", q)), - request - .url() - .fragment() - .map_or_else(String::new, |f| format!("#{}", f)), - ); - span_attributes.push(Attribute { - key: URL_FULL_ATTRIBUTE, - value: full_url.into(), - }); - } else { - // If no password is present, we log the full URL. - span_attributes.push(Attribute { - key: URL_FULL_ATTRIBUTE, - value: request.url().to_string().into(), - }); - } - - if let Some(host) = request.url().host() { - span_attributes.push(Attribute { - key: SERVER_ADDRESS_ATTRIBUTE, - value: host.to_string().into(), - }); - } - if let Some(port) = request.url().port_or_known_default() { - span_attributes.push(Attribute { - key: SERVER_PORT_ATTRIBUTE, - value: port.into(), - }); - } - // Get the method as a string to avoid lifetime issues - // let method_str = request.method_as_str(); - let method_str = request.method().as_str(); - let span = if let Some(parent_span) = ctx.value::>() { - // If a parent span exists, start a new span with the parent. - tracer.start_span_with_parent( - method_str, - SpanKind::Client, - span_attributes, - parent_span.clone(), - ) - } else { - // If no parent span exists, start a new span without a parent. - tracer.start_span_with_current(method_str, SpanKind::Client, span_attributes) - }; - - if !span.is_recording() { - // If the span is not recording, we skip instrumentation. - return next[0].send(ctx, request, &next[1..]).await; - } - - if let Some(client_request_id) = request - .headers() - .get_optional_str(&headers::CLIENT_REQUEST_ID) - { - span.set_attribute(AZ_CLIENT_REQUEST_ID_ATTRIBUTE, client_request_id.into()); - } - - if let Some(service_request_id) = - request.headers().get_optional_str(&headers::REQUEST_ID) - { - span.set_attribute(AZ_SERVICE_REQUEST_ID_ATTRIBUTE, service_request_id.into()); - } - - if let Some(retry_count) = ctx.value::() { - span.set_attribute(HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, retry_count.0.into()); - } - - // Propagate the headers for distributed tracing into the request. - span.propagate_headers(request); - - let result = next[0].send(ctx, request, &next[1..]).await; - - if let Some(err) = result.as_ref().err() { - // If the request failed, set an error type attribute. - let azure_error = err.downcast_ref::(); - if let Some(err_kind) = azure_error.map(|e| e.kind()) { - // If the error is an Azure core error, we set the error type. - span.set_attribute(ERROR_TYPE_ATTRIBUTE, err_kind.to_string().into()); - } else { - // Otherwise, we set the error type to the error's text. This should never happen - // as the error should be an Azure core error. - span.set_attribute(ERROR_TYPE_ATTRIBUTE, err.to_string().into()); - } - } - if let Ok(response) = result.as_ref() { - // If the request was successful, set the HTTP response status code. - span.set_attribute( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - u16::from(response.status()).into(), - ); - - if response.status().is_server_error() || response.status().is_client_error() { - // If the response status indicates an error, set the span status to error. - // Since the reason can be inferred from the status code, description is left empty. - span.set_status(crate::tracing::SpanStatus::Error { - description: "".to_string(), - }); - // Set the error type attribute for all HTTP 4XX or 5XX errors. - span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); - } - } - - span.end(); - return result; - } else { - // If no tracer is set, we simply forward the request without instrumentation. - next[0].send(ctx, request, &next[1..]).await - } - } -} -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::{ - http::{ - headers::Headers, policies::TransportPolicy, Method, RawResponse, StatusCode, - TransportOptions, - }, - tracing::{AsAny, AttributeValue, Span, SpanStatus, Tracer, TracerProvider}, - Result, - }; - use azure_core_test::http::MockHttpClient; - use futures::future::BoxFuture; - use std::sync::{Arc, Mutex}; - use typespec_client_core::http::headers::HeaderName; - - #[derive(Debug)] - pub(crate) struct MockTracingProvider { - pub(crate) tracers: Mutex>>, - } - - impl MockTracingProvider { - pub(crate) fn new() -> Self { - Self { - tracers: Mutex::new(Vec::new()), - } - } - } - impl TracerProvider for MockTracingProvider { - fn get_tracer( - &self, - azure_namespace: Option<&'static str>, - crate_name: &'static str, - crate_version: &'static str, - ) -> Arc { - let mut tracers = self.tracers.lock().unwrap(); - let tracer = Arc::new(MockTracer { - namespace: azure_namespace, - package_name: crate_name, - package_version: crate_version, - spans: Mutex::new(Vec::new()), - }); - - tracers.push(tracer.clone()); - tracer - } - } - - #[derive(Debug)] - pub(crate) struct MockTracer { - pub(crate) namespace: Option<&'static str>, - pub(crate) package_name: &'static str, - pub(crate) package_version: &'static str, - pub(crate) spans: Mutex>>, - } - - impl Tracer for MockTracer { - fn namespace(&self) -> Option<&'static str> { - self.namespace - } - - fn start_span_with_current( - &self, - name: &str, - kind: SpanKind, - attributes: Vec, - ) -> Arc { - let span = Arc::new(MockSpan::new(name, kind, attributes)); - self.spans.lock().unwrap().push(span.clone()); - span - } - - fn start_span_with_parent( - &self, - name: &str, - kind: SpanKind, - attributes: Vec, - _parent: Arc, - ) -> Arc { - let span = Arc::new(MockSpan::new(name, kind, attributes)); - self.spans.lock().unwrap().push(span.clone()); - span - } - - fn start_span( - &self, - name: &str, - kind: SpanKind, - attributes: Vec, - ) -> Arc { - let span = Arc::new(MockSpan::new(name, kind, attributes)); - self.spans.lock().unwrap().push(span.clone()); - span - } - } - - #[derive(Debug)] - pub(crate) struct MockSpan { - pub(crate) name: String, - pub(crate) kind: SpanKind, - pub(crate) attributes: Mutex>, - pub(crate) state: Mutex, - pub(crate) is_open: Mutex, - } - impl MockSpan { - fn new(name: &str, kind: SpanKind, attributes: Vec) -> Self { - println!("Creating MockSpan: {}", name); - println!("Attributes: {:?}", attributes); - Self { - name: name.to_string(), - kind, - attributes: Mutex::new(attributes), - state: Mutex::new(SpanStatus::Unset), - is_open: Mutex::new(true), - } - } - } - - impl Span for MockSpan { - fn set_attribute(&self, key: &'static str, value: AttributeValue) { - println!("{}: Setting attribute {}: {:?}", self.name, key, value); - let mut attributes = self.attributes.lock().unwrap(); - attributes.push(Attribute { key, value }); - } - - fn set_status(&self, status: crate::tracing::SpanStatus) { - println!("{}: Setting span status: {:?}", self.name, status); - let mut state = self.state.lock().unwrap(); - *state = status; - } - - fn end(&self) { - println!("Ending span: {}", self.name); - let mut is_open = self.is_open.lock().unwrap(); - *is_open = false; - } - - fn is_recording(&self) -> bool { - true - } - - fn span_id(&self) -> [u8; 8] { - [0; 8] // Mock span ID - } - - fn record_error(&self, _error: &dyn std::error::Error) { - todo!() - } - - fn set_current( - &self, - _context: &Context, - ) -> Box { - todo!() - } - - /// Insert two dummy headers for distributed tracing. - // cspell: ignore traceparent tracestate - fn propagate_headers(&self, request: &mut Request) { - request.insert_header( - HeaderName::from_static("traceparent"), - "00---01", - ); - request.insert_header(HeaderName::from_static("tracestate"), "="); - } - } - - impl AsAny for MockSpan { - fn as_any(&self) -> &dyn std::any::Any { - self - } - } - - async fn run_instrumentation_test( - test_namespace: Option<&'static str>, - crate_name: Option<&'static str>, - version: Option<&'static str>, - request: &mut Request, - callback: C, - ) -> Arc - where - C: FnMut(&Request) -> BoxFuture<'_, Result> + Send + Sync + 'static, - { - let mock_tracer_provider = Arc::new(MockTracingProvider::new()); - let tracer = mock_tracer_provider.get_tracer( - test_namespace, - crate_name.unwrap_or("unknown"), - version.unwrap_or("unknown"), - ); - let policy = Arc::new(RequestInstrumentationPolicy::new(Some(tracer.clone()))); - - let transport = TransportPolicy::new(TransportOptions::new(Arc::new(MockHttpClient::new( - callback, - )))); - - let ctx = Context::default(); - let next: Vec> = vec![Arc::new(transport)]; - let _result = policy.send(&ctx, request, &next).await; - - mock_tracer_provider - } - pub(crate) struct InstrumentationExpectation<'a> { - pub(crate) namespace: Option<&'a str>, - pub(crate) name: &'a str, - pub(crate) version: &'a str, - pub(crate) span_name: &'a str, - pub(crate) status: SpanStatus, - pub(crate) kind: SpanKind, - pub(crate) attributes: Vec<(&'a str, AttributeValue)>, - } - pub(crate) fn check_request_instrumentation_result( - mock_tracer: Arc, - expected_span_count: usize, - span_index: usize, - expectation: InstrumentationExpectation, - ) { - assert_eq!( - mock_tracer.tracers.lock().unwrap().len(), - 1, - "Expected one tracer to be created", - ); - let tracers = mock_tracer.tracers.lock().unwrap(); - let tracer = tracers.first().unwrap(); - assert_eq!(tracer.package_name, expectation.name); - assert_eq!(tracer.package_version, expectation.version); - assert_eq!(tracer.namespace, expectation.namespace); - let spans = tracer.spans.lock().unwrap(); - assert_eq!( - spans.len(), - expected_span_count, - "Expected one span to be created" - ); - println!("Spans: {:?}", spans); - let span = spans[span_index].as_ref(); - assert_eq!(span.name, expectation.span_name); - assert_eq!(span.kind, expectation.kind); - assert_eq!(*span.state.lock().unwrap(), expectation.status); - let attributes = span.attributes.lock().unwrap(); - for attr in attributes.iter() { - println!("Attribute: {} = {:?}", attr.key, attr.value); - let mut found = false; - for (key, value) in &expectation.attributes { - if attr.key == *key { - assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); - found = true; - break; - } - } - if !found { - panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); - } - } - for (key, value) in &expectation.attributes { - if !attributes - .iter() - .any(|attr| attr.key == *key && attr.value == *value) - { - panic!("Expected attribute not found: {} = {:?}", key, value); - } - } - } - - #[tokio::test] - async fn simple_instrumentation_policy() { - let url = "http://example.com/path"; - let mut request = Request::new(url.parse().unwrap(), Method::Get); - - let mock_tracer = run_instrumentation_test( - Some("test namespace"), - Some("test_crate"), - Some("1.0.0"), - &mut request, - |req| { - Box::pin(async move { - assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Get); - Ok(RawResponse::from_bytes( - StatusCode::Ok, - Headers::new(), - vec![], - )) - }) - }, - ) - .await; - - check_request_instrumentation_result( - mock_tracer, - 1, - 0, - InstrumentationExpectation { - namespace: Some("test namespace"), - name: "test_crate", - version: "1.0.0", - span_name: "GET", - status: SpanStatus::Unset, - kind: SpanKind::Client, - attributes: vec![ - ( - AZ_NAMESPACE_ATTRIBUTE, - AttributeValue::from("test namespace"), - ), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(200), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), - ( - SERVER_ADDRESS_ATTRIBUTE, - AttributeValue::from("example.com"), - ), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(80)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("http://example.com/path"), - ), - ], - }, - ); - } - - #[tokio::test] - async fn client_request_id() { - let url = "https://example.com/client_request_id"; - let mut request = Request::new(url.parse().unwrap(), Method::Get); - request.insert_header(headers::CLIENT_REQUEST_ID, "test-client-request-id"); - - let mock_tracer = run_instrumentation_test( - None, - Some("test_crate"), - Some("1.0.0"), - &mut request, - |req| { - Box::pin(async move { - assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Get); - assert_eq!( - req.headers() - .get_optional_str(&HeaderName::from_static("traceparent")), - Some("00---01") - ); - Ok(RawResponse::from_bytes( - StatusCode::Ok, - Headers::new(), - vec![], - )) - }) - }, - ) - .await; - - check_request_instrumentation_result( - mock_tracer.clone(), - 1, - 0, - InstrumentationExpectation { - namespace: None, - name: "test_crate", - version: "1.0.0", - span_name: "GET", - status: SpanStatus::Unset, - kind: SpanKind::Client, - attributes: vec![ - ( - AZ_CLIENT_REQUEST_ID_ATTRIBUTE, - AttributeValue::from("test-client-request-id"), - ), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(200), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), - ( - SERVER_ADDRESS_ATTRIBUTE, - AttributeValue::from("example.com"), - ), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("https://example.com/client_request_id"), - ), - ], - }, - ); - } - - #[tokio::test] - async fn test_url_with_password() { - let url = "https://user:password@host:8080/path?query=value#fragment"; - let mut request = Request::new(url.parse().unwrap(), Method::Get); - - let mock_tracer_provider = - run_instrumentation_test(None, None, None, &mut request, |req| { - Box::pin(async move { - assert_eq!(req.url().host_str(), Some("host")); - assert_eq!(req.method(), &Method::Get); - Ok(RawResponse::from_bytes( - StatusCode::Ok, - Headers::new(), - vec![], - )) - }) - }) - .await; - check_request_instrumentation_result( - mock_tracer_provider, - 1, - 0, - InstrumentationExpectation { - namespace: None, - name: "unknown", - version: "unknown", - span_name: "GET", - status: SpanStatus::Unset, - kind: SpanKind::Client, - attributes: vec![ - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(200), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), - (SERVER_ADDRESS_ATTRIBUTE, AttributeValue::from("host")), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(8080)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("https://host:8080/path?query=value#fragment"), - ), - ], - }, - ); - } - - #[tokio::test] - async fn request_failed() { - let url = "https://microsoft.com/request_failed.htm"; - let mut request = Request::new(url.parse().unwrap(), Method::Put); - request.insert_header(headers::REQUEST_ID, "test-service-request-id"); - - let mock_tracer = run_instrumentation_test( - Some("test namespace"), - Some("test_crate"), - Some("1.0.0"), - &mut request, - |req| { - Box::pin(async move { - assert_eq!(req.url().host_str(), Some("microsoft.com")); - assert_eq!(req.method(), &Method::Put); - Ok(RawResponse::from_bytes( - StatusCode::NotFound, - Headers::new(), - vec![], - )) - }) - }, - ) - .await; - check_request_instrumentation_result( - mock_tracer, - 1, - 0, - InstrumentationExpectation { - namespace: Some("test namespace"), - name: "test_crate", - version: "1.0.0", - span_name: "PUT", - status: SpanStatus::Error { - description: "".to_string(), - }, - kind: SpanKind::Client, - attributes: vec![ - (ERROR_TYPE_ATTRIBUTE, AttributeValue::from("404")), - ( - AZ_SERVICE_REQUEST_ID_ATTRIBUTE, - AttributeValue::from("test-service-request-id"), - ), - ( - AZ_NAMESPACE_ATTRIBUTE, - AttributeValue::from("test namespace"), - ), - ( - AZ_SERVICE_REQUEST_ID_ATTRIBUTE, - AttributeValue::from("test-service-request-id"), - ), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(404), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("PUT")), - ( - SERVER_ADDRESS_ATTRIBUTE, - AttributeValue::from("microsoft.com"), - ), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("https://microsoft.com/request_failed.htm"), - ), - ], - }, - ); - } -} diff --git a/sdk/core/azure_core/src/lib.rs b/sdk/core/azure_core/src/lib.rs index 6f68543bf2..b9120eb212 100644 --- a/sdk/core/azure_core/src/lib.rs +++ b/sdk/core/azure_core/src/lib.rs @@ -27,6 +27,7 @@ pub use typespec_client_core::{ fmt, json, sleep, stream, time, Bytes, Uuid, }; +/// Abstractions for distributed tracing and telemetry. pub mod tracing { pub use crate::http::policies::PublicApiInstrumentationInformation; pub use typespec_client_core::tracing::*; diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index a87a8db7f9..d555d08c2c 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -1,14 +1,14 @@ # Azure Core OpenTelemetry Tracing -This crate provides OpenTelemetry distributed tracing support for the Azure SDK for Rust. -It bridges the standardized azure_core tracing traits with OpenTelemetry implementation, +This crate provides [OpenTelemetry](https://opentelemetry.io) distributed tracing support for the Azure SDK for Rust. +It bridges the standardized `azure_core` tracing traits with the OpenTelemetry for Rust implementation, enabling automatic span creation, context propagation, and telemetry collection for Azure services. It allows Rust applications which use the [OpenTelemetry](https://opentelemetry.io/) APIs to generate OpenTelemetry spans for Azure SDK for Rust Clients. ## OpenTelemetry integration with the Azure SDK for Rust -To integrate the OpenTelemetry APIs with the Azure SDK for Rust, you create a [`OpenTelemetryTracerProvider`] and pass it into your SDK ClientOptions. +To integrate the OpenTelemetry APIs with the Azure SDK for Rust, you create a `OpenTelemetryTracerProvider` and pass it into your SDK ClientOptions. ```rust no_run # use azure_identity::DefaultAzureCredential; @@ -41,7 +41,7 @@ let options = ServiceClientOptions { # } ``` -If it is more convenient to use the global OpenTelemetry provider, then the [`OpenTelemetryTracerProvider::new_from_global_provider`] method will configure the OpenTelemetry support to use the global provider instead of a custom configured provider. +If it is more convenient to use the global OpenTelemetry provider, then the `OpenTelemetryTracerProvider::new_from_global_provider` method will configure the OpenTelemetry support to use the global provider instead of a custom configured provider. ```rust no_run # use azure_identity::DefaultAzureCredential; @@ -73,3 +73,34 @@ let options = ServiceClientOptions { ``` Once the `OpenTelemetryTracerProvider` is integrated with the Azure Service ClientOptions, the Azure SDK will be configured to capture per-API and per-HTTP operation tracing options, and the HTTP requests will be annotated with [W3C Trace Context headers](https://www.w3.org/TR/trace-context/). + +# Troubleshooting + +## General + +## Logging + +# Contributing + +See the [CONTRIBUTING.md] for details on building, testing, and contributing to these libraries. + +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit . + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct]. For more information see the [Code of Conduct FAQ] or contact with any additional questions or comments. + +## Reporting security issues and security bugs + +Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) . You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the MSRC PGP key, can be found in the [Security TechCenter](https://www.microsoft.com/msrc/faqs-report-an-issue). + +## License + +Azure SDK for Rust is licensed under the [MIT](https://github.com/Azure/azure-sdk-for-cpp/blob/main/LICENSE.txt) license. + + + +[Microsoft Open Source Code of Conduct]: https://opensource.microsoft.com/codeofconduct/ +[Cargo]: https://crates.io/ +[CONTRIBUTING.md]: https://github.com/Azure/azure-sdk-for-rust/blob/main/CONTRIBUTING.md +[Code of Conduct FAQ]: https://opensource.microsoft.com/codeofconduct/faq/ diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index a864f615f6..2c1137f26a 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -90,7 +90,7 @@ impl Span for OpenTelemetrySpan { // We then insert each of the headers from the OpenTelemetry header map into the // Request's header map. - for (key, value) in header_map.into_iter() { + for (key, value) in header_map { // Note: The OpenTelemetry HeaderInjector will always produce unique header names, so we don't need to // handle the multiple headers case here. diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index 12127a43ee..c085028fec 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -7,7 +7,7 @@ use opentelemetry::{ global::{BoxedTracer, ObjectSafeTracerProvider}, InstrumentationScope, }; -use std::sync::Arc; +use std::{fmt::Debug, sync::Arc}; /// Enum to hold different OpenTelemetry tracer provider implementations. pub struct OpenTelemetryTracerProvider { @@ -44,15 +44,22 @@ impl OpenTelemetryTracerProvider { } } +impl Debug for OpenTelemetryTracerProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OpenTelemetryTracerProvider") + .finish_non_exhaustive() + } +} + impl TracerProvider for OpenTelemetryTracerProvider { fn get_tracer( &self, namespace: Option<&'static str>, - package_name: &'static str, - package_version: &'static str, + crate_name: &'static str, + crate_version: &'static str, ) -> Arc { - let scope = InstrumentationScope::builder(package_name) - .with_version(package_version) + let scope = InstrumentationScope::builder(crate_name) + .with_version(crate_version) .with_schema_url("https://opentelemetry.io/schemas/1.23.0") .build(); if let Some(provider) = &self.inner { diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 9051643534..d6b14c3b9a 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -12,7 +12,7 @@ use opentelemetry::{ trace::{TraceContextExt, Tracer as OpenTelemetryTracerTrait}, Context, KeyValue, }; -use std::sync::Arc; +use std::{fmt::Debug, sync::Arc}; pub struct OpenTelemetryTracer { namespace: Option<&'static str>, @@ -29,6 +29,14 @@ impl OpenTelemetryTracer { } } +impl Debug for OpenTelemetryTracer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OpenTelemetryTracer") + .field("namespace", &self.namespace) + .finish_non_exhaustive() + } +} + impl Tracer for OpenTelemetryTracer { fn namespace(&self) -> Option<&'static str> { self.namespace @@ -44,8 +52,8 @@ impl Tracer for OpenTelemetryTracer { .with_kind(OpenTelemetrySpanKind(kind).into()) .with_attributes( attributes - .into_iter() - .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr))), + .iter() + .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr.clone()))), ); let context = Context::new(); let span = self.inner.build_with_context(span_builder, &context); @@ -63,8 +71,8 @@ impl Tracer for OpenTelemetryTracer { .with_kind(OpenTelemetrySpanKind(kind).into()) .with_attributes( attributes - .into_iter() - .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr))), + .iter() + .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr.clone()))), ); let context = Context::current(); let span = self.inner.build_with_context(span_builder, &context); @@ -83,8 +91,8 @@ impl Tracer for OpenTelemetryTracer { .with_kind(OpenTelemetrySpanKind(kind).into()) .with_attributes( attributes - .into_iter() - .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr))), + .iter() + .map(|attr| KeyValue::from(OpenTelemetryAttribute(attr.clone()))), ); // Cast the parent span to Any type diff --git a/sdk/core/azure_core_opentelemetry/tests/integration_test.rs b/sdk/core/azure_core_opentelemetry/tests/otel_span_tests.rs similarity index 100% rename from sdk/core/azure_core_opentelemetry/tests/integration_test.rs rename to sdk/core/azure_core_opentelemetry/tests/otel_span_tests.rs diff --git a/sdk/typespec/typespec_client_core/src/http/method.rs b/sdk/typespec/typespec_client_core/src/http/method.rs index 40b559b691..30f4f362a3 100644 --- a/sdk/typespec/typespec_client_core/src/http/method.rs +++ b/sdk/typespec/typespec_client_core/src/http/method.rs @@ -112,7 +112,7 @@ impl Method { } /// Returns the HTTP method as a static string slice. - pub fn as_str(&self) -> &'static str { + pub const fn as_str(&self) -> &'static str { match self { Method::Delete => "DELETE", Method::Get => "GET", diff --git a/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs b/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs index 818efe34c7..f50a14fdf0 100644 --- a/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs @@ -20,7 +20,7 @@ use crate::{ time::{self, Duration, OffsetDateTime}, }; use async_trait::async_trait; -use std::sync::Arc; +use std::{ops::Deref, sync::Arc}; use tracing::{debug, trace}; use typespec::error::{Error, ErrorKind, ResultExt}; @@ -71,7 +71,15 @@ pub fn get_retry_after(headers: &Headers, now: DateTimeFn) -> Option { /// A wrapper around a retry count to be used in the context of a retry policy. /// /// This allows a post-retry policy to access the retry count -pub struct RetryPolicyCount(pub u32); +pub struct RetryPolicyCount(u32); + +impl Deref for RetryPolicyCount { + type Target = u32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} /// A retry policy. /// @@ -136,8 +144,8 @@ where "failed to reset body stream before retrying request", )?; } - let try_context = ctx.clone().with_value(RetryPolicyCount(retry_count)); - let result = next[0].send(&try_context, request, &next[1..]).await; + let ctx = ctx.clone().with_value(RetryPolicyCount(retry_count)); + let result = next[0].send(&ctx, request, &next[1..]).await; // only start keeping track of time after the first request is made let start = start.get_or_insert_with(OffsetDateTime::now_utc); let (last_error, retry_after) = match result { diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 7ae0108038..0360471710 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -29,7 +29,14 @@ pub enum AttributeValue { Array(AttributeArray), } -#[derive(Debug, PartialEq)] +/// Represents a key-value pair attribute, which is used for tracing and telemetry. +/// +/// Attributes are used to provide additional context and metadata about a span or event. +/// They can be of various types, including strings, integers, booleans, and arrays. +/// +/// Attributes are typically used to enrich telemetry data with additional information +/// that can be useful for debugging, monitoring, and analysis. +#[derive(Debug, PartialEq, Clone)] pub struct Attribute { /// A key-value pair attribute. pub key: &'static str, diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index 0ff30140e1..36f9f661bc 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -24,30 +24,24 @@ pub use with_context::{FutureExt, WithContext}; /// The TracerProvider trait is the entrypoint for distributed tracing in the SDK. /// /// It provides a method to get a tracer for a specific name and package version. -pub trait TracerProvider: Send + Sync { +pub trait TracerProvider: Send + Sync + Debug { /// Returns a tracer for the given name. /// /// Arguments: /// - `namespace_name`: The namespace of the package for which the tracer is requested. See /// [this page](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers) /// for more information on namespace names. - /// - `package_name`: The name of the package for which the tracer is requested. - /// - `package_version`: The version of the package for which the tracer is requested. + /// - `crate_name`: The name of the crate for which the tracer is requested. + /// - `crate_version`: The version of the crate for which the tracer is requested. fn get_tracer( &self, namespace_name: Option<&'static str>, - package_name: &'static str, - package_version: &'static str, + crate_name: &'static str, + crate_version: &'static str, ) -> Arc; } -impl Debug for dyn TracerProvider { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("TracerProvider").finish_non_exhaustive() - } -} - -pub trait Tracer: Send + Sync { +pub trait Tracer: Send + Sync + Debug { /// Starts a new span with the given name and type. /// /// The newly created span will not have a parent span. @@ -115,12 +109,6 @@ pub trait Tracer: Send + Sync { fn namespace(&self) -> Option<&'static str>; } -impl Debug for dyn Tracer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Tracer").finish_non_exhaustive() - } -} - /// The status of a span. /// /// This enum represents the possible statuses of a span in distributed tracing. @@ -135,7 +123,7 @@ pub enum SpanStatus { Error { description: String }, } -#[derive(Debug, Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq)] pub enum SpanKind { #[default] Internal, From 1b84a83b6fbffa35e621bcaefc3f42232ddd2e31 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Fri, 11 Jul 2025 11:56:30 -0700 Subject: [PATCH 50/84] Added tests for no tracer. --- .../instrumentation/request_instrumentation.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs index 03596c5925..e568ffc589 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs @@ -318,6 +318,23 @@ pub(crate) mod tests { ); } + #[test] + fn test_request_instrumentation_policy_creation() { + let policy = RequestInstrumentationPolicy::new(None); + assert!(policy.tracer.is_none()); + + let mock_tracer_provider = Arc::new(MockTracingProvider::new()); + let tracer = mock_tracer_provider.get_tracer(Some("test namespace"), "test_crate", "1.0.0"); + let policy_with_tracer = RequestInstrumentationPolicy::new(Some(tracer)); + assert!(policy_with_tracer.tracer.is_some()); + } + + #[test] + fn test_request_instrumentation_policy_without_tracer() { + let policy = RequestInstrumentationPolicy::new(None); + assert!(policy.tracer.is_none()); + } + #[tokio::test] async fn client_request_id() { let url = "https://example.com/client_request_id"; From 6f3217ad1c2c043fba36be73b37e2efc5951cc73 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Fri, 11 Jul 2025 11:58:37 -0700 Subject: [PATCH 51/84] Removed tests module from telemetry service implementation. --- .../tests/telemetry_service_implementation.rs | 574 +++++++++--------- 1 file changed, 283 insertions(+), 291 deletions(-) diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index e9576d455a..fbcaec36a6 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -170,268 +170,141 @@ impl TestServiceClient { } } -#[cfg(test)] -mod tests { - use super::*; - use azure_core::Result; - use azure_core_test::{recorded, TestContext}; - use opentelemetry::trace::{ - SpanKind as OpenTelemetrySpanKind, Status as OpenTelemetrySpanStatus, - }; - use opentelemetry::Value as OpenTelemetryAttributeValue; - use tracing::{info, trace}; - - fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { - let otel_exporter = InMemorySpanExporter::default(); - let otel_tracer_provider = SdkTracerProvider::builder() - .with_simple_exporter(otel_exporter.clone()) - .build(); - let otel_tracer_provider = Arc::new(otel_tracer_provider); - (otel_tracer_provider, otel_exporter) - } +use super::*; +use azure_core::Result; +use azure_core_test::{recorded, TestContext}; +use opentelemetry::trace::{SpanKind as OpenTelemetrySpanKind, Status as OpenTelemetrySpanStatus}; +use opentelemetry::Value as OpenTelemetryAttributeValue; +use tracing::{info, trace}; + +fn create_exportable_tracer_provider() -> (Arc, InMemorySpanExporter) { + let otel_exporter = InMemorySpanExporter::default(); + let otel_tracer_provider = SdkTracerProvider::builder() + .with_simple_exporter(otel_exporter.clone()) + .build(); + let otel_tracer_provider = Arc::new(otel_tracer_provider); + (otel_tracer_provider, otel_exporter) +} - // Span verification utility functions. +// Span verification utility functions. - struct ExpectedSpan { - name: &'static str, - kind: OpenTelemetrySpanKind, - parent_span_id: Option, - status: OpenTelemetrySpanStatus, - attributes: Vec<(&'static str, OpenTelemetryAttributeValue)>, - } +struct ExpectedSpan { + name: &'static str, + kind: OpenTelemetrySpanKind, + parent_span_id: Option, + status: OpenTelemetrySpanStatus, + attributes: Vec<(&'static str, OpenTelemetryAttributeValue)>, +} - fn verify_span( - span: &opentelemetry_sdk::trace::SpanData, - expected: ExpectedSpan, - ) -> Result<()> { - assert_eq!(span.name, expected.name); - assert_eq!(span.span_kind, expected.kind); - assert_eq!(span.status, expected.status); - assert_eq!( - span.parent_span_id, - expected - .parent_span_id - .unwrap_or(opentelemetry::trace::SpanId::INVALID) - ); - - for attr in span.attributes.iter() { - println!("Attribute: {} = {:?}", attr.key, attr.value); - let mut found = false; - for (key, value) in expected.attributes.iter() { - if attr.key.as_str() == (*key) { - found = true; - // Skip checking the value for "" as it is a placeholder - if *value != OpenTelemetryAttributeValue::String("".into()) { - assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", *key); - } - break; +fn verify_span(span: &opentelemetry_sdk::trace::SpanData, expected: ExpectedSpan) -> Result<()> { + assert_eq!(span.name, expected.name); + assert_eq!(span.span_kind, expected.kind); + assert_eq!(span.status, expected.status); + assert_eq!( + span.parent_span_id, + expected + .parent_span_id + .unwrap_or(opentelemetry::trace::SpanId::INVALID) + ); + + for attr in span.attributes.iter() { + println!("Attribute: {} = {:?}", attr.key, attr.value); + let mut found = false; + for (key, value) in expected.attributes.iter() { + if attr.key.as_str() == (*key) { + found = true; + // Skip checking the value for "" as it is a placeholder + if *value != OpenTelemetryAttributeValue::String("".into()) { + assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", *key); } - } - if !found { - panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); + break; } } - for (key, value) in expected.attributes.iter() { - if !span.attributes.iter().any(|attr| attr.key == (*key).into()) { - panic!("Expected attribute not found: {} = {:?}", key, value); - } + if !found { + panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); } - - Ok(()) } - - // Basic functionality tests. - #[recorded::test()] - async fn test_service_client_new(ctx: TestContext) -> Result<()> { - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - ..Default::default() - }; - - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); - assert_eq!(client.endpoint().as_str(), "https://example.com/"); - assert_eq!(client.api_version, "2023-10-01"); - - Ok(()) - } - - // Ensure that the the test client actually does what it's supposed to do without telemetry. - #[recorded::test()] - async fn test_service_client_get(ctx: TestContext) -> Result<()> { - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - - let client = TestServiceClient::new(endpoint, credential, None).unwrap(); - let response = client.get("index.html", None).await; - info!("Response: {:?}", response); - assert!(response.is_ok()); - let response = response.unwrap(); - assert_eq!(response.status(), azure_core::http::StatusCode::Ok); - Ok(()) + for (key, value) in expected.attributes.iter() { + if !span.attributes.iter().any(|attr| attr.key == (*key).into()) { + panic!("Expected attribute not found: {} = {:?}", key, value); + } } - #[recorded::test()] - async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { - let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); - - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - azure_client_options: ClientOptions { - request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), - }), - ..Default::default() - }, - ..Default::default() - }; - - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); - let response = client.get("index.html", None).await; - info!("Response: {:?}", response); - assert!(response.is_ok()); - let response = response.unwrap(); - assert_eq!(response.status(), azure_core::http::StatusCode::Ok); - - let spans = otel_exporter.get_finished_spans().unwrap(); - assert_eq!(spans.len(), 1); - for span in &spans { - trace!("Span: {:?}", span); - - verify_span( - span, - ExpectedSpan { - name: "GET", - kind: OpenTelemetrySpanKind::Client, - status: OpenTelemetrySpanStatus::Unset, - parent_span_id: None, - attributes: vec![ - ("http.request.method", "GET".into()), - ("az.client.request.id", "".into()), - ( - "url.full", - format!( - "{}{}", - client.endpoint(), - "index.html?api-version=2023-10-01" - ) - .into(), - ), - ("server.address", "example.com".into()), - ("server.port", 443.into()), - ("http.request.resend_count", 0.into()), - ("http.response.status_code", 200.into()), - ], - }, - )?; - } + Ok(()) +} - Ok(()) - } +// Basic functionality tests. +#[recorded::test()] +async fn test_service_client_new(ctx: TestContext) -> Result<()> { + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + ..Default::default() + }; - #[recorded::test()] - async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { - let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); - - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - azure_client_options: ClientOptions { - request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), - }), - ..Default::default() - }, - ..Default::default() - }; + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + assert_eq!(client.endpoint().as_str(), "https://example.com/"); + assert_eq!(client.api_version, "2023-10-01"); - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); - let response = client.get("failing_url", None).await; - info!("Response: {:?}", response); - - let spans = otel_exporter.get_finished_spans().unwrap(); - assert_eq!(spans.len(), 1); - for span in &spans { - trace!("Span: {:?}", span); - - verify_span( - span, - ExpectedSpan { - name: "GET", - kind: OpenTelemetrySpanKind::Client, - parent_span_id: None, - status: OpenTelemetrySpanStatus::Error { - description: "".into(), - }, - attributes: vec![ - ("http.request.method", "GET".into()), - ("az.client.request.id", "".into()), - ( - "url.full", - format!( - "{}{}", - client.endpoint(), - "failing_url?api-version=2023-10-01" - ) - .into(), - ), - ("server.address", "example.com".into()), - ("server.port", 443.into()), - ("error.type", "404".into()), - ("http.request.resend_count", 0.into()), - ("http.response.status_code", 404.into()), - ], - }, - )?; - } + Ok(()) +} - Ok(()) - } +// Ensure that the the test client actually does what it's supposed to do without telemetry. +#[recorded::test()] +async fn test_service_client_get(ctx: TestContext) -> Result<()> { + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + + let client = TestServiceClient::new(endpoint, credential, None).unwrap(); + let response = client.get("index.html", None).await; + info!("Response: {:?}", response); + assert!(response.is_ok()); + let response = response.unwrap(); + assert_eq!(response.status(), azure_core::http::StatusCode::Ok); + Ok(()) +} - #[recorded::test()] - async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { - let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); - - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - azure_client_options: ClientOptions { - request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), - }), - ..Default::default() - }, +#[recorded::test()] +async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), ..Default::default() - }; + }, + ..Default::default() + }; - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); - let response = client.get_with_function_tracing("index.html", None).await; - info!("Response: {:?}", response); + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("index.html", None).await; + info!("Response: {:?}", response); + assert!(response.is_ok()); + let response = response.unwrap(); + assert_eq!(response.status(), azure_core::http::StatusCode::Ok); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + for span in &spans { + trace!("Span: {:?}", span); - let spans = otel_exporter.get_finished_spans().unwrap(); - assert_eq!(spans.len(), 2); - for span in &spans { - trace!("Span: {:?}", span); - } verify_span( - &spans[0], + span, ExpectedSpan { name: "GET", kind: OpenTelemetrySpanKind::Client, - parent_span_id: Some(spans[1].span_context.span_id()), status: OpenTelemetrySpanStatus::Unset, + parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("az.namespace", "Az.TestServiceClient".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -449,59 +322,49 @@ mod tests { ], }, )?; - verify_span( - &spans[1], - ExpectedSpan { - name: "get_with_tracing", - kind: OpenTelemetrySpanKind::Internal, - parent_span_id: None, - status: OpenTelemetrySpanStatus::Unset, - attributes: vec![("az.namespace", "Az.TestServiceClient".into())], - }, - )?; - - Ok(()) } - #[recorded::test()] - async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { - let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); - - let recording = ctx.recording(); - let endpoint = "https://example.com"; - let credential = recording.credential().clone(); - let options = TestServiceClientOptions { - azure_client_options: ClientOptions { - request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), - }), - ..Default::default() - }, + Ok(()) +} + +#[recorded::test()] +async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), ..Default::default() - }; + }, + ..Default::default() + }; - let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); - let response = client.get_with_function_tracing("failing_url", None).await; - info!("Response: {:?}", response); + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get("failing_url", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + for span in &spans { + trace!("Span: {:?}", span); - let spans = otel_exporter.get_finished_spans().unwrap(); - assert_eq!(spans.len(), 2); - for span in &spans { - trace!("Span: {:?}", span); - } verify_span( - &spans[0], + span, ExpectedSpan { name: "GET", kind: OpenTelemetrySpanKind::Client, - parent_span_id: Some(spans[1].span_context.span_id()), + parent_span_id: None, status: OpenTelemetrySpanStatus::Error { description: "".into(), }, attributes: vec![ ("http.request.method", "GET".into()), - ("az.namespace", "Az.TestServiceClient".into()), ("az.client.request.id", "".into()), ( "url.full", @@ -514,26 +377,155 @@ mod tests { ), ("server.address", "example.com".into()), ("server.port", 443.into()), + ("error.type", "404".into()), ("http.request.resend_count", 0.into()), ("http.response.status_code", 404.into()), - ("error.type", "404".into()), - ], - }, - )?; - verify_span( - &spans[1], - ExpectedSpan { - name: "get_with_tracing", - kind: OpenTelemetrySpanKind::Internal, - parent_span_id: None, - status: OpenTelemetrySpanStatus::Unset, - attributes: vec![ - ("az.namespace", "Az.TestServiceClient".into()), - ("error.type", "404".into()), ], }, )?; + } + + Ok(()) +} + +#[recorded::test()] +async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get_with_function_tracing("index.html", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + for span in &spans { + trace!("Span: {:?}", span); + } + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[1].span_context.span_id()), + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "index.html?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend_count", 0.into()), + ("http.response.status_code", 200.into()), + ], + }, + )?; + verify_span( + &spans[1], + ExpectedSpan { + name: "get_with_tracing", + kind: OpenTelemetrySpanKind::Internal, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![("az.namespace", "Az.TestServiceClient".into())], + }, + )?; + + Ok(()) +} - Ok(()) +#[recorded::test()] +async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); + + let recording = ctx.recording(); + let endpoint = "https://example.com"; + let credential = recording.credential().clone(); + let options = TestServiceClientOptions { + azure_client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracing_provider: Some(azure_provider), + }), + ..Default::default() + }, + ..Default::default() + }; + + let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get_with_function_tracing("failing_url", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + for span in &spans { + trace!("Span: {:?}", span); } + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[1].span_context.span_id()), + status: OpenTelemetrySpanStatus::Error { + description: "".into(), + }, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.request.resend_count", 0.into()), + ("http.response.status_code", 404.into()), + ("error.type", "404".into()), + ], + }, + )?; + verify_span( + &spans[1], + ExpectedSpan { + name: "get_with_tracing", + kind: OpenTelemetrySpanKind::Internal, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![ + ("az.namespace", "Az.TestServiceClient".into()), + ("error.type", "404".into()), + ], + }, + )?; + + Ok(()) } From 7abec7d097306a8162a9815d3ed75a1074109ed4 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Fri, 11 Jul 2025 12:22:05 -0700 Subject: [PATCH 52/84] Fixed build issues. --- .../tests/telemetry_service_implementation.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index fbcaec36a6..4050cbc5f7 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -170,8 +170,6 @@ impl TestServiceClient { } } -use super::*; -use azure_core::Result; use azure_core_test::{recorded, TestContext}; use opentelemetry::trace::{SpanKind as OpenTelemetrySpanKind, Status as OpenTelemetrySpanStatus}; use opentelemetry::Value as OpenTelemetryAttributeValue; From 65b0c598ed65d7561facd66b1364981ee8505dd1 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Fri, 11 Jul 2025 12:28:42 -0700 Subject: [PATCH 53/84] Test fix --- .../http/policies/instrumentation/request_instrumentation.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs index e568ffc589..8c81b3b851 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs @@ -418,6 +418,7 @@ pub(crate) mod tests { }) }) .await; + // Because the URL contains a username and password, we do not set the URL_FULL_ATTRIBUTE. check_request_instrumentation_result( mock_tracer_provider, 1, @@ -437,10 +438,6 @@ pub(crate) mod tests { (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), (SERVER_ADDRESS_ATTRIBUTE, AttributeValue::from("host")), (SERVER_PORT_ATTRIBUTE, AttributeValue::from(8080)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("https://host:8080/path?query=value#fragment"), - ), ], }, ); From 5f6d1c6ca9eb0300a36a37b4284d17c8a13403ed Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Fri, 11 Jul 2025 13:36:10 -0700 Subject: [PATCH 54/84] Updated documentation a bit --- ...ibuted-tracing-for-rust-service-clients.md | 344 ++++++++++++++++++ sdk/core/azure_core_opentelemetry/README.md | 5 +- 2 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 doc/distributed-tracing-for-rust-service-clients.md diff --git a/doc/distributed-tracing-for-rust-service-clients.md b/doc/distributed-tracing-for-rust-service-clients.md new file mode 100644 index 0000000000..16d2466a5d --- /dev/null +++ b/doc/distributed-tracing-for-rust-service-clients.md @@ -0,0 +1,344 @@ + + + +# Distributed tracing options in Rust service clients + +## Distributed tracing fundamentals + +There are three core constructs which are used in distributed tracing: + +* Tracer Providers +* Tracers +* Spans + +### Tracer Provider + +The job of a "Tracer Provider" is to be a factory for tracers. It is the "gateway" construct for distributed tracing. + +### Tracer + +A "tracer" is a factory for "Spans". A `Tracer` is configured with three parameters: + +* `namespace` - the "namespace" for the service client. The namespace for all azure services are listed [on this page](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-services-resource-providers). +* `package name` - this is typically the Cargo package name for the service client (`env!("CARGO_PKG_NAME")`) +* `package version` - this is typically the version of the Cargo package for the service client (`env!("CARGO_PKG_VERSION")`) + +Tracers have three mechanisms for creating spans: + +* Create a new root span. +* Create a new child span from a parent span. +* Create a new child span from the "current" span (where the "current" span is tracer implementation specific). + +### Span + +A "Span" is a unit of tracing. Each span has the following attributes: + +* "name" - the "name" of the span. For public APIs, this is typically the name of the public API, for HTTP request, it is typically the HTTP verb. +* "kind" - HTTP spans come in several flavours: + * Internal - the "default" for span kinds. + * Client - represents a client application - HTTP request spans are "Client" spans. + * Server - represents a server - Azure SDK clients will never use these. + * Producer - represents a messaging (Event Hubs and Service Bus) message producer. + * Consumer - represents a message consumer. +* "status" - A span status is either "Unset" or "Error" - OpenTelemetry defines a status of "Ok" in addition to these, but it is reserved for client applications. +* "attributes" - the attributes on a span describe the span. Attributes include: + * "az.namespace" - the namespace of a request. + * "url.full" - the full (sanitized) URL for an HTTP request + * "server.address" - the DNS address of the HTTP server + * "http.request.method" - the HTTP method used for the request ("GET", "PUT" etc). + +## Azure Distributed Tracing requirements + +Azure's distributed tracing requirements are laid out in a number of documents: + +* [Azure Distributed Tracing Conventions](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.md) +* [Azure Distributed Tracing Implementation](https://github.com/Azure/azure-sdk/blob/main/docs/general/implementation.md#distributed-tracing) +* [Open Telemetry HTTP Span Conventions](https://opentelemetry.io/docs/specs/semconv/http/http-spans/) + +Looking at each document, the following two requirements for distributed tracing clients fall out: + +1) Each public API (service client function) needs to have a span with the `az.namespace` attribute added - the az.attribute (as defined above). [See this for more information](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.md#public-api-calls). +1) Each HTTP request needs to have a span with the same `az.namespace` attribute and a number of other attributes derived from the HTTP operation. [See this for more information](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.md#http-client-spans). The HTTP request span should be a child of a public API span if possible. + +Implementations are allowed to skip adding the `az.namespace` attribute but it is strongly discouraged. + +It turns out that in OpenTelemetry, an `OpenTelemetry::Tracer` is constructed with an `InstrumentationScope` which allows arbitrary attributes to be attached to the tracer, which is also attached to each span constructed from the tracer. As such, it makes sense for each service client to have a `Tracer` attached to the service client, and this `Tracer` can be used to hold the namespace attribute. This architecture is reflected in the distributed tracing wrapper API, the `Tracer` trait contains a `namespace()` function. + +## Additional requirements + +For public APIs, the rule of thumb is: "If the operation does not take time and cannot fail, it doesn't get a span". For most public APIs, this isn't a huge deal, but for pageable and long running operations, it changes how the code is generated. Specifically, for pageables, the actual service client does not actually interact with the network and cannot fail, but the individual pager returned does interact with the network and can fail - thus the pager's interactions with the service need to be instrumented with a span. Long Running Operations behave similarly - while the original API is instrumented with a span, the same is true for the status monitor - it also needs to be instrumented with a span whose name matches the name of the original API. + +In addition, [certain service clients](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.md#library-specific-attributes) (Cosmos DB, KeyVault, etc) have additional client-specific attributes which need to be added to the span. + +## Core API design + +Given this architecture, it implies that each service client needs the following: + +1) A struct field named `tracer` which is an `Arc` which holds the tracing implementation specific tracer. +2) Code in the service client's `new` function which instantiates a `tracer` from the `TracerProvider` configured in the service client options. The primary function of this tracer is to provide the value for the `az.namespace` attribute for both the public API spans and the HTTP request spans. +3) Code in each service client public API which instantiates a public API span. + +For the Rust implementation, if a tracer provider is configured, ALL http operations will have HTTP request spans created regardless of whether the public API spans are created. + +## Implementation details + +To provide for requirement #1, if a customer provides a value for the `azure_core::ClientOptions::request_instrumentation` structure, the Azure Core HTTP pipeline will add a `PublicApiInstrumentationPolicy` to the pipeline which is responsible for creating the public API outer span. + +To provide for requirement #2, if a customer provides a `azure_core::ClientOptions::request_instrumentation` the `azure_core` HTTP pipeline will add a `RequestInstrumentationPolicy` to the pipeline which is responsible for creating the required HTTP request span to the pipeline. + +This implementation means that operations like Long Running Operations (Pollers) and Pageable Operations (Pagers) will all have a Public API span created by the `PublicApiInstrumentationPolicy` and a HTTP Request span created by the `RequestInstrumentationPolicy`. + +### Pipeline Construction + +When an `azure_core::http::Pipeline` is constructed, if the client options include a tracing provider, then the pipeline will create a tracer from that tracer provider with the crate name and crate version (which are both parameters to the pipeline constructor). This tracer will have a namespace of "None" and acts as a fallback in case the public APIs don't provide a `Tracer` implementation (if, for example public APIs are instrumented, but the service client itself is not instrumented). This tracer will be passed to both of the tracing policies. + +### PublicApiInstrumentationPolicy + +1) If the pipeline context has a `Arc` attached to the context, then the public API policy will simply call the next policy in the pipeline, because a span in the pipeline indicates that this API call is a nested API call. +1) If the context does not have a `PublicApiInstrumentationInformation` attached to it, the policy will call the next policy in the pipeline, otherwise the policy will: + 1) Look for an `Arc` attached to the context. If one is found, it uses that tracer, otherwise it uses a tracer attached to the policy. + 1) Create a span with a name matching the `name` in the [`PublicApiInstrumentationInformation`] structure and attributes from the attributes attached to the `PublicApiInstrumentationInformation`. It will also add the `az.namespace` attribute to the span if the tracer has a namespace associated with it (this will typically only be the case for tracers attached to the context). + 1) Once the span has been created, the policy will attach the newly created span to the context so other policies in the pipeline (specifically the `RequestInstrumentationPolicy` can use this span). +1) Once the span has been created, the policy calls the next policy in the pipeline. +1) After the remaining policies in the pipeline have run, the policy inspects the `Result` of the pipeline execution and sets the `error.type` attribute and the span status based on the `Result` of the operation. + +### RequestInstrumentationPolicy + +The `RequestInstrumentationPolicy` will do the following: + +1) If the `Context` parameter for the `RequestInstrumentationPolicy` contains a `Tracer` value, then the `RequestInstrumentationPolicy` will use that `Tracer` value to create the span, otherwise it will use the pre-configured tracer from when the policy was created. +2) If the `Context` parameter for the `RequestInstrumentationPolicy contains a`Span` value, then the policy will use that span as the parent span for the newly created HTTP request span, otherwise it will create a new span. + +This design means that even if a service public API is not fully instrumented with a `Tracer` or a `Span`, it will still generate some HTTP request traces. + +Since the namespace attribute is service-client wide, it makes sense to capture that inside a per-service client field, that way it can be easily accessed from service clients. + +## Convenience Macros + +To facilitate the implementation of the three core requirements above, three attribute-like macros are defined for the use of each service. + +Those macros are: + +* `#[tracing::client]` - applied to each service client `struct` declaration. +* `#[tracing::new]` - applied to each service client "constructor". +* `#[tracing::function]` - applied to each service client "public API". + +### `#[tracing::client]` + +The `tracing::client` attribute macro does one thing and one thing only: It defines a field named `tracer` which is added to the list of fields in the service client structure. + +#### Modification introduced by this macro + +From: + +```rust +pub struct MyServiceClient { + endpoint: Url, +} +``` + +to + +```diff +pub struct MyServiceClient { + endpoint: Url, ++ tracer: std::sync::Arc, +} +``` + +Arguably this attribute is unnecessary, but it can be incredibly helpful especially if we need to add more elements to each service client in the future. + +### `#[tracing::new()]` + +Annotates a `new` service client function to initialize the `tracer` field in the structure. + +#### Modification introduced by this macro + +from: + +```diff +pub fn new( + endpoint: &str, + credential: Arc, + options: Option, +) -> Result { + let options = options.unwrap_or_default(); + let mut endpoint = Url::parse(endpoint)?; + if !endpoint.scheme().starts_with("http") { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("{endpoint} must use http(s)"), + )); + } + endpoint.set_query(None); + let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( + credential, + vec!["https://vault.azure.net/.default"], + )); + Ok(Self { + endpoint, + api_version: options.api_version, + pipeline: Pipeline::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options.client_options, + Vec::default(), + vec![auth_policy], + ), + }) +} +``` + +to: + +```diff +pub fn new( + endpoint: &str, + credential: Arc, + options: Option +) -> Result { + let options = options.unwrap_or_default(); + let mut endpoint = Url::parse(endpoint)?; + if !endpoint.scheme().starts_with("http") { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("{endpoint} must use http(s)"), + )); + } + endpoint.set_query(None); + let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( + credential, + vec!["https://vault.azure.net/.default"], + )); ++ let tracer = tracing::create_tracer(Some(""), ++ option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), ++ option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), ++ &options.client_options); + Ok(Self { ++ tracer, + endpoint, + api_version: options.api_version, + pipeline: Pipeline::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options.client_options, + Vec::default(), + vec![auth_policy], + ), + }) +} +``` + +Note that this implementation takes advantage of a helper function `create_tracer` - without this function, the logic to create the per-client tracer looks like: + +```rust +let tracer = + if let Some(tracer_options) = &options.azure_client_options.request_instrumentation { + tracer_options + .tracing_provider + .as_ref() + .map(|tracing_provider| { + tracing_provider.get_tracer( + Some(""), + option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + ) + }) + } else { + None + }; +``` + +#### Pros + +* Simple implementation for clients +* Adds ability for centralized implementation + +#### Cons + +* Potentially fragile - there are several patterns for implementing the `Self` element within a `new` function, each of which needs to be handled - the current macro handles `Self{}` and `Ok(Self{})` but there are other patterns like `let this = Self {}; this` or similar constructs which would be skipped. +* Does not handle `builder` patterns at all. + +### `#[tracing::function(::)]` + +Applied to all public functions in the service client ("public APIs" in distributed tracing terms). This macro creates the client span for each service client method, and updates the client span if appropriate. + +#### Modification introduced by this macro + +From: + +```rust +pub async fn get( + &self, + path: &str, + options: Option>, +) -> Result { + let options = options.unwrap_or_default(); + let mut url = self.endpoint.clone(); + url.set_path(path); + url.query_pairs_mut() + .append_pair("api-version", &self.api_version); + + let mut request = Request::new(url, azure_core::http::Method::Get); + + let response = self + .pipeline + .send(&options.method_options.context, &mut request) + .await?; + if !response.status().is_success() { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::HttpResponse { + status: response.status(), + error_code: None, + }, + format!("Failed to GET {}: {}", request.url(), response.status()))); + } + Ok(response) +} +``` + +To: + +```diff +pub async fn get( + &self, + path: &str, + options: Option>, +) -> Result { ++ let mut options = options.unwrap_or_default(); + ++ let public_api_info = PublicApiInstrumentationInformation { ++ api_name: "get_with_tracing", ++ attributes: vec![], ++ }; ++ ++ // Add the span to the tracer. ++ let mut ctx = options.method_options.context.with_value(public_api_info); ++ // If the service has a tracer, we add it to the context. ++ if let Some(tracer) = &self.tracer { ++ ctx = ctx.with_value(tracer.clone()); ++ } + + let mut url = self.endpoint.clone(); + url.set_path(path); + url.query_pairs_mut() + .append_pair("api-version", &self.api_version); + + let mut request = Request::new(url, azure_core::http::Method::Get); + + let response = self + .pipeline +- .send(&options.method_options.context, &mut request) ++ .send(&ctx, &mut request) + .await?; + if !response.status().is_success() { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::HttpResponse { + status: response.status(), + error_code: None, + }, + format!("Failed to GET {}: {}", request.url(), response.status()))); + } + response +} +``` diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index 7b272db7fa..9573a57180 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -74,13 +74,13 @@ let options = ServiceClientOptions { Once the `OpenTelemetryTracerProvider` is integrated with the Azure Service ClientOptions, the Azure SDK will be configured to capture per-API and per-HTTP operation tracing options, and the HTTP requests will be annotated with [W3C Trace Context headers](https://www.w3.org/TR/trace-context/). -# Troubleshooting +## Troubleshooting ## General ## Logging -# Contributing +## Contributing See the [CONTRIBUTING.md] for details on building, testing, and contributing to these libraries. @@ -101,6 +101,5 @@ Azure SDK for Rust is licensed under the [MIT](https://github.com/Azure/azure-sd [Microsoft Open Source Code of Conduct]: https://opensource.microsoft.com/codeofconduct/ -[Cargo]: https://crates.io/ [CONTRIBUTING.md]: https://github.com/Azure/azure-sdk-for-rust/blob/main/CONTRIBUTING.md [Code of Conduct FAQ]: https://opensource.microsoft.com/codeofconduct/faq/ From da804a4694a50f4af970e3e07a91706953a82c1a Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 09:42:33 -0700 Subject: [PATCH 55/84] CI fixes; clean up public instrumentation policy implementation --- Cargo.lock | 1 - ...ibuted-tracing-for-rust-service-clients.md | 2 +- sdk/core/azure_core/src/http/options/mod.rs | 3 +- .../public_api_instrumentation.rs | 338 ++++++++++++------ sdk/core/azure_core_macros/Cargo.toml | 3 +- sdk/core/azure_core_macros/src/tracing.rs | 30 -- 6 files changed, 239 insertions(+), 138 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c2d7e2429..bbd2fc51d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,7 +208,6 @@ dependencies = [ name = "azure_core_macros" version = "0.1.0" dependencies = [ - "azure_core", "proc-macro2", "quote", "syn", diff --git a/doc/distributed-tracing-for-rust-service-clients.md b/doc/distributed-tracing-for-rust-service-clients.md index 16d2466a5d..36830ef85b 100644 --- a/doc/distributed-tracing-for-rust-service-clients.md +++ b/doc/distributed-tracing-for-rust-service-clients.md @@ -19,7 +19,7 @@ The job of a "Tracer Provider" is to be a factory for tracers. It is the "gatewa A "tracer" is a factory for "Spans". A `Tracer` is configured with three parameters: -* `namespace` - the "namespace" for the service client. The namespace for all azure services are listed [on this page](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-services-resource-providers). +* `namespace` - the "namespace" for the service client. The namespace for all azure services are listed [on this page](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers). * `package name` - this is typically the Cargo package name for the service client (`env!("CARGO_PKG_NAME")`) * `package version` - this is typically the version of the Cargo package for the service client (`env!("CARGO_PKG_VERSION")`) diff --git a/sdk/core/azure_core/src/http/options/mod.rs b/sdk/core/azure_core/src/http/options/mod.rs index cfad8865af..e337fa1873 100644 --- a/sdk/core/azure_core/src/http/options/mod.rs +++ b/sdk/core/azure_core/src/http/options/mod.rs @@ -8,8 +8,7 @@ pub use request_instrumentation::*; use std::sync::Arc; use typespec_client_core::http::policies::Policy; pub use typespec_client_core::http::{ - ClientMethodOptions, ExponentialRetryOptions, FixedRetryOptions, - RetryOptions, TransportOptions, + ClientMethodOptions, ExponentialRetryOptions, FixedRetryOptions, RetryOptions, TransportOptions, }; pub use user_agent::*; diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs index 6161e5b24f..af09d31885 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs @@ -86,106 +86,94 @@ impl Policy for PublicApiInstrumentationPolicy { return next[0].send(ctx, request, &next[1..]).await; } - // We're not a nested call, so we can proceed with instrumentation. - // We first check if the context has public API instrumentation information. - // If it does, we use that information to create a span for the request. - // If it doesn't, we skip instrumentation. - let public_api_information = ctx.value::(); - let span: Option> = public_api_information - .map(|info| { - // Use the public API information for instrumentation. - // If the context has a tracer (which happens when called from an instrumented method), - // we prefer the tracer from the context. - // Otherwise, we use the tracer from the policy itself. - // This allows for flexibility in using different tracers in different contexts. - let mut span_attributes = vec![]; - - for attr in &info.attributes { - // Add the attributes from the public API information to the span. - span_attributes.push(Attribute { - key: attr.key, - value: attr.value.clone(), - }); - } + // We next confirm if the context has public API instrumentation information. + // Without a public API information, we skip instrumentation. + let Some(info) = ctx.value::() else { + return next[0].send(ctx, request, &next[1..]).await; + }; - if let Some(tracer) = ctx.value::>() { - if let Some(namespace) = tracer.namespace() { - // If the tracer has a namespace, we set it as an attribute. - span_attributes.push(Attribute { - key: AZ_NAMESPACE_ATTRIBUTE, - value: namespace.into(), - }); - } - // Create a span with the public API information. - Some(tracer.start_span_with_current( - info.api_name, - SpanKind::Internal, - span_attributes, - )) - } else { - self.tracer.as_ref().map(|tracer| { - tracer.start_span_with_current( - info.api_name, - SpanKind::Internal, - span_attributes, - ) - }) - } - }) - .unwrap(); + // Get the tracer from either the context or the policy. + let tracer = match ctx.value::>() { + Some(tracer) => Some(tracer), + None => self.tracer.as_ref(), + }; - // Now add the span to the context, so that it can be used by the next policies. - let mut ctx = ctx.clone(); - if let Some(span) = &span { - // If we have a span, we set it in the context. - ctx = ctx.with_value(span.clone()); + // If we don't have a tracer, skip instrumentation + let Some(tracer) = tracer else { + return next[0].send(ctx, request, &next[1..]).await; + }; + + // We now have public API information and a tracer. + // Calculate the span attributes based on the public API information and + // tracer. + let mut span_attributes = vec![]; + + for attr in &info.attributes { + // Add the attributes from the public API information to the span. + span_attributes.push(Attribute { + key: attr.key, + value: attr.value.clone(), + }); } + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + span_attributes.push(Attribute { + key: AZ_NAMESPACE_ATTRIBUTE, + value: namespace.into(), + }); + } + + // Create a span with the public API information and attributes. + let span = + tracer.start_span_with_current(info.api_name, SpanKind::Internal, span_attributes); + + // If nothing is listening to the span, we skip instrumentation. + if !span.is_recording() { + return next[0].send(ctx, request, &next[1..]).await; + } + // Now add the span to the context, so that it can be used by the next policies. + let ctx = ctx.clone().with_value(span.clone()); + let result = next[0].send(&ctx, request, &next[1..]).await; - if let Some(span) = span { - match &result { - Err(e) => { - // If the request failed, we set the error type on the span. - match e.kind() { - crate::error::ErrorKind::HttpResponse { status, .. } => { - span.set_attribute(ERROR_TYPE_ATTRIBUTE, status.to_string().into()); - - // 5xx status codes SHOULD set status to Error. - // The description should not be set because it can be inferred from "http.response.status_code". - if status.is_server_error() { - span.set_status(crate::tracing::SpanStatus::Error { - description: "".to_string(), - }); - } - } - _ => { - span.set_attribute(ERROR_TYPE_ATTRIBUTE, e.kind().to_string().into()); + match &result { + Err(e) => { + // If the request failed, we set the error type on the span. + match e.kind() { + crate::error::ErrorKind::HttpResponse { status, .. } => { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, status.to_string().into()); + + // 5xx status codes SHOULD set status to Error. + // The description should not be set because it can be inferred from "http.response.status_code". + if status.is_server_error() { span.set_status(crate::tracing::SpanStatus::Error { - description: e.kind().to_string(), + description: "".to_string(), }); } } - } - Ok(response) => { - // 5xx status codes SHOULD set status to Error. - // The description should not be set because it can be inferred from "http.response.status_code". - if response.status().is_server_error() { + _ => { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, e.kind().to_string().into()); span.set_status(crate::tracing::SpanStatus::Error { - description: "".to_string(), + description: e.kind().to_string(), }); } - if response.status().is_client_error() || response.status().is_server_error() { - span.set_attribute( - ERROR_TYPE_ATTRIBUTE, - response.status().to_string().into(), - ); - } } } - - span.end(); + Ok(response) => { + // 5xx status codes SHOULD set status to Error. + // The description should not be set because it can be inferred from "http.response.status_code". + if response.status().is_server_error() { + span.set_status(crate::tracing::SpanStatus::Error { + description: "".to_string(), + }); + } + if response.status().is_client_error() || response.status().is_server_error() { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); + } + } } + span.end(); result } } @@ -212,31 +200,51 @@ mod tests { // Test just the public API instrumentation policy without request instrumentation. async fn run_public_api_instrumentation_test( - api_name: Option<&'static str>, + api_information: Option, + create_tracer: bool, + add_tracer_to_context: bool, request: &mut Request, callback: C, ) -> Arc where C: FnMut(&Request) -> BoxFuture<'_, Result> + Send + Sync + 'static, { - let public_api_information = PublicApiInstrumentationInformation { - api_name: api_name.unwrap_or("unknown"), - attributes: Vec::new(), - }; // Add the public API information and tracer to the context so that it can be used by the policy. let mock_tracer_provider = Arc::new(MockTracingProvider::new()); - let tracer = mock_tracer_provider.get_tracer(Some("test namespace"), "test_crate", "1.0.0"); - let public_api_policy = Arc::new(PublicApiInstrumentationPolicy::new(Some(tracer.clone()))); + let tracer = if create_tracer { + Some(mock_tracer_provider.get_tracer( + add_tracer_to_context.then_some("test namespace"), + "test_crate", + "1.0.0", + )) + } else { + None + }; + + let public_api_policy = { + let policy_tracer = tracer.clone(); + Arc::new(PublicApiInstrumentationPolicy::new(policy_tracer)) + }; let transport = TransportPolicy::new(TransportOptions::new(Arc::new(MockHttpClient::new( callback, )))); let next: Vec> = vec![Arc::new(transport)]; - let ctx = Context::default() - .with_value(public_api_information) - .with_value(tracer.clone()); + + let mut ctx = Context::default(); + if let Some(t) = tracer { + if add_tracer_to_context { + // If we have a tracer, add it to the context. + ctx = ctx.with_value(t.clone()); + } + } + + if api_information.is_some() { + // If we have public API information, add it to the context. + ctx = ctx.with_value(api_information.unwrap()); + } let _result = public_api_policy.send(&ctx, request, &next).await; mock_tracer_provider @@ -340,13 +348,130 @@ mod tests { } } + #[tokio::test] + async fn public_api_instrumentation_no_public_api_info() { + let url = "http://example.com/path"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer = run_public_api_instrumentation_test( + None, // No public API information. + true, // Create tracer. + true, + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; + + assert_eq!( + mock_tracer.tracers.lock().unwrap().len(), + 1, + "Expected one tracer to be created", + ); + let tracers = mock_tracer.tracers.lock().unwrap(); + let tracer = tracers.first().unwrap(); + assert_eq!( + tracer.spans.lock().unwrap().len(), + 0, + "Expected no spans to be created" + ); + } + + #[tokio::test] + async fn public_api_instrumentation_no_tracer() { + let url = "http://example.com/path"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer = run_public_api_instrumentation_test( + Some(PublicApiInstrumentationInformation { + api_name: "MyClient.MyApi", + attributes: vec![], + }), + false, // Create tracer. + false, // Add tracer to context. + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; + + assert_eq!( + mock_tracer.tracers.lock().unwrap().len(), + 0, + "Expected no tracers to be created", + ); + } + + #[tokio::test] + async fn public_api_instrumentation_tracer_not_in_context() { + let url = "http://example.com/path"; + let mut request = Request::new(url.parse().unwrap(), Method::Get); + + let mock_tracer = run_public_api_instrumentation_test( + Some(PublicApiInstrumentationInformation { + api_name: "MyClient.MyApi", + attributes: vec![], + }), + true, // Create tracer. + false, // Add tracer to context. + &mut request, + |req| { + Box::pin(async move { + assert_eq!(req.url().host_str(), Some("example.com")); + assert_eq!(req.method(), &Method::Get); + Ok(RawResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + vec![], + )) + }) + }, + ) + .await; + + check_public_api_instrumentation_result( + mock_tracer.clone(), + 1, + 0, + Some("MyClient.MyApi"), + SpanKind::Internal, + SpanStatus::Unset, + vec![], + ); + } + #[tokio::test] async fn simple_public_api_instrumentation_policy() { let url = "http://example.com/path"; let mut request = Request::new(url.parse().unwrap(), Method::Get); - let mock_tracer = - run_public_api_instrumentation_test(Some("MyClient.MyApi"), &mut request, |req| { + let mock_tracer = run_public_api_instrumentation_test( + Some(PublicApiInstrumentationInformation { + api_name: "MyClient.MyApi", + attributes: vec![], + }), + true, // Create tracer. + true, + &mut request, + |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); assert_eq!(req.method(), &Method::Get); @@ -356,8 +481,9 @@ mod tests { vec![], )) }) - }) - .await; + }, + ) + .await; check_public_api_instrumentation_result( mock_tracer, @@ -375,8 +501,15 @@ mod tests { let url = "http://example.com/path"; let mut request = Request::new(url.parse().unwrap(), Method::Get); - let mock_tracer = - run_public_api_instrumentation_test(Some("MyClient.MyApi"), &mut request, |req| { + let mock_tracer = run_public_api_instrumentation_test( + Some(PublicApiInstrumentationInformation { + api_name: "MyClient.MyApi", + attributes: vec![], + }), + true, + true, + &mut request, + |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); assert_eq!(req.method(), &Method::Get); @@ -386,8 +519,9 @@ mod tests { vec![], )) }) - }) - .await; + }, + ) + .await; check_public_api_instrumentation_result( mock_tracer, diff --git a/sdk/core/azure_core_macros/Cargo.toml b/sdk/core/azure_core_macros/Cargo.toml index 7632fbffd9..0e6fc5d4f7 100644 --- a/sdk/core/azure_core_macros/Cargo.toml +++ b/sdk/core/azure_core_macros/Cargo.toml @@ -23,6 +23,5 @@ syn.workspace = true typespec_client_core.workspace = true [dev-dependencies] -azure_core.workspace = true -#azure_core_test.workspace = true +#azure_core.workspace = true tokio.workspace = true diff --git a/sdk/core/azure_core_macros/src/tracing.rs b/sdk/core/azure_core_macros/src/tracing.rs index 0d68be3903..abf1074878 100644 --- a/sdk/core/azure_core_macros/src/tracing.rs +++ b/sdk/core/azure_core_macros/src/tracing.rs @@ -74,34 +74,4 @@ pub(crate) mod tests { } true } - - use azure_core::{ - http::{ClientOptions, Url}, - tracing, - }; - use std::sync::Arc; - - #[tracing::client] - pub struct MyServiceClient { - endpoint: Url, - } - - #[derive(Default)] - pub struct MyServiceClientOptions { - #[allow(dead_code)] - pub client_options: ClientOptions, - } - - impl MyServiceClient { - #[tracing::new("MyServiceClientNamespace")] - pub fn new( - endpoint: &str, - _credential: Arc, - options: Option, - ) -> Self { - let options = options.unwrap_or_default(); - let url = Url::parse(endpoint).expect("Invalid endpoint URL"); - Self { endpoint: url } - } - } } From 58186f711b7c1195df00b58d88eec30305d5c469 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 09:58:56 -0700 Subject: [PATCH 56/84] Tracing feature is gated on HTTP feature --- sdk/typespec/typespec_client_core/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/typespec/typespec_client_core/src/lib.rs b/sdk/typespec/typespec_client_core/src/lib.rs index da7b18a5e1..0baa57dfc5 100644 --- a/sdk/typespec/typespec_client_core/src/lib.rs +++ b/sdk/typespec/typespec_client_core/src/lib.rs @@ -18,6 +18,7 @@ pub mod json; pub mod sleep; pub mod stream; pub mod time; +#[cfg(feature = "http")] pub mod tracing; #[cfg(feature = "xml")] pub mod xml; From 6c64c3481b554919a7b8773884291c4baf794d84 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 10:19:32 -0700 Subject: [PATCH 57/84] Doc fixes --- Cargo.lock | 1 + sdk/core/azure_core_macros/Cargo.toml | 2 +- sdk/core/azure_core_macros/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbd2fc51d4..3c2d7e2429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,7 @@ dependencies = [ name = "azure_core_macros" version = "0.1.0" dependencies = [ + "azure_core", "proc-macro2", "quote", "syn", diff --git a/sdk/core/azure_core_macros/Cargo.toml b/sdk/core/azure_core_macros/Cargo.toml index 0e6fc5d4f7..11e4056f37 100644 --- a/sdk/core/azure_core_macros/Cargo.toml +++ b/sdk/core/azure_core_macros/Cargo.toml @@ -23,5 +23,5 @@ syn.workspace = true typespec_client_core.workspace = true [dev-dependencies] -#azure_core.workspace = true +azure_core.workspace = true tokio.workspace = true diff --git a/sdk/core/azure_core_macros/src/lib.rs b/sdk/core/azure_core_macros/src/lib.rs index 4df16c8892..804e55d382 100644 --- a/sdk/core/azure_core_macros/src/lib.rs +++ b/sdk/core/azure_core_macros/src/lib.rs @@ -105,7 +105,7 @@ pub fn new(attr: TokenStream, item: TokenStream) -> TokenStream { /// impl MyServiceClient { /// /// #[tracing::function("MyServiceClient.PublicFunction")] -/// pub async fn public_function(&self, param: &str, options: Option) -> Result<()> { +/// pub async fn public_function(&self, param: &str, options: Option>) -> Result<()> { /// let options = options.unwrap_or_default(); /// Ok(()) /// } From 6ffc905d8942edf62057f4ebebd75de2e5d6fe08 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 10:37:19 -0700 Subject: [PATCH 58/84] Fixed test function --- .../azure_core_macros/src/tracing_function.rs | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/sdk/core/azure_core_macros/src/tracing_function.rs b/sdk/core/azure_core_macros/src/tracing_function.rs index 7d630491b7..b09f760bc1 100644 --- a/sdk/core/azure_core_macros/src/tracing_function.rs +++ b/sdk/core/azure_core_macros/src/tracing_function.rs @@ -349,11 +349,43 @@ mod tests { let actual = parse_function(attr, item)?; let expected = quote! { - pub async fn my_function() -> Result<(), Box> { - let mut options = None; - let result = tracing::function("TestFunction", options); - Ok(()) + pub async fn my_function(&self, path: &str) -> Result<(), Box> { + let options = { + let mut options = options.unwrap_or_default(); + let public_api_info = azure_core::tracing::PublicApiInstrumentationInformation { + api_name: "TestFunction", + attributes: Vec::new(), + }; + let mut ctx = options.method_options.context.with_value(public_api_info); + if let Some(tracer) = &self.tracer { + ctx = ctx.with_value(tracer.clone()); + } + options.method_options.context = ctx; + Some(options) + }; + { + let options = options.unwrap_or_default(); + let mut url = self.endpoint.clone(); + url.set_path(path); + url.query_pairs_mut() + .append_pair("api-version", &self.api_version); + let mut request = Request::new(url, azure_core::http::Method::Get); + let response = self + .pipeline + .send(&options.method_options.context, &mut request) + .await?; + if !response.status().is_success() { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::HttpResponse { + status: response.status(), + error_code: None, + }, + format!("Failed to GET {}: {}", request.url(), response.status()), + )); + } + Ok(response) } + } }; println!("Parsed tokens: {:?}", actual.to_string()); From f09d06f0430c2ff273de422b554be1565634cea3 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 10:49:33 -0700 Subject: [PATCH 59/84] Try fixing pack error --- sdk/core/azure_core/Cargo.toml | 1 + sdk/core/azure_core_macros/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/core/azure_core/Cargo.toml b/sdk/core/azure_core/Cargo.toml index bb5afe45ba..8bccbc6228 100644 --- a/sdk/core/azure_core/Cargo.toml +++ b/sdk/core/azure_core/Cargo.toml @@ -38,6 +38,7 @@ typespec_client_core = { workspace = true, features = [ rustc_version.workspace = true [dev-dependencies] +azure_core_macros.path = "../azure_core_macros" azure_core_test.workspace = true azure_identity.workspace = true azure_security_keyvault_certificates.path = "../../keyvault/azure_security_keyvault_certificates" diff --git a/sdk/core/azure_core_macros/Cargo.toml b/sdk/core/azure_core_macros/Cargo.toml index 11e4056f37..c5c7f2952f 100644 --- a/sdk/core/azure_core_macros/Cargo.toml +++ b/sdk/core/azure_core_macros/Cargo.toml @@ -20,7 +20,7 @@ proc-macro = true proc-macro2.workspace = true quote.workspace = true syn.workspace = true -typespec_client_core.workspace = true +typespec_client_core = { workspace = true, features = ["http", "json"] } [dev-dependencies] azure_core.workspace = true From 66297ac2703002aeb160d5f0377b0e25a47d83b5 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 11:57:15 -0700 Subject: [PATCH 60/84] Removed azure_core dependency from azure_core_macros to fix packing problem --- Cargo.lock | 1 - sdk/core/azure_core_macros/Cargo.toml | 1 - sdk/core/azure_core_macros/src/lib.rs | 87 +++------------------------ 3 files changed, 9 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c2d7e2429..bbd2fc51d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,7 +208,6 @@ dependencies = [ name = "azure_core_macros" version = "0.1.0" dependencies = [ - "azure_core", "proc-macro2", "quote", "syn", diff --git a/sdk/core/azure_core_macros/Cargo.toml b/sdk/core/azure_core_macros/Cargo.toml index c5c7f2952f..a2a941fecf 100644 --- a/sdk/core/azure_core_macros/Cargo.toml +++ b/sdk/core/azure_core_macros/Cargo.toml @@ -23,5 +23,4 @@ syn.workspace = true typespec_client_core = { workspace = true, features = ["http", "json"] } [dev-dependencies] -azure_core.workspace = true tokio.workspace = true diff --git a/sdk/core/azure_core_macros/src/lib.rs b/sdk/core/azure_core_macros/src/lib.rs index 804e55d382..ac1edb394c 100644 --- a/sdk/core/azure_core_macros/src/lib.rs +++ b/sdk/core/azure_core_macros/src/lib.rs @@ -12,22 +12,8 @@ use proc_macro::TokenStream; /// Attribute client struct declarations to enable distributed tracing. /// -/// # Examples -/// -/// -/// -/// For example, to declare a client that will be traced, you should use the `#[trace::client]` attribute. -/// -/// ``` -/// use azure_core::tracing; -/// use azure_core::http::Url; -/// use std::sync::Arc; -/// -/// #[tracing::client] -/// pub struct MyServiceClient { -/// endpoint: Url, -/// } -/// ``` +/// To declare a client that will be traced, you should use the `#[tracing::client]` attribute +/// exported from azure_core. /// #[proc_macro_attribute] pub fn client(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -37,37 +23,16 @@ pub fn client(attr: TokenStream, item: TokenStream) -> TokenStream { /// Attribute client struct instantiation to enable distributed tracing. /// -/// # Examples -/// -/// To declare a client that will be traced, you should use the `#[traced::client]` attribute. -/// To instantiate a client, use the `[traced::new]` which generates a distributed tracing tracer associated with the client namespace. -/// -/// ``` -/// use azure_core::{tracing, http::{Url, ClientOptions}}; -/// use std::sync::Arc; -/// -/// #[tracing::client] -/// pub struct MyServiceClient { -/// endpoint: Url, -/// } +/// To enable tracing for a client instantiation, you should use the `#[tracing::new]` attribute +/// exported from azure_core. /// -/// #[derive(Default)] -/// pub struct MyServiceClientOptions { -/// pub client_options: ClientOptions, -/// } +/// This macro will automatically instrument the client instantiation with tracing information. +/// It will also ensure that the client is created with the necessary tracing context. /// -/// impl MyServiceClient { +/// The `#[tracing::new]` attribute takes a single argument, which is a string +/// representing the Azure Namespace name for the service being traced. /// -/// #[tracing::new("MyServiceClientNamespace")] -/// pub fn new(endpoint: &str, _credential: Arc, options: Option) -> Self { -/// let options = options.unwrap_or_default(); -/// let url = Url::parse(endpoint).expect("Invalid endpoint URL"); -/// Self { -/// endpoint: url, -/// } -/// } -/// } -/// ``` +/// The list of Azure Namespaces can be found [on this page](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers /// #[proc_macro_attribute] pub fn new(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -77,40 +42,6 @@ pub fn new(attr: TokenStream, item: TokenStream) -> TokenStream { /// Attribute client struct instantiation to enable distributed tracing. /// -/// # Examples -/// -/// To declare a client that will be traced, you should use the `#[traced::client]` attribute. -/// To instantiate a client, use the `[traced::new]` which generates a distributed tracing tracer associated with the client namespace. -/// -/// ``` -/// use azure_core::{tracing, http::{Url, ClientOptions}, Result}; -/// use azure_core::http::ClientMethodOptions; -/// use std::sync::Arc; -/// -/// #[tracing::client] -/// pub struct MyServiceClient { -/// endpoint: Url, -/// } -/// -/// #[derive(Default)] -/// pub struct MyServiceClientOptions { -/// pub client_options: ClientOptions, -/// } -/// -/// #[derive(Default)] -/// pub struct MyServiceClientMethodOptions<'a> { -/// pub method_options: ClientMethodOptions<'a>, -/// } -/// -/// impl MyServiceClient { -/// -/// #[tracing::function("MyServiceClient.PublicFunction")] -/// pub async fn public_function(&self, param: &str, options: Option>) -> Result<()> { -/// let options = options.unwrap_or_default(); -/// Ok(()) -/// } -/// } -/// ``` /// #[proc_macro_attribute] pub fn function(attr: TokenStream, item: TokenStream) -> TokenStream { From e6325bd40ea90e6cbaea59b02e265fc7f414f044 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 12:24:43 -0700 Subject: [PATCH 61/84] Fixed url in doccoment --- sdk/core/azure_core_macros/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure_core_macros/src/lib.rs b/sdk/core/azure_core_macros/src/lib.rs index ac1edb394c..fdef5b8794 100644 --- a/sdk/core/azure_core_macros/src/lib.rs +++ b/sdk/core/azure_core_macros/src/lib.rs @@ -32,7 +32,7 @@ pub fn client(attr: TokenStream, item: TokenStream) -> TokenStream { /// The `#[tracing::new]` attribute takes a single argument, which is a string /// representing the Azure Namespace name for the service being traced. /// -/// The list of Azure Namespaces can be found [on this page](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers +/// The list of Azure Namespaces can be found [on this page](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers) /// #[proc_macro_attribute] pub fn new(attr: TokenStream, item: TokenStream) -> TokenStream { From af4661e93ccd5726e2e81dc3aa2de1797a5958fc Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 13:09:50 -0700 Subject: [PATCH 62/84] Doc and commented out code cleanup --- .../request_instrumentation.rs | 36 ++++--------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs index 8c81b3b851..0a5639d83d 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs @@ -27,41 +27,18 @@ impl RequestInstrumentationPolicy { /// Creates a new `RequestInstrumentationPolicy`. /// /// # Arguments - /// - `azure_namespace`: The Azure namespace for the tracer. - /// - `crate_name`: The name of the crate for which the tracer is created. - /// - `crate_version`: The version of the crate for which the tracer is created. - /// - `options`: Options for request instrumentation, including the tracing provider. + /// - `tracer`: Pre-configured tracer to use for instrumentation. /// /// # Returns /// A new instance of `RequestInstrumentationPolicy`. /// /// # Note - /// This policy will only create a tracer if a tracing provider is provided in the options. /// - /// This policy will create a tracer that can be used to instrument HTTP requests. - /// However this tracer is only used when the client method is NOT instrumented. - /// A part of the client method instrumentation sets a client-specific tracer into the - /// request `[Context]` which will be used instead of the tracer from this policy. + /// The tracer provided is a "fallback" tracer which is used if the `ctx` parameter + /// to the `send` method does not contain a tracer. /// - pub fn new( - tracer: Option>, - // azure_namespace: Option<&'static str>, - // crate_name: Option<&'static str>, - // crate_version: Option<&'static str>, - // options: &RequestInstrumentationOptions, - ) -> Self { + pub fn new(tracer: Option>) -> Self { Self { tracer } - // if let Some(tracing_provider) = &options.tracing_provider { - // Self { - // tracer: Some(tracing_provider.get_tracer( - // azure_namespace, - // crate_name.unwrap_or("unknown"), - // crate_version.unwrap_or("unknown"), - // )), - // } - // } else { - // Self { tracer: None } - // } } } @@ -96,6 +73,7 @@ impl Policy for RequestInstrumentationPolicy { let Some(tracer) = tracer else { return next[0].send(ctx, request, &next[1..]).await; }; + let mut span_attributes = vec![Attribute { key: HTTP_REQUEST_METHOD_ATTRIBUTE, value: request.method().to_string().into(), @@ -132,7 +110,6 @@ impl Policy for RequestInstrumentationPolicy { }); } // Get the method as a string to avoid lifetime issues - // let method_str = request.method_as_str(); let method_str = request.method().as_str(); let span = if let Some(parent_span) = ctx.value::>() { // If a parent span exists, start a new span with the parent. @@ -143,7 +120,8 @@ impl Policy for RequestInstrumentationPolicy { parent_span.clone(), ) } else { - // If no parent span exists, start a new span without a parent. + // If no parent span exists, start a new span with the "current" span (if any). + // It is up to the tracer implementation to determine what "current" means. tracer.start_span_with_current(method_str, SpanKind::Client, span_attributes) }; From c8e512a848c8e2a83167504d0aef6e7cc670598c Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 13:16:33 -0700 Subject: [PATCH 63/84] Renamed tracing_provider to tracer_provider --- .../http/options/request_instrumentation.rs | 4 ++-- sdk/core/azure_core/src/http/pipeline.rs | 11 +++++++---- sdk/core/azure_core_macros/src/tracing_new.rs | 18 +++++++++--------- sdk/core/azure_core_opentelemetry/README.md | 4 ++-- .../tests/telemetry_service_implementation.rs | 14 +++++++------- .../tests/telemetry_service_macros.rs | 4 ++-- 6 files changed, 29 insertions(+), 26 deletions(-) diff --git a/sdk/core/azure_core/src/http/options/request_instrumentation.rs b/sdk/core/azure_core/src/http/options/request_instrumentation.rs index d15dd01584..0bd253224c 100644 --- a/sdk/core/azure_core/src/http/options/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/options/request_instrumentation.rs @@ -6,6 +6,6 @@ use std::sync::Arc; /// Policy options to enable distributed tracing. #[derive(Clone, Debug, Default)] pub struct RequestInstrumentationOptions { - /// Set the tracing provider for distributed tracing. - pub tracing_provider: Option>, + /// Set the tracer provider for distributed tracing. + pub tracer_provider: Option>, } diff --git a/sdk/core/azure_core/src/http/pipeline.rs b/sdk/core/azure_core/src/http/pipeline.rs index 9fa7dec26d..97d60d452d 100644 --- a/sdk/core/azure_core/src/http/pipeline.rs +++ b/sdk/core/azure_core/src/http/pipeline.rs @@ -54,16 +54,18 @@ impl Pipeline { let install_instrumentation_policies = core_client_options .request_instrumentation - .tracing_provider + .tracer_provider .is_some(); + // Create a fallback tracer if no tracer provider is set. + // This is useful for service clients that have not yet been instrumented. let tracer = if install_instrumentation_policies { core_client_options .request_instrumentation - .tracing_provider + .tracer_provider .as_ref() - .map(|tracing_provider| { - tracing_provider.get_tracer( + .map(|tracer_provider| { + tracer_provider.get_tracer( None, crate_name.unwrap_or("Unknown"), crate_version.unwrap_or("0.1.0"), @@ -72,6 +74,7 @@ impl Pipeline { } else { None }; + let mut per_call_policies = per_call_policies.clone(); push_unique(&mut per_call_policies, ClientRequestIdPolicy::default()); if install_instrumentation_policies { diff --git a/sdk/core/azure_core_macros/src/tracing_new.rs b/sdk/core/azure_core_macros/src/tracing_new.rs index 9c68e580c2..a23bdfc589 100644 --- a/sdk/core/azure_core_macros/src/tracing_new.rs +++ b/sdk/core/azure_core_macros/src/tracing_new.rs @@ -20,10 +20,10 @@ fn parse_struct_expr( let tracer = if let Some(tracer_options) = &options.client_options.request_instrumentation { tracer_options - .tracing_provider + .tracer_provider .as_ref() - .map(|tracing_provider| { - tracing_provider.get_tracer( + .map(|tracer_provider| { + tracer_provider.get_tracer( Some(#client_namespace), option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), @@ -186,10 +186,10 @@ mod tests { &options.client_options.request_instrumentation { tracer_options - .tracing_provider + .tracer_provider .as_ref() - .map(|tracing_provider| { - tracing_provider.get_tracer( + .map(|tracer_provider| { + tracer_provider.get_tracer( Some("Az.Namespace"), option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), @@ -276,10 +276,10 @@ mod tests { &options.client_options.request_instrumentation { tracer_options - .tracing_provider + .tracer_provider .as_ref() - .map(|tracing_provider| { - tracing_provider.get_tracer( + .map(|tracer_provider| { + tracer_provider.get_tracer( Some("Az.GeneratedNamespace"), option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), diff --git a/sdk/core/azure_core_opentelemetry/README.md b/sdk/core/azure_core_opentelemetry/README.md index 9573a57180..033e4c2ca2 100644 --- a/sdk/core/azure_core_opentelemetry/README.md +++ b/sdk/core/azure_core_opentelemetry/README.md @@ -30,7 +30,7 @@ let azure_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider); let options = ServiceClientOptions { client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), + tracer_provider: Some(azure_provider), }), ..Default::default() }, @@ -62,7 +62,7 @@ let azure_provider = OpenTelemetryTracerProvider::new_from_global_provider(); let options = ServiceClientOptions { client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), + tracer_provider: Some(azure_provider), }), ..Default::default() }, diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 9f49d81634..f0681ff4d1 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -63,10 +63,10 @@ impl TestServiceClient { let tracer = if let Some(tracer_options) = &options.azure_client_options.request_instrumentation { tracer_options - .tracing_provider + .tracer_provider .as_ref() - .map(|tracing_provider| { - tracing_provider.get_tracer( + .map(|tracer_provider| { + tracer_provider.get_tracer( Some("Az.TestServiceClient"), option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), @@ -275,7 +275,7 @@ async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { let options = TestServiceClientOptions { azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), + tracer_provider: Some(azure_provider), }), ..Default::default() }, @@ -336,7 +336,7 @@ async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result< let options = TestServiceClientOptions { azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), + tracer_provider: Some(azure_provider), }), ..Default::default() }, @@ -397,7 +397,7 @@ async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Resu let options = TestServiceClientOptions { azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), + tracer_provider: Some(azure_provider), }), ..Default::default() }, @@ -465,7 +465,7 @@ async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) - let options = TestServiceClientOptions { azure_client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), + tracer_provider: Some(azure_provider), }), ..Default::default() }, diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs index d586def81c..695075bb85 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs @@ -200,7 +200,7 @@ mod tests { let options = TestServiceClientWithMacrosOptions { client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), + tracer_provider: Some(azure_provider), }), ..Default::default() }, @@ -449,7 +449,7 @@ mod tests { let options = TestServiceClientWithMacrosOptions { client_options: ClientOptions { request_instrumentation: Some(RequestInstrumentationOptions { - tracing_provider: Some(azure_provider), + tracer_provider: Some(azure_provider), }), ..Default::default() }, From 6557eba218d8fe927bad9b683ab321362b04c1b6 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 13:56:38 -0700 Subject: [PATCH 64/84] Updated distributed tracing documentation --- ...ibuted-tracing-for-rust-service-clients.md | 107 ++++++++++-------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/doc/distributed-tracing-for-rust-service-clients.md b/doc/distributed-tracing-for-rust-service-clients.md index 36830ef85b..68373ce864 100644 --- a/doc/distributed-tracing-for-rust-service-clients.md +++ b/doc/distributed-tracing-for-rust-service-clients.md @@ -148,7 +148,7 @@ pub struct MyServiceClient { Arguably this attribute is unnecessary, but it can be incredibly helpful especially if we need to add more elements to each service client in the future. -### `#[tracing::new()]` +### `#[tracing::new()]` Annotates a `new` service client function to initialize the `tracer` field in the structure. @@ -156,7 +156,7 @@ Annotates a `new` service client function to initialize the `tracer` field in th from: -```diff +```rust pub fn new( endpoint: &str, credential: Arc, @@ -210,10 +210,21 @@ pub fn new( credential, vec!["https://vault.azure.net/.default"], )); -+ let tracer = tracing::create_tracer(Some(""), -+ option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), -+ option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), -+ &options.client_options); ++ let tracer = ++ if let Some(tracer_options) = &options.client_options.request_instrumentation { ++ tracer_options ++ .tracer_provider ++ .as_ref() ++ .map(|tracer_provider| { ++ tracer_provider.get_tracer( ++ Some(#client_namespace), ++ option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), ++ option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), ++ ) ++ }) ++ } else { ++ None ++ }; Ok(Self { + tracer, endpoint, @@ -229,40 +240,14 @@ pub fn new( } ``` -Note that this implementation takes advantage of a helper function `create_tracer` - without this function, the logic to create the per-client tracer looks like: - -```rust -let tracer = - if let Some(tracer_options) = &options.azure_client_options.request_instrumentation { - tracer_options - .tracing_provider - .as_ref() - .map(|tracing_provider| { - tracing_provider.get_tracer( - Some(""), - option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), - option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), - ) - }) - } else { - None - }; -``` - -#### Pros - -* Simple implementation for clients -* Adds ability for centralized implementation - -#### Cons - -* Potentially fragile - there are several patterns for implementing the `Self` element within a `new` function, each of which needs to be handled - the current macro handles `Self{}` and `Ok(Self{})` but there are other patterns like `let this = Self {}; this` or similar constructs which would be skipped. -* Does not handle `builder` patterns at all. +Note that if the service client uses the `builder` pattern, it cannot use the `tracing::new` attribute. -### `#[tracing::function(::)]` +### `#[tracing::function(.)]` Applied to all public functions in the service client ("public APIs" in distributed tracing terms). This macro creates the client span for each service client method, and updates the client span if appropriate. +Note that the `` and `` should be the values from the client typespec - that way the public API span names align for all client languages. + #### Modification introduced by this macro From: @@ -305,20 +290,19 @@ pub async fn get( path: &str, options: Option>, ) -> Result { -+ let mut options = options.unwrap_or_default(); - -+ let public_api_info = PublicApiInstrumentationInformation { -+ api_name: "get_with_tracing", -+ attributes: vec![], ++ let options = { ++ let mut options = options.unwrap_or_default(); ++ let public_api_info = azure_core::tracing::PublicApiInstrumentationInformation { ++ api_name: "TestFunction", ++ attributes: Vec::new(), ++ }; ++ let mut ctx = options.method_options.context.with_value(public_api_info); ++ if let Some(tracer) = &self.tracer { ++ ctx = ctx.with_value(tracer.clone()); ++ } ++ options.method_options.context = ctx; ++ Some(options) + }; -+ -+ // Add the span to the tracer. -+ let mut ctx = options.method_options.context.with_value(public_api_info); -+ // If the service has a tracer, we add it to the context. -+ if let Some(tracer) = &self.tracer { -+ ctx = ctx.with_value(tracer.clone()); -+ } - let mut url = self.endpoint.clone(); url.set_path(path); url.query_pairs_mut() @@ -342,3 +326,28 @@ pub async fn get( response } ``` + +The `tracing::function` has a separate form which can be used for service clients which +implement per-service-client distributed tracing attributes. + +The parameters for the `tracing::function` roughly follow the following BNF: + +```bnf +tracing_parameters = quoted_string [ ',' '( attribute_list ')'] +quoted_string = `"` `"` +attribute_list = attribute | attribute [`,`] attribute_list +attribute = key '=' value +key = identifier | quoted_string +identifier = rust-identifier | rust-identifier '.' identifier +rust-identifier = +value = +``` + +This means that the following are valid parameters for `tracing::function`: + +* `#[tracing::function("MyServiceClient.MyApi")]` - specifies a public API name. +* `#[tracing::function("Name", (az.namespace="namespace"))]` - specifies a public API name and creates a span with an attribute named "az.namespace" and a value of "namespace". +* `#[tracing::function("Name", (api_count=23, "my_attribute" = "Abc"))]` - specifies a public API name and creates a span with two attributes, one named "api_count" with a value of "23" and the other with the name "my_attribute" and a value of "Abc" +* `#[tracing::function("Name", ("API path"=path))]` - specifies a public API name and creates a span with an attribute named "API path" and the value of the parameter named "path". + +This allows a generator to pass in simple attribute annotations to the public API spans created by the pipeline. From 0ca013691a8026a4240a9eb2bf9dbb759c4741b6 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 16:10:21 -0700 Subject: [PATCH 65/84] Added helper function to allow service clients to create their own spans if needed --- ...ibuted-tracing-for-rust-service-clients.md | 20 ++++++ .../public_api_instrumentation.rs | 62 +++++++++++++------ 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/doc/distributed-tracing-for-rust-service-clients.md b/doc/distributed-tracing-for-rust-service-clients.md index 68373ce864..ae56b9ecc7 100644 --- a/doc/distributed-tracing-for-rust-service-clients.md +++ b/doc/distributed-tracing-for-rust-service-clients.md @@ -351,3 +351,23 @@ This means that the following are valid parameters for `tracing::function`: * `#[tracing::function("Name", ("API path"=path))]` - specifies a public API name and creates a span with an attribute named "API path" and the value of the parameter named "path". This allows a generator to pass in simple attribute annotations to the public API spans created by the pipeline. + +## Special cases + +For the most part, the three tracing attribute macros provide functionality that should allow most generated and wrapped clients to create all the required distributed tracing span attributes. + +However there are some cases where having additional control over the traces is needed. This functionality is primarily intended for wrapped service clients to handle span attributes which cannot be easily determined from the operation. + +### Service Client needs to add attributes *before* the pipeline + +If your service client needs to define attributes in the client span before the pipeline and the attributes cannot be determined by reflecting the contents of service parameters, then the service client can create its own `PublicApiInstrumentationInformation` structure and add the desired attributes manually. If this `PublicApiInstrumentationInformation` is added to the request Context, it will be reflected in the spans created by the `PublicApiInstrumentationPolicy`. + +### Service Client needs to add attributes before and after the pipeline + +For some operations, a service client cannot easily express the information needed to generate the span (or needs to add attributes based on the response to the public API). In that case, the service client should create its own span. + +The `PublicApiInstrumentationPolicy` struct has a convenience method `create_public_api_span` which allows a service client to create a span based on the current context. The function signature for `create_public_api_span` is `create_public_api_span(ctx: &Context, tracer: Option>) -> Option>`. It assumes that the `Context`in `ctx` has already had a `PublicApiInstrumentationInformation` attribute added and returns an optional span - if the span has the value of Some, it is a tracer which can be used for the public API, if it has the value of None, then there is no public API span created (this can happen if there is no `PublicApiInstrumentationInformation` provided, or if the `Context` already contains a public API span). + +The client can then add whatever attributes to the span it needs, and after the pipeline has run, can add any additional attributes to the span. + +Note that in this model, the client is responsible for ending the span. diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs index af09d31885..2f92947f8f 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs @@ -69,39 +69,48 @@ impl PublicApiInstrumentationPolicy { pub fn new(tracer: Option>) -> Self { Self { tracer } } -} -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -impl Policy for PublicApiInstrumentationPolicy { - async fn send( - &self, + /// Creates a span for the public API instrumentation policy. + /// + /// This function creates a span for the public API instrumentation policy based on the + /// public API information in the context. + /// + /// This function assumes that the `Context` already has a `PublicApiInstrumentationInformation` value, + /// if it is not present, it will return `None`. + /// + /// # Arguments + /// - `ctx`: The context containing the public API information. + /// - `tracer`: An optional tracer to use for creating the span. + /// + /// # Returns + /// An optional span if the public API information is present and a tracer is available. + /// + /// If the context already has a span, it will return `None` to avoid nested spans. + /// If the context does not have a tracer it will use the value of the `tracer` argument. + /// If no tracer can be determined, it will return `None`. + /// + pub fn create_public_api_span( ctx: &Context, - request: &mut Request, - next: &[Arc], - ) -> PolicyResult { + tracer: Option>, + ) -> Option> { // If there is a span in the context, we're a nested call, so we just want to forward the request. if ctx.value::>().is_some() { trace!("PublicApiPolicy: Nested call detected, forwarding request without instrumentation."); - return next[0].send(ctx, request, &next[1..]).await; + return None; } // We next confirm if the context has public API instrumentation information. // Without a public API information, we skip instrumentation. - let Some(info) = ctx.value::() else { - return next[0].send(ctx, request, &next[1..]).await; - }; + let info = ctx.value::()?; // Get the tracer from either the context or the policy. let tracer = match ctx.value::>() { Some(tracer) => Some(tracer), - None => self.tracer.as_ref(), + None => tracer.as_ref(), }; // If we don't have a tracer, skip instrumentation - let Some(tracer) = tracer else { - return next[0].send(ctx, request, &next[1..]).await; - }; + let tracer = tracer?; // We now have public API information and a tracer. // Calculate the span attributes based on the public API information and @@ -130,8 +139,25 @@ impl Policy for PublicApiInstrumentationPolicy { // If nothing is listening to the span, we skip instrumentation. if !span.is_recording() { - return next[0].send(ctx, request, &next[1..]).await; + return None; } + Some(span) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Policy for PublicApiInstrumentationPolicy { + async fn send( + &self, + ctx: &Context, + request: &mut Request, + next: &[Arc], + ) -> PolicyResult { + let Some(span) = Self::create_public_api_span(ctx, self.tracer.clone()) else { + return next[0].send(ctx, request, &next[1..]).await; + }; + // Now add the span to the context, so that it can be used by the next policies. let ctx = ctx.clone().with_value(span.clone()); From cdc3ab207d0227dbe12e1804e724bcae70437650 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 14 Jul 2025 16:35:52 -0700 Subject: [PATCH 66/84] Added tests for public_api_information function --- .../public_api_instrumentation.rs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs index 2f92947f8f..b61f3429fe 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs @@ -374,6 +374,73 @@ mod tests { } } + // Tests for the create_public_api_span function. + #[test] + fn create_public_api_span() { + let tracer = Arc::new(MockTracingProvider::new()).get_tracer(Some("test"), "test", "1.0.0"); + + // Test when context has no PublicApiInstrumentationInformation + { + let ctx = Context::default(); + let span = + PublicApiInstrumentationPolicy::create_public_api_span(&ctx, Some(tracer.clone())); + assert!(span.is_none(), "Should return None when no API info exists"); + } + // Test when context already has a span + { + let existing_span = tracer.start_span("existing", SpanKind::Internal, vec![]); + let ctx = Context::default().with_value(existing_span.clone()); + let span = + PublicApiInstrumentationPolicy::create_public_api_span(&ctx, Some(tracer.clone())); + assert!( + span.is_none(), + "Should return None when context already has a span" + ); + } + // Test with API info but no tracer + { + let api_info = PublicApiInstrumentationInformation { + api_name: "TestClient.test_api", + attributes: vec![], + }; + let ctx = Context::default().with_value(api_info); + let span = PublicApiInstrumentationPolicy::create_public_api_span(&ctx, None); + assert!( + span.is_none(), + "Should return None when no tracer is available" + ); + } + // Test with API info and tracer from context + { + let api_info = PublicApiInstrumentationInformation { + api_name: "TestClient.test_api", + attributes: vec![], + }; + let ctx = Context::default() + .with_value(api_info) + .with_value(tracer.clone()); + let span = PublicApiInstrumentationPolicy::create_public_api_span(&ctx, None); + assert!( + span.is_some(), + "Should create span when API info and tracer are available" + ); + } + // Test with API info, tracer from parameter, and attributes + { + let api_info = PublicApiInstrumentationInformation { + api_name: "TestClient.test_api", + attributes: vec![Attribute { + key: "test.attribute", + value: "test_value".into(), + }], + }; + let ctx = Context::default().with_value(api_info); + let span = + PublicApiInstrumentationPolicy::create_public_api_span(&ctx, Some(tracer.clone())); + assert!(span.is_some(), "Should create span with attributes"); + } + } + #[tokio::test] async fn public_api_instrumentation_no_public_api_info() { let url = "http://example.com/path"; From 858c0bb8f15ba08fd61785427207d861a16ebd2a Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 15 Jul 2025 13:42:35 -0700 Subject: [PATCH 67/84] Added pageable test --- .../azure_core_macros/src/tracing_function.rs | 153 +++++++++++++++++- 1 file changed, 146 insertions(+), 7 deletions(-) diff --git a/sdk/core/azure_core_macros/src/tracing_function.rs b/sdk/core/azure_core_macros/src/tracing_function.rs index b09f760bc1..f1c5049b27 100644 --- a/sdk/core/azure_core_macros/src/tracing_function.rs +++ b/sdk/core/azure_core_macros/src/tracing_function.rs @@ -173,7 +173,9 @@ impl Parse for FunctionNameAndAttributes { fn is_function_declaration(item: &TokenStream) -> bool { let item_fn: ItemFn = match syn::parse2(item.clone()) { Ok(fn_item) => fn_item, - Err(_) => return false, + Err(_) => { + return false; + } }; // Function must be public. @@ -181,11 +183,6 @@ fn is_function_declaration(item: &TokenStream) -> bool { return false; } - // Function must be public. - if item_fn.sig.asyncness.is_none() { - return false; - } - // Function must return a Result type. if let syn::ReturnType::Type(_, ty) = &item_fn.sig.output { if !matches!(ty.as_ref(), syn::Type::Path(_)) { @@ -308,7 +305,7 @@ mod tests { } }; let invalid_fn = quote! { - pub fn my_function() -> Result<(), Box> { + pub fn my_function() { } }; @@ -397,4 +394,146 @@ mod tests { ); Ok(()) } + + // cspell: ignore deletedsecrets + #[test] + fn test_parse_pageable_function() -> std::result::Result<(), syn::Error> { + let attr = quote! { "TestFunction" }; + let item = quote! { + pub fn list_deleted_secret_properties( + &self, + options: Option>, + ) -> Result> { + let options = options.unwrap_or_default().into_owned(); + let pipeline = self.pipeline.clone(); + let mut first_url = self.endpoint.clone(); + first_url = first_url.join("deletedsecrets")?; + first_url + .query_pairs_mut() + .append_pair("api-version", &self.api_version); + if let Some(maxresults) = options.maxresults { + first_url + .query_pairs_mut() + .append_pair("maxresults", &maxresults.to_string()); + } + let api_version = self.api_version.clone(); + Ok(Pager::from_callback(move |next_link: Option| { + let url = match next_link { + Some(next_link) => { + let qp = next_link + .query_pairs() + .filter(|(name, _)| name.ne("api-version")); + let mut next_link = next_link.clone(); + next_link + .query_pairs_mut() + .clear() + .extend_pairs(qp) + .append_pair("api-version", &api_version); + next_link + } + None => first_url.clone(), + }; + let mut request = Request::new(url, Method::Get); + request.insert_header("accept", "application/json"); + let ctx = options.method_options.context.clone(); + let pipeline = pipeline.clone(); + async move { + let rsp: RawResponse = pipeline.send(&ctx, &mut request).await?; + let (status, headers, body) = rsp.deconstruct(); + let bytes = body.collect().await?; + let res: ListDeletedSecretPropertiesResult = json::from_json(&bytes)?; + let rsp = RawResponse::from_bytes(status, headers, bytes).into(); + Ok(match res.next_link { + Some(next_link) if !next_link.is_empty() => PagerResult::More { + response: rsp, + continuation: next_link.parse()?, + }, + _ => PagerResult::Done { response: rsp }, + }) + } + })) + } + }; + + let actual = parse_function(attr, item)?; + let expected = quote! { + pub fn list_deleted_secret_properties( + &self, + options: Option>, + ) -> Result> { + let options = { + let mut options = options.unwrap_or_default(); + let public_api_info = azure_core::tracing::PublicApiInstrumentationInformation { + api_name: "TestFunction", + attributes: Vec::new(), + }; + let mut ctx = options.method_options.context.with_value(public_api_info); + if let Some(tracer) = &self.tracer { + ctx = ctx.with_value(tracer.clone()); + } + options.method_options.context = ctx; + Some(options) + }; + { + let options = options.unwrap_or_default().into_owned(); + let pipeline = self.pipeline.clone(); + let mut first_url = self.endpoint.clone(); + first_url = first_url.join("deletedsecrets")?; + first_url + .query_pairs_mut() + .append_pair("api-version", &self.api_version); + if let Some(maxresults) = options.maxresults { + first_url + .query_pairs_mut() + .append_pair("maxresults", &maxresults.to_string()); + } + let api_version = self.api_version.clone(); + Ok(Pager::from_callback(move |next_link: Option| { + let url = match next_link { + Some(next_link) => { + let qp = next_link + .query_pairs() + .filter(|(name, _)| name.ne("api-version")); + let mut next_link = next_link.clone(); + next_link + .query_pairs_mut() + .clear() + .extend_pairs(qp) + .append_pair("api-version", &api_version); + next_link + } + None => first_url.clone(), + }; + let mut request = Request::new(url, Method::Get); + request.insert_header("accept", "application/json"); + let ctx = options.method_options.context.clone(); + let pipeline = pipeline.clone(); + async move { + let rsp: RawResponse = pipeline.send(&ctx, &mut request).await?; + let (status, headers, body) = rsp.deconstruct(); + let bytes = body.collect().await?; + let res: ListDeletedSecretPropertiesResult = json::from_json(&bytes)?; + let rsp = RawResponse::from_bytes(status, headers, bytes).into(); + Ok(match res.next_link { + Some(next_link) if !next_link.is_empty() => PagerResult::More { + response: rsp, + continuation: next_link.parse()?, + }, + _ => PagerResult::Done { response: rsp }, + }) + } + })) + } + } + }; + + println!("Parsed tokens: {:?}", actual.to_string()); + println!("Expected tokens: {:?}", expected.to_string()); + + assert!( + crate::tracing::tests::compare_token_stream(actual, expected), + "Parsed tokens do not match expected tokens" + ); + Ok(()) + } } From 9821dd426f9b4863e18cbc9a33e80ea4988d953d Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 15 Jul 2025 14:20:23 -0700 Subject: [PATCH 68/84] tracing::client is pub(crate) to match rust generator --- sdk/core/azure_core_macros/src/tracing_client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure_core_macros/src/tracing_client.rs b/sdk/core/azure_core_macros/src/tracing_client.rs index d05ecd5b43..c652916ed9 100644 --- a/sdk/core/azure_core_macros/src/tracing_client.rs +++ b/sdk/core/azure_core_macros/src/tracing_client.rs @@ -27,7 +27,7 @@ pub fn parse_client(_attr: TokenStream, item: TokenStream) -> Result>, + pub(crate) tracer: Option>, } }) } @@ -65,7 +65,7 @@ mod tests { pub struct ServiceClient { name: &'static str, endpoint: Url, - tracer: Option>, + pub(crate) tracer: Option>, } }; // println!("Parsed tokens: {:?}", tokens); From e859cc93d63b433c4a5c450adb4f1edca8a27c2f Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 15 Jul 2025 14:36:27 -0700 Subject: [PATCH 69/84] allow tracing::new on methods starting with with --- sdk/core/azure_core_macros/src/tracing_new.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure_core_macros/src/tracing_new.rs b/sdk/core/azure_core_macros/src/tracing_new.rs index a23bdfc589..382743ee7d 100644 --- a/sdk/core/azure_core_macros/src/tracing_new.rs +++ b/sdk/core/azure_core_macros/src/tracing_new.rs @@ -140,8 +140,10 @@ fn is_new_declaration(item: &TokenStream) -> bool { return false; } - // Service clients new functions must have a name that starts with `new_`. - if !item_fn.sig.ident.to_string().starts_with("new") { + // Service clients new functions must have a name that starts with `new_` or "with_". + if !item_fn.sig.ident.to_string().starts_with("new") + && !item_fn.sig.ident.to_string().starts_with("with") + { return false; } From 6d1bb0d34abbc1f3aa1d0179390c19cd6391881c Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 15 Jul 2025 16:30:02 -0700 Subject: [PATCH 70/84] Added tracing::subclient to express subclient instantiation --- sdk/core/azure_core_macros/src/lib.rs | 10 +- .../src/tracing_subclient.rs | 147 ++++++++++++++++++ 2 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 sdk/core/azure_core_macros/src/tracing_subclient.rs diff --git a/sdk/core/azure_core_macros/src/lib.rs b/sdk/core/azure_core_macros/src/lib.rs index fdef5b8794..b66104c637 100644 --- a/sdk/core/azure_core_macros/src/lib.rs +++ b/sdk/core/azure_core_macros/src/lib.rs @@ -7,6 +7,7 @@ mod tracing; mod tracing_client; mod tracing_function; mod tracing_new; +mod tracing_subclient; use proc_macro::TokenStream; @@ -40,8 +41,13 @@ pub fn new(attr: TokenStream, item: TokenStream) -> TokenStream { .map_or_else(|e| e.into_compile_error().into(), |v| v.into()) } -/// Attribute client struct instantiation to enable distributed tracing. -/// +#[proc_macro_attribute] +pub fn subclient(attr: TokenStream, item: TokenStream) -> TokenStream { + tracing_subclient::parse_subclient(attr.into(), item.into()) + .map_or_else(|e| e.into_compile_error().into(), |v| v.into()) +} + +/// Attribute client public APIs to enable distributed tracing. /// #[proc_macro_attribute] pub fn function(attr: TokenStream, item: TokenStream) -> TokenStream { diff --git a/sdk/core/azure_core_macros/src/tracing_subclient.rs b/sdk/core/azure_core_macros/src/tracing_subclient.rs new file mode 100644 index 0000000000..cbea8101f0 --- /dev/null +++ b/sdk/core/azure_core_macros/src/tracing_subclient.rs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{spanned::Spanned, ExprStruct, ItemFn, Result}; + +const INVALID_SUBCLIENT_MESSAGE: &str = "subclient attribute must be applied to a public function named `get__client` that returns a client type"; + +/// Parse the token stream for an Azure Service subclient declaration. +/// +/// An Azure Service client is a public struct that represents a client for an Azure service. +/// +/// +pub fn parse_subclient(_attr: TokenStream, item: TokenStream) -> Result { + if !is_subclient_declaration(&item) { + return Err(syn::Error::new(item.span(), INVALID_SUBCLIENT_MESSAGE)); + } + + let ItemFn { + vis, + sig, + block, + attrs, + } = syn::parse2(item.clone())?; + + let body = block.stmts; + + let ExprStruct { fields, path, .. } = syn::parse2(body.first().unwrap().to_token_stream())?; + + let fields = fields.iter(); + + Ok(quote! { + #(#attrs)* + #vis #sig { + #path { + #(#fields),*, + tracer: self.tracer.clone(), + } + } + }) +} + +fn is_subclient_declaration(item: &TokenStream) -> bool { + let fn_struct: ItemFn = match syn::parse2(item.clone()) { + Ok(fn_item) => fn_item, + Err(_) => return false, + }; + + // Subclient constructors must be public functions. + if !matches!(fn_struct.vis, syn::Visibility::Public(_)) { + return false; + } + + let fn_body = fn_struct.block; + // Subclient constructors must have a body with a single statement. + if fn_body.stmts.len() != 1 { + return false; + } + + // Subclient constructors must have a name that starts with `get_`. + if !fn_struct.sig.ident.to_string().starts_with("get_") { + return false; + } + + // Subclient constructors must have a return type that is a client type. + if let syn::ReturnType::Type(_, ty) = &fn_struct.sig.output { + if !matches!(ty.as_ref(), syn::Type::Path(p) if p.path.segments.last().unwrap().ident.to_string().ends_with("Client")) + { + return false; + } + } else { + return false; + } + + true +} + +#[cfg(test)] +mod tests { + use super::*; + use proc_macro2::TokenStream; + use quote::quote; + + #[test] + fn test_is_subclient_declaration() { + assert!(is_subclient_declaration("e! { + pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient { + OperationTemplatesLroClient { + api_version: self.api_version.clone(), + endpoint: self.endpoint.clone(), + pipeline: self.pipeline.clone(), + subscription_id: self.subscription_id.clone(), + } + } + }),); + + assert!(!is_subclient_declaration("e! { + pub fn not_a_subclient() {} + })); + + assert!(!is_subclient_declaration("e! { + pub fn operation_templates_lro_client() -> OperationTemplatesLroClient { + OperationTemplatesLroClient { + api_version: "2021-01-01".to_string(), + endpoint: "https://example.com".to_string(), + pipeline: "pipeline".to_string(), + subscription_id: "subscription_id".to_string(), + } + } + })); + } + + #[test] + fn test_parse_subclient() { + let attr = TokenStream::new(); + let item = quote! { + pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient { + OperationTemplatesLroClient { + api_version: self.api_version.clone(), + endpoint: self.endpoint.clone(), + pipeline: self.pipeline.clone(), + subscription_id: self.subscription_id.clone(), + } + } + }; + + let actual = parse_subclient(attr.clone(), item.clone()) + .expect("Failed to parse subclient declaration"); + println!("Actual:{actual}"); + let expected = quote! { + pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient { + OperationTemplatesLroClient { + api_version: self.api_version.clone(), + endpoint: self.endpoint.clone(), + pipeline: self.pipeline.clone(), + subscription_id: self.subscription_id.clone(), + tracer: self.tracer.clone(), + } + } + }; + assert!( + crate::tracing::tests::compare_token_stream(actual, expected), + "Parsed tokens do not match expected tokens" + ); + } +} From c2ebbf276966dcc7f1b1484e2d228bdfcb4fdcd7 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 16 Jul 2025 10:29:57 -0700 Subject: [PATCH 71/84] Added subclient to cspell.json --- .vscode/cspell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 2d3fd78fbd..334529b6dc 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -73,6 +73,7 @@ "seekable", "servicebus", "stylesheet", + "subclient", "telemetered", "typespec", "undelete", @@ -208,4 +209,4 @@ ] } ] -} \ No newline at end of file +} From 41571b75dac3e2c1bfcfc5cc7eac02ff9e8018a0 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Wed, 16 Jul 2025 11:32:16 -0700 Subject: [PATCH 72/84] Added subclient text to distributed tracing documentation --- ...ibuted-tracing-for-rust-service-clients.md | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/doc/distributed-tracing-for-rust-service-clients.md b/doc/distributed-tracing-for-rust-service-clients.md index ae56b9ecc7..023d1ac4ed 100644 --- a/doc/distributed-tracing-for-rust-service-clients.md +++ b/doc/distributed-tracing-for-rust-service-clients.md @@ -1,6 +1,6 @@ - + # Distributed tracing options in Rust service clients ## Distributed tracing fundamentals @@ -371,3 +371,37 @@ The `PublicApiInstrumentationPolicy` struct has a convenience method `create_pub The client can then add whatever attributes to the span it needs, and after the pipeline has run, can add any additional attributes to the span. Note that in this model, the client is responsible for ending the span. + +### Service implementations with "subclients" + +Service clients can sometimes contain "subclients" - clients which have their own pipelines and endpoint which contain subclient specific functionality. + +Such subclients often have an accessor function to create a new subclient instance which looks like this: + +```rust + +pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient { + OperationTemplatesLroClient { + api_version: self.api_version.clone(), + endpoint: self.endpoint.clone(), + pipeline: self.pipeline.clone(), + subscription_id: self.subscription_id.clone(), + } +} +``` + +To support subclient instantiation, the `azure_core` crate defines an attribute macro named `tracing::subclient` to support subclient instantiation. + +```rust +#[tracing::subclient] +pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient { + OperationTemplatesLroClient { + api_version: self.api_version.clone(), + endpoint: self.endpoint.clone(), + pipeline: self.pipeline.clone(), + subscription_id: self.subscription_id.clone(), + } +} +``` + +This adds a clone of the parent client's `tracer` to the subclient - it functions similarly to `#[tracing::new]` but for subclient instantiation. From 5d95e0278169ff28f9155db2d4c15bb8a5fa50ba Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Fri, 18 Jul 2025 17:08:03 -0700 Subject: [PATCH 73/84] PR feedback part 1 - does not pass tests currently --- Cargo.lock | 2 + .../examples/core_remove_user_agent.rs | 2 +- .../src/http/policies/instrumentation/mod.rs | 230 ----------- .../public_api_instrumentation.rs | 246 +++++------- .../request_instrumentation.rs | 264 +++++++------ sdk/core/azure_core_macros/Cargo.toml | 2 + sdk/core/azure_core_macros/src/tracing.rs | 76 ++-- .../azure_core_macros/src/tracing_client.rs | 3 +- .../azure_core_macros/src/tracing_function.rs | 110 +++--- sdk/core/azure_core_macros/src/tracing_new.rs | 357 +++++++++++++++--- .../tests/telemetry_service_macros.rs | 165 ++++++++ sdk/core/azure_core_test/src/lib.rs | 3 +- sdk/core/azure_core_test/src/tracing.rs | 248 ++++++++++++ .../typespec_client_core/src/http/pipeline.rs | 5 +- .../src/http/policies/retry/exponential.rs | 3 + .../src/http/policies/transport.rs | 5 +- 16 files changed, 1057 insertions(+), 664 deletions(-) create mode 100644 sdk/core/azure_core_test/src/tracing.rs diff --git a/Cargo.lock b/Cargo.lock index bbd2fc51d4..8f023f9611 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,8 @@ dependencies = [ "quote", "syn", "tokio", + "tracing", + "tracing-subscriber", "typespec_client_core", ] diff --git a/sdk/core/azure_core/examples/core_remove_user_agent.rs b/sdk/core/azure_core/examples/core_remove_user_agent.rs index ba4eef5a35..1e258868a6 100644 --- a/sdk/core/azure_core/examples/core_remove_user_agent.rs +++ b/sdk/core/azure_core/examples/core_remove_user_agent.rs @@ -88,7 +88,7 @@ fn setup() -> Result<(Arc, Arc), Box>>, - } - - impl MockTracingProvider { - pub(super) fn new() -> Self { - Self { - tracers: Mutex::new(Vec::new()), - } - } - } - impl TracerProvider for MockTracingProvider { - fn get_tracer( - &self, - azure_namespace: Option<&'static str>, - crate_name: &'static str, - crate_version: &'static str, - ) -> Arc { - let mut tracers = self.tracers.lock().unwrap(); - let tracer = Arc::new(MockTracer { - namespace: azure_namespace, - package_name: crate_name, - package_version: crate_version, - spans: Mutex::new(Vec::new()), - }); - - tracers.push(tracer.clone()); - tracer - } - } - - #[derive(Debug)] - pub(super) struct MockTracer { - pub(super) namespace: Option<&'static str>, - pub(super) package_name: &'static str, - pub(super) package_version: &'static str, - pub(super) spans: Mutex>>, - } - - impl Tracer for MockTracer { - fn namespace(&self) -> Option<&'static str> { - self.namespace - } - - fn start_span_with_current( - &self, - name: &str, - kind: SpanKind, - attributes: Vec, - ) -> Arc { - let span = Arc::new(MockSpan::new(name, kind, attributes)); - self.spans.lock().unwrap().push(span.clone()); - span - } - - fn start_span_with_parent( - &self, - name: &str, - kind: SpanKind, - attributes: Vec, - _parent: Arc, - ) -> Arc { - let span = Arc::new(MockSpan::new(name, kind, attributes)); - self.spans.lock().unwrap().push(span.clone()); - span - } - - fn start_span( - &self, - name: &str, - kind: SpanKind, - attributes: Vec, - ) -> Arc { - let span = Arc::new(MockSpan::new(name, kind, attributes)); - self.spans.lock().unwrap().push(span.clone()); - span - } - } - - #[derive(Debug)] - pub(super) struct MockSpan { - pub(super) name: String, - pub(super) kind: SpanKind, - pub(super) attributes: Mutex>, - pub(super) state: Mutex, - pub(super) is_open: Mutex, - } - impl MockSpan { - fn new(name: &str, kind: SpanKind, attributes: Vec) -> Self { - println!("Creating MockSpan: {}", name); - println!("Attributes: {:?}", attributes); - Self { - name: name.to_string(), - kind, - attributes: Mutex::new(attributes), - state: Mutex::new(SpanStatus::Unset), - is_open: Mutex::new(true), - } - } - } - - impl Span for MockSpan { - fn set_attribute(&self, key: &'static str, value: AttributeValue) { - println!("{}: Setting attribute {}: {:?}", self.name, key, value); - let mut attributes = self.attributes.lock().unwrap(); - attributes.push(Attribute { key, value }); - } - - fn set_status(&self, status: crate::tracing::SpanStatus) { - println!("{}: Setting span status: {:?}", self.name, status); - let mut state = self.state.lock().unwrap(); - *state = status; - } - - fn end(&self) { - println!("Ending span: {}", self.name); - let mut is_open = self.is_open.lock().unwrap(); - *is_open = false; - } - - fn is_recording(&self) -> bool { - true - } - - fn span_id(&self) -> [u8; 8] { - [0; 8] // Mock span ID - } - - fn record_error(&self, _error: &dyn std::error::Error) { - todo!() - } - - fn set_current( - &self, - _context: &Context, - ) -> Box { - todo!() - } - - /// Insert two dummy headers for distributed tracing. - // cspell: ignore traceparent tracestate - fn propagate_headers(&self, request: &mut Request) { - request.insert_header( - HeaderName::from_static("traceparent"), - "00---01", - ); - request.insert_header(HeaderName::from_static("tracestate"), "="); - } - } - - impl AsAny for MockSpan { - fn as_any(&self) -> &dyn std::any::Any { - self - } - } - - pub(super) struct InstrumentationExpectation<'a> { - pub(super) namespace: Option<&'a str>, - pub(super) name: &'a str, - pub(super) version: &'a str, - pub(super) span_name: &'a str, - pub(super) status: SpanStatus, - pub(super) kind: SpanKind, - pub(super) attributes: Vec<(&'a str, AttributeValue)>, - } - pub(super) fn check_request_instrumentation_result( - mock_tracer: Arc, - expected_span_count: usize, - span_index: usize, - expectation: InstrumentationExpectation, - ) { - assert_eq!( - mock_tracer.tracers.lock().unwrap().len(), - 1, - "Expected one tracer to be created", - ); - let tracers = mock_tracer.tracers.lock().unwrap(); - let tracer = tracers.first().unwrap(); - assert_eq!(tracer.package_name, expectation.name); - assert_eq!(tracer.package_version, expectation.version); - assert_eq!(tracer.namespace, expectation.namespace); - let spans = tracer.spans.lock().unwrap(); - assert_eq!( - spans.len(), - expected_span_count, - "Expected one span to be created" - ); - println!("Spans: {:?}", spans); - let span = spans[span_index].as_ref(); - assert_eq!(span.name, expectation.span_name); - assert_eq!(span.kind, expectation.kind); - assert_eq!(*span.state.lock().unwrap(), expectation.status); - let attributes = span.attributes.lock().unwrap(); - for attr in attributes.iter() { - println!("Attribute: {} = {:?}", attr.key, attr.value); - let mut found = false; - for (key, value) in &expectation.attributes { - if attr.key == *key { - assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); - found = true; - break; - } - } - if !found { - panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); - } - } - for (key, value) in &expectation.attributes { - if !attributes - .iter() - .any(|attr| attr.key == *key && attr.value == *value) - { - panic!("Expected attribute not found: {} = {:?}", key, value); - } - } - } -} diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs index b61f3429fe..b22a99bf39 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs @@ -104,26 +104,23 @@ impl PublicApiInstrumentationPolicy { let info = ctx.value::()?; // Get the tracer from either the context or the policy. - let tracer = match ctx.value::>() { - Some(tracer) => Some(tracer), - None => tracer.as_ref(), - }; - - // If we don't have a tracer, skip instrumentation - let tracer = tracer?; + #[allow(clippy::unnecessary_lazy_evaluations)] + let tracer = ctx.value::>().or_else(|| tracer.as_ref())?; // We now have public API information and a tracer. // Calculate the span attributes based on the public API information and // tracer. - let mut span_attributes = vec![]; - - for attr in &info.attributes { - // Add the attributes from the public API information to the span. - span_attributes.push(Attribute { - key: attr.key, - value: attr.value.clone(), - }); - } + let mut span_attributes = info + .attributes + .iter() + .map(|attr| { + // Convert the attribute to a span attribute. + Attribute { + key: attr.key, + value: attr.value.clone(), + } + }) + .collect::>(); if let Some(namespace) = tracer.namespace() { // If the tracer has a namespace, we set it as an attribute. @@ -208,19 +205,22 @@ impl Policy for PublicApiInstrumentationPolicy { mod tests { // cspell: ignore traceparent use super::*; - use crate::http::policies::instrumentation::tests::{ - check_request_instrumentation_result, InstrumentationExpectation, MockTracingProvider, - }; use crate::{ http::{ headers::Headers, policies::{RequestInstrumentationPolicy, TransportPolicy}, Method, RawResponse, StatusCode, TransportOptions, }, - tracing::{AttributeValue, SpanStatus, TracerProvider}, + tracing::{SpanStatus, TracerProvider}, Result, }; - use azure_core_test::http::MockHttpClient; + use azure_core_test::{ + http::MockHttpClient, + tracing::{ + check_instrumentation_result, ExpectedSpanInformation, ExpectedTracerInformation, + MockTracingProvider, + }, + }; use futures::future::BoxFuture; use std::sync::Arc; @@ -326,54 +326,6 @@ mod tests { mock_tracer_provider } - fn check_public_api_instrumentation_result( - mock_tracer: Arc, - span_count: usize, - span_index: usize, - expected_api_name: Option<&str>, - expected_kind: SpanKind, - expected_status: SpanStatus, - expected_attributes: Vec<(&str, AttributeValue)>, - ) { - assert_eq!( - mock_tracer.tracers.lock().unwrap().len(), - 1, - "Expected one tracer to be created", - ); - let tracers = mock_tracer.tracers.lock().unwrap(); - let tracer = tracers.first().unwrap(); - let spans = tracer.spans.lock().unwrap(); - assert_eq!(spans.len(), span_count, "Expected one span to be created"); - println!("Spans: {:?}", spans); - let span = spans[span_index].as_ref(); - assert_eq!(span.name, expected_api_name.unwrap_or("unknown")); - assert_eq!(span.kind, expected_kind); - assert_eq!(*span.state.lock().unwrap(), expected_status); - let attributes = span.attributes.lock().unwrap(); - for attr in attributes.iter() { - println!("Attribute: {} = {:?}", attr.key, attr.value); - let mut found = false; - for (key, value) in &expected_attributes { - if attr.key == *key { - assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); - found = true; - break; - } - } - if !found { - panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); - } - } - for (key, value) in &expected_attributes { - if !attributes - .iter() - .any(|attr| attr.key == *key && attr.value == *value) - { - panic!("Expected attribute not found: {} = {:?}", key, value); - } - } - } - // Tests for the create_public_api_span function. #[test] fn create_public_api_span() { @@ -465,17 +417,14 @@ mod tests { ) .await; - assert_eq!( - mock_tracer.tracers.lock().unwrap().len(), - 1, - "Expected one tracer to be created", - ); - let tracers = mock_tracer.tracers.lock().unwrap(); - let tracer = tracers.first().unwrap(); - assert_eq!( - tracer.spans.lock().unwrap().len(), - 0, - "Expected no spans to be created" + check_instrumentation_result( + mock_tracer, + vec![ExpectedTracerInformation { + name: "test_crate", + version: "1.0.0", + namespace: Some("test namespace"), + spans: vec![], + }], ); } @@ -506,11 +455,8 @@ mod tests { ) .await; - assert_eq!( - mock_tracer.tracers.lock().unwrap().len(), - 0, - "Expected no tracers to be created", - ); + // No tracer should be created, so we expect no spans. + check_instrumentation_result(mock_tracer, vec![]); } #[tokio::test] @@ -540,15 +486,20 @@ mod tests { ) .await; - check_public_api_instrumentation_result( - mock_tracer.clone(), - 1, - 0, - Some("MyClient.MyApi"), - SpanKind::Internal, - SpanStatus::Unset, - vec![], - ); + check_instrumentation_result( + mock_tracer, + vec![ExpectedTracerInformation { + name: "test_crate", + version: "1.0.0", + namespace: None, + spans: vec![ExpectedSpanInformation { + span_name: "MyClient.MyApi", + status: SpanStatus::Unset, + kind: SpanKind::Internal, + attributes: vec![], + }], + }], + ) } #[tokio::test] @@ -578,14 +529,19 @@ mod tests { ) .await; - check_public_api_instrumentation_result( + check_instrumentation_result( mock_tracer, - 1, - 0, - Some("MyClient.MyApi"), - SpanKind::Internal, - SpanStatus::Unset, - vec![(AZ_NAMESPACE_ATTRIBUTE, "test namespace".into())], + vec![ExpectedTracerInformation { + name: "test_crate", + version: "1.0.0", + namespace: Some("test namespace"), + spans: vec![ExpectedSpanInformation { + span_name: "MyClient.MyApi", + status: SpanStatus::Unset, + kind: SpanKind::Internal, + attributes: vec![(AZ_NAMESPACE_ATTRIBUTE, "test namespace".into())], + }], + }], ); } @@ -616,19 +572,24 @@ mod tests { ) .await; - check_public_api_instrumentation_result( - mock_tracer, - 1, - 0, - Some("MyClient.MyApi"), - SpanKind::Internal, - SpanStatus::Error { - description: "".to_string(), - }, - vec![ - (AZ_NAMESPACE_ATTRIBUTE, "test namespace".into()), - (ERROR_TYPE_ATTRIBUTE, "500".into()), - ], + check_instrumentation_result( + mock_tracer.clone(), + vec![ExpectedTracerInformation { + name: "test_crate", + version: "1.0.0", + namespace: Some("test namespace"), + spans: vec![ExpectedSpanInformation { + span_name: "MyClient.MyApi", + status: SpanStatus::Error { + description: "".to_string(), + }, + kind: SpanKind::Internal, + attributes: vec![ + (AZ_NAMESPACE_ATTRIBUTE, "test namespace".into()), + (ERROR_TYPE_ATTRIBUTE, "500".into()), + ], + }], + }], ); } @@ -657,40 +618,37 @@ mod tests { ) .await; - check_public_api_instrumentation_result( - mock_tracer.clone(), - 2, - 0, - Some("MyClient.MyApi"), - SpanKind::Internal, - SpanStatus::Unset, - vec![ - (AZ_NAMESPACE_ATTRIBUTE, "test.namespace".into()), - // Attribute comes from the public API information. - ("az.fake_attribute", "attribute value".into()), - ], - ); - - check_request_instrumentation_result( + check_instrumentation_result( mock_tracer.clone(), - 2, - 1, - InstrumentationExpectation { - namespace: Some("test.namespace"), + vec![ExpectedTracerInformation { name: "test_crate", version: "1.0.0", - span_name: "PUT", - kind: SpanKind::Client, - status: SpanStatus::Unset, - attributes: vec![ - (AZ_NAMESPACE_ATTRIBUTE, "test.namespace".into()), - ("http.request.method", "PUT".into()), - ("url.full", "http://example.com/path_with_request".into()), - ("server.address", "example.com".into()), - ("server.port", 80.into()), - ("http.response.status_code", 200.into()), + namespace: Some("test.namespace"), + spans: vec![ + ExpectedSpanInformation { + span_name: "MyClient.MyApi", + status: SpanStatus::Unset, + kind: SpanKind::Internal, + attributes: vec![ + (AZ_NAMESPACE_ATTRIBUTE, "test.namespace".into()), + ("az.fake_attribute", "attribute value".into()), + ], + }, + ExpectedSpanInformation { + span_name: "PUT", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + (AZ_NAMESPACE_ATTRIBUTE, "test.namespace".into()), + ("http.request.method", "PUT".into()), + ("url.full", "http://example.com/path_with_request".into()), + ("server.address", "example.com".into()), + ("server.port", 80.into()), + ("http.response.status_code", 200.into()), + ], + }, ], - }, + }], ); } } diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs index 0a5639d83d..ee3ec68917 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs @@ -55,11 +55,13 @@ impl Policy for RequestInstrumentationPolicy { // we prefer the tracer from the context. // Otherwise, we use the tracer from the policy itself. // This allows for flexibility in using different tracers in different contexts. - let tracer = if ctx.value::>().is_some() { - ctx.value::>() - } else { - self.tracer.as_ref() - }; + + // We use `.or_else` here instead of `.or` because `.or` eagerly evaluates the right-hand side, + // which can lead to unnecessary overhead if the tracer is not needed. + #[allow(clippy::unnecessary_lazy_evaluations)] + let tracer = ctx + .value::>() + .or_else(|| self.tracer.as_ref()); // If there is a span in the context, if it's not recording, just forward the request // without instrumentation. @@ -152,15 +154,7 @@ impl Policy for RequestInstrumentationPolicy { if let Some(err) = result.as_ref().err() { // If the request failed, set an error type attribute. - let azure_error = err.downcast_ref::(); - if let Some(err_kind) = azure_error.map(|e| e.kind()) { - // If the error is an Azure core error, we set the error type. - span.set_attribute(ERROR_TYPE_ATTRIBUTE, err_kind.to_string().into()); - } else { - // Otherwise, we set the error type to the error's text. This should never happen - // as the error should be an Azure core error. - span.set_attribute(ERROR_TYPE_ATTRIBUTE, err.to_string().into()); - } + span.set_attribute(ERROR_TYPE_ATTRIBUTE, err.kind().to_string().into()); } if let Ok(response) = result.as_ref() { // If the request was successful, set the HTTP response status code. @@ -190,20 +184,19 @@ pub(crate) mod tests { use super::*; use crate::{ http::{ - headers::Headers, - policies::{ - instrumentation::tests::{ - check_request_instrumentation_result, InstrumentationExpectation, - MockTracingProvider, - }, - TransportPolicy, - }, - Method, RawResponse, StatusCode, TransportOptions, + headers::Headers, policies::TransportPolicy, Method, RawResponse, StatusCode, + TransportOptions, }, tracing::{AttributeValue, SpanStatus, TracerProvider}, Result, }; - use azure_core_test::http::MockHttpClient; + use azure_core_test::{ + http::MockHttpClient, + tracing::{ + check_instrumentation_result, ExpectedSpanInformation, ExpectedTracerInformation, + MockTracingProvider, + }, + }; use futures::future::BoxFuture; use std::sync::Arc; use typespec_client_core::http::headers::HeaderName; @@ -261,38 +254,38 @@ pub(crate) mod tests { ) .await; - check_request_instrumentation_result( + check_instrumentation_result( mock_tracer, - 1, - 0, - InstrumentationExpectation { + vec![ExpectedTracerInformation { namespace: Some("test namespace"), name: "test_crate", version: "1.0.0", - span_name: "GET", - status: SpanStatus::Unset, - kind: SpanKind::Client, - attributes: vec![ - ( - AZ_NAMESPACE_ATTRIBUTE, - AttributeValue::from("test namespace"), - ), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(200), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), - ( - SERVER_ADDRESS_ATTRIBUTE, - AttributeValue::from("example.com"), - ), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(80)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("http://example.com/path"), - ), - ], - }, + spans: vec![ExpectedSpanInformation { + span_name: "GET", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(80)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("http://example.com/path"), + ), + ], + }], + }], ); } @@ -343,38 +336,38 @@ pub(crate) mod tests { ) .await; - check_request_instrumentation_result( - mock_tracer.clone(), - 1, - 0, - InstrumentationExpectation { + check_instrumentation_result( + mock_tracer, + vec![ExpectedTracerInformation { namespace: None, name: "test_crate", version: "1.0.0", - span_name: "GET", - status: SpanStatus::Unset, - kind: SpanKind::Client, - attributes: vec![ - ( - AZ_CLIENT_REQUEST_ID_ATTRIBUTE, - AttributeValue::from("test-client-request-id"), - ), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(200), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), - ( - SERVER_ADDRESS_ATTRIBUTE, - AttributeValue::from("example.com"), - ), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("https://example.com/client_request_id"), - ), - ], - }, + spans: vec![ExpectedSpanInformation { + span_name: "GET", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + ( + AZ_CLIENT_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-client-request-id"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("example.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://example.com/client_request_id"), + ), + ], + }], + }], ); } @@ -397,27 +390,27 @@ pub(crate) mod tests { }) .await; // Because the URL contains a username and password, we do not set the URL_FULL_ATTRIBUTE. - check_request_instrumentation_result( + check_instrumentation_result( mock_tracer_provider, - 1, - 0, - InstrumentationExpectation { + vec![ExpectedTracerInformation { namespace: None, name: "unknown", version: "unknown", - span_name: "GET", - status: SpanStatus::Unset, - kind: SpanKind::Client, - attributes: vec![ - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(200), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), - (SERVER_ADDRESS_ATTRIBUTE, AttributeValue::from("host")), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(8080)), - ], - }, + spans: vec![ExpectedSpanInformation { + span_name: "GET", + status: SpanStatus::Unset, + kind: SpanKind::Client, + attributes: vec![ + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(200), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("GET")), + (SERVER_ADDRESS_ATTRIBUTE, AttributeValue::from("host")), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(8080)), + ], + }], + }], ); } @@ -445,49 +438,46 @@ pub(crate) mod tests { }, ) .await; - check_request_instrumentation_result( + + check_instrumentation_result( mock_tracer, - 1, - 0, - InstrumentationExpectation { + vec![ExpectedTracerInformation { namespace: Some("test namespace"), name: "test_crate", version: "1.0.0", - span_name: "PUT", - status: SpanStatus::Error { - description: "".to_string(), - }, - kind: SpanKind::Client, - attributes: vec![ - (ERROR_TYPE_ATTRIBUTE, AttributeValue::from("404")), - ( - AZ_SERVICE_REQUEST_ID_ATTRIBUTE, - AttributeValue::from("test-service-request-id"), - ), - ( - AZ_NAMESPACE_ATTRIBUTE, - AttributeValue::from("test namespace"), - ), - ( - AZ_SERVICE_REQUEST_ID_ATTRIBUTE, - AttributeValue::from("test-service-request-id"), - ), - ( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - AttributeValue::from(404), - ), - (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("PUT")), - ( - SERVER_ADDRESS_ATTRIBUTE, - AttributeValue::from("microsoft.com"), - ), - (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), - ( - URL_FULL_ATTRIBUTE, - AttributeValue::from("https://microsoft.com/request_failed.htm"), - ), - ], - }, + spans: vec![ExpectedSpanInformation { + span_name: "PUT", + status: SpanStatus::Error { + description: "".to_string(), + }, + kind: SpanKind::Client, + attributes: vec![ + (ERROR_TYPE_ATTRIBUTE, AttributeValue::from("404")), + ( + AZ_SERVICE_REQUEST_ID_ATTRIBUTE, + AttributeValue::from("test-service-request-id"), + ), + ( + AZ_NAMESPACE_ATTRIBUTE, + AttributeValue::from("test namespace"), + ), + ( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + AttributeValue::from(404), + ), + (HTTP_REQUEST_METHOD_ATTRIBUTE, AttributeValue::from("PUT")), + ( + SERVER_ADDRESS_ATTRIBUTE, + AttributeValue::from("microsoft.com"), + ), + (SERVER_PORT_ATTRIBUTE, AttributeValue::from(443)), + ( + URL_FULL_ATTRIBUTE, + AttributeValue::from("https://microsoft.com/request_failed.htm"), + ), + ], + }], + }], ); } } diff --git a/sdk/core/azure_core_macros/Cargo.toml b/sdk/core/azure_core_macros/Cargo.toml index a2a941fecf..382853ea21 100644 --- a/sdk/core/azure_core_macros/Cargo.toml +++ b/sdk/core/azure_core_macros/Cargo.toml @@ -21,6 +21,8 @@ proc-macro2.workspace = true quote.workspace = true syn.workspace = true typespec_client_core = { workspace = true, features = ["http", "json"] } +tracing.workspace = true [dev-dependencies] tokio.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } diff --git a/sdk/core/azure_core_macros/src/tracing.rs b/sdk/core/azure_core_macros/src/tracing.rs index abf1074878..8f07bd4c76 100644 --- a/sdk/core/azure_core_macros/src/tracing.rs +++ b/sdk/core/azure_core_macros/src/tracing.rs @@ -3,45 +3,45 @@ #[cfg(test)] pub(crate) mod tests { + use ::tracing::{error, trace}; use proc_macro2::{TokenStream, TokenTree}; + static INIT_LOGGING: std::sync::Once = std::sync::Once::new(); + + pub(crate) fn setup_tracing() { + INIT_LOGGING.call_once(|| { + println!("Setting up test logger..."); + + use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter}; + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .with_ansi(std::env::var("NO_COLOR").map_or(true, |v| v.is_empty())) + .with_writer(std::io::stderr) + .init(); + }); + } // cspell: ignore punct pub(crate) fn compare_token_tree(token: &TokenTree, expected_token: &TokenTree) -> bool { - // println!("Comparing token: {token:?} with expected token: {expected_token:?}"); - match token { - TokenTree::Group(group) => match expected_token { - TokenTree::Group(expected_group) => { - compare_token_stream(group.stream(), expected_group.stream()) - } - _ => { - println!("Unexpected token: {expected_token:?}"); - false - } - }, - TokenTree::Ident(ident) => match expected_token { - TokenTree::Ident(expected_ident) => *expected_ident == *ident, - _ => { - println!("Unexpected token: {expected_token:?}"); - false - } - }, - TokenTree::Punct(punct) => match expected_token { - TokenTree::Punct(expected_punct) => punct.as_char() == expected_punct.as_char(), - _ => { - println!("Unexpected token: {expected_token:?}"); - false - } - }, - TokenTree::Literal(literal) => match expected_token { - TokenTree::Literal(expected_literal) => { - literal.to_string() == expected_literal.to_string() - } - _ => { - println!("Unexpected token: {expected_token:?}"); - false - } - }, + match (token, expected_token) { + (TokenTree::Group(group), TokenTree::Group(expected_group)) => { + compare_token_stream(group.stream(), expected_group.stream()) + } + + (TokenTree::Ident(ident), TokenTree::Ident(expected_ident)) => { + *expected_ident == *ident + } + (TokenTree::Punct(punct), TokenTree::Punct(expected_punct)) => { + punct.as_char() == expected_punct.as_char() + } + (TokenTree::Literal(literal), TokenTree::Literal(expected_literal)) => { + literal.to_string() == expected_literal.to_string() + } + _ => { + error!("Unexpected token: {expected_token:?}"); + false + } } } @@ -50,17 +50,17 @@ pub(crate) mod tests { let expected_tokens = Vec::from_iter(expected); if actual_tokens.len() != expected_tokens.len() { - println!( + error!( "Token lengths do not match: actual: {} != expected: {}", actual_tokens.len(), expected_tokens.len() ); for (i, actual) in actual_tokens.iter().enumerate() { - println!("Actual token at index {i}: {actual:?}"); + trace!("Actual token at index {i}: {actual:?}"); } for (i, expected) in expected_tokens.iter().enumerate() { - println!("Expected token at index {i}: {expected:?}"); + trace!("Expected token at index {i}: {expected:?}"); } return false; } @@ -68,7 +68,7 @@ pub(crate) mod tests { for (actual, expected) in actual_tokens.iter().zip(expected_tokens.iter()) { let equal = compare_token_tree(actual, expected); if !equal { - println!("Tokens do not match: {actual:?} != {expected:?}"); + error!("Tokens do not match: {actual:?} != {expected:?}"); return false; } } diff --git a/sdk/core/azure_core_macros/src/tracing_client.rs b/sdk/core/azure_core_macros/src/tracing_client.rs index c652916ed9..6caf24d8cc 100644 --- a/sdk/core/azure_core_macros/src/tracing_client.rs +++ b/sdk/core/azure_core_macros/src/tracing_client.rs @@ -5,7 +5,8 @@ use proc_macro2::TokenStream; use quote::quote; use syn::{spanned::Spanned, ItemStruct, Result}; -const INVALID_SERVICE_CLIENT_MESSAGE: &str = "client attribute must be applied to a public struct"; +const INVALID_SERVICE_CLIENT_MESSAGE: &str = + "client attribute must be applied to a public struct with no generic type parameters"; /// Parse the token stream for an Azure Service client declaration. /// diff --git a/sdk/core/azure_core_macros/src/tracing_function.rs b/sdk/core/azure_core_macros/src/tracing_function.rs index f1c5049b27..f25d193381 100644 --- a/sdk/core/azure_core_macros/src/tracing_function.rs +++ b/sdk/core/azure_core_macros/src/tracing_function.rs @@ -6,7 +6,7 @@ use quote::{quote, ToTokens}; use syn::{parse::Parse, spanned::Spanned, ItemFn, Member, Result, Token}; const INVALID_PUBLIC_FUNCTION_MESSAGE: &str = - "function attribute must be applied to a public function returning a Result."; + "function attribute must be applied to a public function returning a Result"; // cspell: ignore asyncness @@ -197,100 +197,116 @@ fn is_function_declaration(item: &TokenStream) -> bool { #[cfg(test)] mod tests { + use std::vec; + use syn::parse_quote; use super::*; #[test] fn test_parse_function_name_and_attributes() { - let types = [ - quote! { "Text String" }, - quote! { "Text String", (a = 1, b = 2) }, - ]; - - for stream in types.iter() { - let parsed: FunctionNameAndAttributes = - syn::parse2(stream.clone()).expect("Failed to parse"); - assert_eq!(parsed.function_name, "Text String"); - if !parsed.arguments.is_empty() { - assert_eq!(parsed.arguments.len(), 2); - assert_eq!(parsed.arguments[0].0, "a"); - assert_eq!(parsed.arguments[1].0, "b"); - } - } - { let test_stream = quote! { "Test Function", (arg1 = 42, arg2 = "value") }; let parsed: FunctionNameAndAttributes = syn::parse2(test_stream).expect("Failed to parse"); assert_eq!(parsed.function_name, "Test Function"); - assert_eq!(parsed.arguments.len(), 2); - assert_eq!(parsed.arguments[0].0, "arg1"); - assert_eq!(parsed.arguments[0].1, parse_quote!(42)); - assert_eq!(parsed.arguments[1].0, "arg2"); - assert_eq!(parsed.arguments[1].1, parse_quote!("value")); + assert_eq!( + parsed.arguments, + vec![ + ("arg1".to_string(), parse_quote!(42)), + ("arg2".to_string(), parse_quote!("value")) + ] + ); } + } + #[test] + fn test_parse_function_name_and_attributes_with_string_name() { { let test_stream = quote! { "Test Function", ("az.namespace" = "my namespace", az.test_value = "value") }; let parsed: FunctionNameAndAttributes = syn::parse2(test_stream).expect("Failed to parse"); assert_eq!(parsed.function_name, "Test Function"); - assert_eq!(parsed.arguments.len(), 2); - assert_eq!(parsed.arguments[0].0, "az.namespace"); - assert_eq!(parsed.arguments[0].1, parse_quote!("my namespace")); - assert_eq!(parsed.arguments[1].0, "az.test_value"); - assert_eq!(parsed.arguments[1].1, parse_quote!("value")); + assert_eq!( + parsed.arguments, + vec![ + ("az.namespace".to_string(), parse_quote!("my namespace")), + ("az.test_value".to_string(), parse_quote!("value")), + ] + ); } + } + #[test] + fn test_parse_function_name_and_attributes_with_dotted_name() { { let test_stream = quote! { "Test Function", (az.namespace = "my namespace", az.test_value = "value") }; let parsed: FunctionNameAndAttributes = syn::parse2(test_stream).expect("Failed to parse"); assert_eq!(parsed.function_name, "Test Function"); - assert_eq!(parsed.arguments.len(), 2); - assert_eq!(parsed.arguments[0].0, "az.namespace"); - assert_eq!(parsed.arguments[0].1, parse_quote!("my namespace")); - assert_eq!(parsed.arguments[1].0, "az.test_value"); - assert_eq!(parsed.arguments[1].1, parse_quote!("value")); + assert_eq!( + parsed.arguments, + vec![ + ("az.namespace".to_string(), parse_quote!("my namespace")), + ("az.test_value".to_string(), parse_quote!("value")) + ] + ); } + } + #[test] + fn test_parse_function_name_and_attributes_with_identifier_argument() { { let test_stream = quote! {"macros_get_with_tracing", (az.path = path, az.info = "Test", az.number = 42)}; let parsed: FunctionNameAndAttributes = syn::parse2(test_stream).expect("Failed to parse"); assert_eq!(parsed.function_name, "macros_get_with_tracing"); - assert_eq!(parsed.arguments.len(), 3); - assert_eq!(parsed.arguments[0].0, "az.path"); - assert_eq!(parsed.arguments[0].1, parse_quote!(path)); - - assert_eq!(parsed.arguments[1].0, "az.info"); - assert_eq!(parsed.arguments[1].1, parse_quote!("Test")); - - assert_eq!(parsed.arguments[2].0, "az.number"); - assert_eq!(parsed.arguments[2].1, parse_quote!(42)); + assert_eq!( + parsed.arguments, + vec![ + ("az.path".to_string(), parse_quote!(path)), + ("az.info".to_string(), parse_quote!("Test")), + ("az.number".to_string(), parse_quote!(42)), + ] + ); } + } + #[test] + fn test_parse_function_name_and_attributes_with_identifier_name() { { let test_stream = quote! { "Test Function", (az.foo.bar.namespace = "my namespace", az.test_value = "value") }; let parsed: FunctionNameAndAttributes = syn::parse2(test_stream).expect("Failed to parse"); assert_eq!(parsed.function_name, "Test Function"); - assert_eq!(parsed.arguments.len(), 2); - assert_eq!(parsed.arguments[0].0, "az.foo.bar.namespace"); - assert_eq!(parsed.arguments[0].1, parse_quote!("my namespace")); - assert_eq!(parsed.arguments[1].0, "az.test_value"); - assert_eq!(parsed.arguments[1].1, parse_quote!("value")); + assert_eq!( + parsed.arguments, + vec![ + ( + "az.foo.bar.namespace".to_string(), + parse_quote!("my namespace") + ), + ("az.test_value".to_string(), parse_quote!("value")) + ] + ); } - + } + #[test] + fn test_parse_function_name_and_attributes_with_comma_no_attributes() { { let test_stream = quote! { "Test Function", }; syn::parse2::(test_stream) .expect_err("Should fail to parse."); } + } + #[test] + fn test_parse_function_name_and_attributes_invalid_attribute_name() { { let test_stream = quote! { "Test Function",(23.5= "value") }; syn::parse2::(test_stream) .expect_err("Should fail to parse."); } + } + #[test] + fn test_parse_function_name_and_attributes_empty_attributes() { { let test_stream = quote! { "Test Function", ()}; diff --git a/sdk/core/azure_core_macros/src/tracing_new.rs b/sdk/core/azure_core_macros/src/tracing_new.rs index 382743ee7d..3a1385fa55 100644 --- a/sdk/core/azure_core_macros/src/tracing_new.rs +++ b/sdk/core/azure_core_macros/src/tracing_new.rs @@ -3,10 +3,13 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use syn::{parse::Parse, spanned::Spanned, ExprStruct, ItemFn, Result}; +use syn::{ + parse::Parse, spanned::Spanned, AngleBracketedGenericArguments, ExprStruct, ItemFn, Result, +}; +use tracing::trace; const INVALID_SERVICE_CLIENT_NEW_MESSAGE: &str = - "new attribute must be applied to a public function with a name starting with `new`"; + "new attribute must be applied to a public function which returns Self, a Result and/or Arc containing Self"; fn parse_struct_expr( client_namespace: &str, @@ -16,8 +19,7 @@ fn parse_struct_expr( ) -> TokenStream { if struct_body.path.is_ident("Self") { let fields = struct_body.fields.iter(); - let before_self = quote! { - let tracer = + let tracer_init = quote! { if let Some(tracer_options) = &options.client_options.request_instrumentation { tracer_options .tracer_provider @@ -31,27 +33,24 @@ fn parse_struct_expr( }) } else { None - }; + } }; if is_ok { quote! { - #before_self Ok(Self { - tracer, + tracer: #tracer_init, #(#fields),*, }) } } else { quote! { - #before_self Self { - tracer, + tracer: #tracer_init, #(#fields),*, } } } } else { - println!("ident is not Self, emitting expression: {default:?}"); default } } @@ -87,25 +86,29 @@ pub fn parse_new(attr: TokenStream, item: TokenStream) -> Result { } let namespace_attrs: NamespaceAttribute = syn::parse2(attr)?; - let client_fn: ItemFn = syn::parse2(item.clone())?; + let ItemFn { + vis, + sig, + block, + attrs, + } = syn::parse2(item.clone())?; - let vis = &client_fn.vis; - let ident = &client_fn.sig.ident; - let inputs = client_fn.sig.inputs.iter(); - let body = client_fn.block.stmts.iter().map(|stmt| { + let ident = &sig.ident; + let inputs = sig.inputs.iter(); + let body =block.stmts.iter().map(|stmt| { // Ensure that the body of the new function initializes the `tracer` field. if let syn::Stmt::Expr(expr, _) = stmt { if let syn::Expr::Call(c) = expr { // If the expression is a call, we need to check if it is a struct initialization. if c.args.len() != 1 { - println!("Call expression does not have exactly one argument, emitting expression: {stmt:?}"); + trace!("Call expression does not have exactly one argument, emitting expression: {stmt:?}"); // If the call does not have exactly one argument, just return it as is. - quote! {#stmt} + stmt.to_token_stream() } else if let syn::Expr::Struct(struct_body) = &c.args[0] { parse_struct_expr(namespace_attrs.client_namespace.as_str(), struct_body, stmt.to_token_stream(), true) } else { - println!("Call expression is not a struct, emitting expression: {stmt:?}"); + trace!("Call expression is not a struct, emitting expression: {stmt:?}"); // If the expression is not a struct, just return it as is. stmt.to_token_stream() } @@ -119,8 +122,9 @@ pub fn parse_new(attr: TokenStream, item: TokenStream) -> Result { stmt.to_token_stream() } }); - let output = &client_fn.sig.output; + let output = &sig.output; Ok(quote! { + #(#attrs)* #vis fn #ident(#(#inputs),*) #output { #(#body)* @@ -128,24 +132,129 @@ pub fn parse_new(attr: TokenStream, item: TokenStream) -> Result { }) } +fn is_arc_of_self(path: &syn::Path) -> bool { + let segment = path.segments.last().unwrap(); + if segment.ident != "Arc" { + eprintln!( + "Invalid return type for new function: Arc must be the first segment, found {:?}", + segment.ident + ); + return false; + } + if segment.arguments.is_empty() { + eprintln!( + "Invalid return type for new function: Arc must have arguments, found {:?}", + segment.arguments + ); + return false; + } + match &segment.arguments { + syn::PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) => { + if args.len() != 1 { + eprintln!( + "Invalid return type for new function: Arc must have one argument, found {args:?}", + ); + return false; + } + if let syn::GenericArgument::Type(syn::Type::Path(path)) = &args[0] { + path.path.is_ident("Self") + } else { + eprintln!( + "Invalid return type for new function: Arc argument must be Self, found {:?}", + args[0] + ); + false + } + } + _ => { + eprintln!( + "Invalid return type for new function: Arc arguments must be angle bracketed" + ); + false + } + } +} +fn is_valid_new_return(return_type: &syn::ReturnType) -> bool { + match return_type { + syn::ReturnType::Default => false, + syn::ReturnType::Type(_, ty) => { + let syn::Type::Path(p) = ty.as_ref() else { + println!("Invalid return type for new function, expected path: {ty:?}"); + return false; + }; + if p.path.segments.is_empty() { + println!("Invalid return type for new function: Path is empty"); + return false; + } + if p.path.is_ident("Self") { + true + } else { + // segments.last to allow for std::arc::Arc or azure_core::Result + let segment = p.path.segments.last().unwrap(); + + if segment.ident == "Result" { + match &segment.arguments { + syn::PathArguments::AngleBracketed(AngleBracketedGenericArguments { + args, + .. + }) => { + if args.len() != 1 && args.len() != 2 { + eprintln!("Invalid return type for new function: Result must have one or two arguments"); + return false; + } + if let syn::GenericArgument::Type(syn::Type::Path(path)) = &args[0] { + if path.path.is_ident("Self") { + true + } else { + is_arc_of_self(&path.path) + } + } else { + eprintln!("Invalid return type for new function: Result first argument must be Self, found {:?}", args[0]); + false + } + } + _ => { + eprintln!("Invalid return type for new function: Result arguments must be angle bracketed"); + false + } + } + } else if segment.ident == "Arc" { + is_arc_of_self(&p.path) + } else { + false + } + } + } + } +} + /// Returns true if the item at the head of the token stream is a valid service client declaration. fn is_new_declaration(item: &TokenStream) -> bool { + // The item must be a function declaration. let item_fn: ItemFn = match syn::parse2(item.clone()) { Ok(fn_item) => fn_item, - Err(_) => return false, + Err(e) => { + eprintln!("could not parse new: {e}"); + return false; + } }; // Service clients new functions must be public. if !matches!(item_fn.vis, syn::Visibility::Public(_)) { + eprintln!("Service client new function must be public"); return false; } - // Service clients new functions must have a name that starts with `new_` or "with_". - if !item_fn.sig.ident.to_string().starts_with("new") - && !item_fn.sig.ident.to_string().starts_with("with") - { + // Verify that this function returns a type that is either Self, Result, Arc, or Result, E>. + + if !is_valid_new_return(&item_fn.sig.output) { + eprintln!( + "Invalid return type for new function: {:?}", + item_fn.sig.output + ); return false; } + // Look at the function body to ensure that the last statement is a struct initialization. true } @@ -153,9 +262,44 @@ fn is_new_declaration(item: &TokenStream) -> bool { #[cfg(test)] mod tests { use super::*; + use crate::tracing::tests::setup_tracing; + + #[test] + fn is_new_declaration_valid() { + setup_tracing(); + assert!(is_new_declaration( + "e! {pub fn new_client(a:u32)-> Self { Self {}}} + )); + assert!(is_new_declaration( + "e! {pub fn new_client(a:u32)-> Arc { Arc::new(Self {})}} + )); + assert!(is_new_declaration( + "e! {pub fn new_client(a:u32)-> std::sync::Arc { std::sync::Arc::new(Self {})}} + )); + assert!(is_new_declaration( + "e! {pub fn new_client(a:u32)-> Result { Ok(Self {})}} + )); + assert!(is_new_declaration( + "e! {pub fn new_client(a:u32)-> std::result::Result { Ok(Self {})}} + )); + assert!(is_new_declaration( + "e! {pub fn new_client(a:u32)-> Result> { Ok(Arc::new(Self {}) )}} + )); + + assert!(!is_new_declaration( + "e! {pub fn new_client(a:u32)-> u64 { Ok(Arc::new(Self {}) )}} + )); + assert!(!is_new_declaration( + "e! {pub fn new_client(a:u32)-> Result { Ok(Arc::new(Self {}) )}} + )); + assert!(!is_new_declaration( + "e! {pub fn new_client(a:u32)-> Result> { Ok(Arc::new(Self {}) )}} + )); + } #[test] fn parse_new_function() { + setup_tracing(); let attr = quote!("Az.Namespace"); let item = quote! { pub fn new_service_client(name: &'static str, endpoint: Url) -> Self { @@ -184,24 +328,24 @@ mod tests { name, endpoint, }; - let tracer = if let Some(tracer_options) = - &options.client_options.request_instrumentation - { - tracer_options - .tracer_provider - .as_ref() - .map(|tracer_provider| { - tracer_provider.get_tracer( - Some("Az.Namespace"), - option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), - option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), - ) - }) - } else { - None - }; + Self { - tracer, + tracer: if let Some(tracer_options) = + &options.client_options.request_instrumentation + { + tracer_options + .tracer_provider + .as_ref() + .map(|tracer_provider| { + tracer_provider.get_tracer( + Some("Az.Namespace"), + option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + ) + }) + } else { + None + }, name, endpoint, } @@ -215,6 +359,7 @@ mod tests { #[test] fn parse_generated_new() { + setup_tracing(); let attr = quote!("Az.GeneratedNamespace"); let new_function = quote! { pub fn new( @@ -274,24 +419,23 @@ mod tests { credential, vec!["https://vault.azure.net/.default"], )); - let tracer = if let Some(tracer_options) = - &options.client_options.request_instrumentation - { - tracer_options - .tracer_provider - .as_ref() - .map(|tracer_provider| { - tracer_provider.get_tracer( - Some("Az.GeneratedNamespace"), - option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), - option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), - ) - }) - } else { - None - }; Ok(Self { - tracer, + tracer: if let Some(tracer_options) = + &options.client_options.request_instrumentation + { + tracer_options + .tracer_provider + .as_ref() + .map(|tracer_provider| { + tracer_provider.get_tracer( + Some("Az.GeneratedNamespace"), + option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + ) + }) + } else { + None + }, endpoint, api_version: options.api_version, pipeline: Pipeline::new( @@ -309,4 +453,101 @@ mod tests { "Parsed tokens do not match expected tokens" ); } + + #[test] + fn parse_arc_new() { + setup_tracing(); + let attr = quote!("Az.GeneratedNamespace"); + let new_function = quote! { + pub fn new( + endpoint: &str, + credential: Arc, + options: Option, + ) -> Result> { + let options = options.unwrap_or_default(); + let mut endpoint = Url::parse(endpoint)?; + if !endpoint.scheme().starts_with("http") { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("{endpoint} must use http(s)"), + )); + } + endpoint.set_query(None); + let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( + credential, + vec!["https://vault.azure.net/.default"], + )); + Ok(Arc::new(Self { + endpoint, + api_version: options.api_version, + pipeline: Pipeline::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options.client_options, + Vec::default(), + vec![auth_policy], + ), + })) + } + }; + let actual = + parse_new(attr, new_function).expect("Failed to parse new function declaration"); + + println!("Parsed tokens: {actual}"); + + // I am not at all sure why the parameters to `new` are not being parsed correctly - + // the trailing comma in the `new_function` token stream is not present. + let expected = quote! { + pub fn new( + endpoint: &str, + credential: Arc, + options: Option + ) -> Result> { + let options = options.unwrap_or_default(); + let mut endpoint = Url::parse(endpoint)?; + if !endpoint.scheme().starts_with("http") { + return Err(azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("{endpoint} must use http(s)"), + )); + } + endpoint.set_query(None); + let auth_policy: Arc = Arc::new(BearerTokenCredentialPolicy::new( + credential, + vec!["https://vault.azure.net/.default"], + )); + Ok(Arc::new(Self { + tracer: if let Some(tracer_options) = + &options.client_options.request_instrumentation + { + tracer_options + .tracer_provider + .as_ref() + .map(|tracer_provider| { + tracer_provider.get_tracer( + Some("Az.GeneratedNamespace"), + option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + ) + }) + } else { + None + }, + endpoint, + api_version: options.api_version, + pipeline: Pipeline::new( + option_env!("CARGO_PKG_NAME"), + option_env!("CARGO_PKG_VERSION"), + options.client_options, + Vec::default(), + vec![auth_policy], + ), + })) + } + }; + assert!( + crate::tracing::tests::compare_token_stream(actual, expected), + "Parsed tokens do not match expected tokens" + ); + } } diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs index 695075bb85..4151a96577 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs @@ -173,6 +173,7 @@ impl TestServiceClientWithMacros { mod tests { use super::*; use ::tracing::{info, trace}; + use azure_core::http::{ExponentialRetryOptions, RetryOptions}; use azure_core::tracing::TracerProvider; use azure_core::Result; use azure_core_test::{recorded, TestContext}; @@ -514,4 +515,168 @@ mod tests { Ok(()) } + + #[recorded::test()] + async fn test_macro_service_client_get_with_function_tracing_dns_error( + ctx: TestContext, + ) -> Result<()> { + let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); + let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); + + let recording = ctx.recording(); + let endpoint = "https://example.invalid_top_level_domain"; + let credential = recording.credential().clone(); + let options = TestServiceClientWithMacrosOptions { + client_options: ClientOptions { + request_instrumentation: Some(RequestInstrumentationOptions { + tracer_provider: Some(azure_provider), + }), + retry: Some(RetryOptions::exponential(ExponentialRetryOptions { + max_retries: 3, + ..Default::default() + })), + ..Default::default() + }, + ..Default::default() + }; + + let client = TestServiceClientWithMacros::new(endpoint, credential, Some(options)).unwrap(); + let response = client.get_with_function_tracing("failing_url", None).await; + info!("Response: {:?}", response); + + let spans = otel_exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 5); + for span in &spans { + trace!("Span: {:?}", span); + } + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[4].span_context.span_id()), + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.invalid_top_level_domain".into()), + ("server.port", 443.into()), + ("http.request.resend_count", 0.into()), + ("error.type", "Io".into()), + ], + }, + )?; + verify_span( + &spans[1], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[4].span_context.span_id()), + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.invalid_top_level_domain".into()), + ("server.port", 443.into()), + ("http.request.resend_count", 1.into()), + ("error.type", "Io".into()), + ], + }, + )?; + verify_span( + &spans[2], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[4].span_context.span_id()), + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.invalid_top_level_domain".into()), + ("server.port", 443.into()), + ("http.request.resend_count", 2.into()), + ("error.type", "Io".into()), + ], + }, + )?; + verify_span( + &spans[3], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: Some(spans[4].span_context.span_id()), + status: OpenTelemetrySpanStatus::Unset, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.namespace", "Az.TestServiceClient".into()), + ("az.client.request.id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.invalid_top_level_domain".into()), + ("server.port", 443.into()), + ("http.request.resend_count", 3.into()), + ("error.type", "Io".into()), + ], + }, + )?; + + verify_span( + &spans[4], + ExpectedSpan { + name: "macros_get_with_tracing", + kind: OpenTelemetrySpanKind::Internal, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Error { + description: "Io".into(), + }, + attributes: vec![ + ("az.namespace", "Az.TestServiceClient".into()), + ("error.type", "Io".into()), + ("a.b", 1.into()), // added by tracing macro. + ("az.telemetry", "Abc".into()), // added by tracing macro + ("string attribute", "failing_url".into()), // added by tracing macro. + ], + }, + )?; + + Ok(()) + } } diff --git a/sdk/core/azure_core_test/src/lib.rs b/sdk/core/azure_core_test/src/lib.rs index 2b7ac5f2da..9f86b4b83b 100644 --- a/sdk/core/azure_core_test/src/lib.rs +++ b/sdk/core/azure_core_test/src/lib.rs @@ -11,6 +11,7 @@ mod recording; #[cfg(doctest)] mod root_readme; pub mod stream; +pub mod tracing; use azure_core::Error; pub use azure_core::{error::ErrorKind, test::TestMode}; @@ -177,7 +178,7 @@ impl TestContext { /// * `cargo_dir` - The directory of the Cargo package, typically the value of the `CARGO_MANIFEST_DIR` environment variable. pub fn load_dotenv_file(cargo_dir: impl AsRef) -> azure_core::Result<()> { if let Ok(path) = find_ancestor_file(cargo_dir, ".env") { - tracing::debug!("loading environment variables from {}", path.display()); + ::tracing::debug!("loading environment variables from {}", path.display()); use azure_core::error::ResultExt as _; dotenvy::from_filename(&path).with_context(azure_core::error::ErrorKind::Io, || { diff --git a/sdk/core/azure_core_test/src/tracing.rs b/sdk/core/azure_core_test/src/tracing.rs new file mode 100644 index 0000000000..ee75d5693e --- /dev/null +++ b/sdk/core/azure_core_test/src/tracing.rs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// cspell: ignore traceparent +use std::sync::{Arc, Mutex}; +use tracing::trace; +use typespec_client_core::{ + http::{headers::HeaderName, Context, Request}, + tracing::{ + AsAny, Attribute, AttributeValue, Span, SpanKind, SpanStatus, Tracer, TracerProvider, + }, +}; + +#[derive(Debug)] +pub struct MockTracingProvider { + tracers: Mutex>>, +} + +impl MockTracingProvider { + pub fn new() -> Self { + Self { + tracers: Mutex::new(Vec::new()), + } + } +} + +impl Default for MockTracingProvider { + fn default() -> Self { + Self::new() + } +} + +impl TracerProvider for MockTracingProvider { + fn get_tracer( + &self, + azure_namespace: Option<&'static str>, + crate_name: &'static str, + crate_version: &'static str, + ) -> Arc { + let mut tracers = self.tracers.lock().unwrap(); + let tracer = Arc::new(MockTracer { + namespace: azure_namespace, + package_name: crate_name, + package_version: crate_version, + spans: Mutex::new(Vec::new()), + }); + + tracers.push(tracer.clone()); + tracer + } +} + +#[derive(Debug)] +pub struct MockTracer { + pub namespace: Option<&'static str>, + pub package_name: &'static str, + pub package_version: &'static str, + pub spans: Mutex>>, +} + +impl Tracer for MockTracer { + fn namespace(&self) -> Option<&'static str> { + self.namespace + } + + fn start_span_with_current( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + + fn start_span_with_parent( + &self, + name: &str, + kind: SpanKind, + attributes: Vec, + _parent: Arc, + ) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } + + fn start_span(&self, name: &str, kind: SpanKind, attributes: Vec) -> Arc { + let span = Arc::new(MockSpan::new(name, kind, attributes)); + self.spans.lock().unwrap().push(span.clone()); + span + } +} + +#[derive(Debug)] +pub struct MockSpan { + pub name: String, + pub kind: SpanKind, + pub attributes: Mutex>, + pub state: Mutex, + pub is_open: Mutex, +} +impl MockSpan { + fn new(name: &str, kind: SpanKind, attributes: Vec) -> Self { + println!("Creating MockSpan: {}", name); + println!("Attributes: {:?}", attributes); + Self { + name: name.to_string(), + kind, + attributes: Mutex::new(attributes), + state: Mutex::new(SpanStatus::Unset), + is_open: Mutex::new(true), + } + } +} + +impl Span for MockSpan { + fn set_attribute(&self, key: &'static str, value: AttributeValue) { + println!("{}: Setting attribute {}: {:?}", self.name, key, value); + let mut attributes = self.attributes.lock().unwrap(); + attributes.push(Attribute { key, value }); + } + + fn set_status(&self, status: crate::tracing::SpanStatus) { + println!("{}: Setting span status: {:?}", self.name, status); + let mut state = self.state.lock().unwrap(); + *state = status; + } + + fn end(&self) { + println!("Ending span: {}", self.name); + let mut is_open = self.is_open.lock().unwrap(); + *is_open = false; + } + + fn is_recording(&self) -> bool { + true + } + + fn span_id(&self) -> [u8; 8] { + [0; 8] // Mock span ID + } + + fn record_error(&self, _error: &dyn std::error::Error) { + todo!() + } + + fn set_current(&self, _context: &Context) -> Box { + todo!() + } + + /// Insert two dummy headers for distributed tracing. + // cspell: ignore traceparent tracestate + fn propagate_headers(&self, request: &mut Request) { + request.insert_header( + HeaderName::from_static("traceparent"), + "00---01", + ); + request.insert_header(HeaderName::from_static("tracestate"), "="); + } +} + +impl AsAny for MockSpan { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[derive(Debug)] +pub struct ExpectedTracerInformation<'a> { + pub name: &'a str, + pub version: &'a str, + pub namespace: Option<&'a str>, + pub spans: Vec>, +} + +#[derive(Debug)] +pub struct ExpectedSpanInformation<'a> { + pub span_name: &'a str, + pub status: SpanStatus, + pub kind: SpanKind, + pub attributes: Vec<(&'a str, AttributeValue)>, +} + +pub fn check_instrumentation_result( + mock_tracer: Arc, + expected_tracers: Vec>, +) { + assert_eq!( + mock_tracer.tracers.lock().unwrap().len(), + expected_tracers.len(), + "Unexpected number of tracers", + ); + let tracers = mock_tracer.tracers.lock().unwrap(); + for (index, expectation) in expected_tracers.iter().enumerate() { + trace!("Checking tracer {}: {}", index, expectation.name); + let tracer = &tracers[index]; + assert_eq!(tracer.package_name, expectation.name); + assert_eq!(tracer.package_version, expectation.version); + assert_eq!(tracer.namespace, expectation.namespace); + + let spans = tracer.spans.lock().unwrap(); + assert_eq!( + spans.len(), + expectation.spans.len(), + "Unexpected number of spans for tracer {}", + expectation.name + ); + + for (span_index, span_expectation) in expectation.spans.iter().enumerate() { + println!( + "Checking span {} of tracer {}: {}", + span_index, expectation.name, span_expectation.span_name + ); + check_span_information(&spans[span_index], span_expectation); + } + } +} + +fn check_span_information(span: &Arc, expectation: &ExpectedSpanInformation<'_>) { + assert_eq!(span.name, expectation.span_name); + assert_eq!(span.kind, expectation.kind); + assert_eq!(*span.state.lock().unwrap(), expectation.status); + let attributes = span.attributes.lock().unwrap(); + for (index, attr) in attributes.iter().enumerate() { + println!("Attribute {}: {} = {:?}", index, attr.key, attr.value); + let mut found = false; + for (key, value) in &expectation.attributes { + if attr.key == *key { + assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); + found = true; + break; + } + } + if !found { + panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); + } + } + for (key, value) in &expectation.attributes { + if !attributes + .iter() + .any(|attr| attr.key == *key && attr.value == *value) + { + panic!("Expected attribute not found: {} = {:?}", key, value); + } + } +} diff --git a/sdk/typespec/typespec_client_core/src/http/pipeline.rs b/sdk/typespec/typespec_client_core/src/http/pipeline.rs index d132ff7ff7..75017fdd57 100644 --- a/sdk/typespec/typespec_client_core/src/http/pipeline.rs +++ b/sdk/typespec/typespec_client_core/src/http/pipeline.rs @@ -3,7 +3,7 @@ use crate::http::{ policies::{CustomHeadersPolicy, Policy, TransportPolicy}, - ClientOptions, Context, RawResponse, Request, RetryOptions, + ClientOptions, Context, RawResponse, Request, }; use std::sync::Arc; @@ -49,8 +49,7 @@ impl Pipeline { pipeline.extend_from_slice(&per_call_policies); pipeline.extend_from_slice(&options.per_call_policies); - // TODO: Consider whether this should be initially customizable as we onboard more services. - let retry_policy = RetryOptions::default().to_policy(); + let retry_policy = options.retry.unwrap_or_default().to_policy(); pipeline.push(retry_policy); pipeline.push(Arc::new(CustomHeadersPolicy::default())); diff --git a/sdk/typespec/typespec_client_core/src/http/policies/retry/exponential.rs b/sdk/typespec/typespec_client_core/src/http/policies/retry/exponential.rs index 5845286e86..3e7bfd9935 100644 --- a/sdk/typespec/typespec_client_core/src/http/policies/retry/exponential.rs +++ b/sdk/typespec/typespec_client_core/src/http/policies/retry/exponential.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use tracing::trace; + use super::RetryPolicy; use crate::time::Duration; @@ -26,6 +28,7 @@ impl ExponentialRetryPolicy { max_elapsed: Duration, max_delay: Duration, ) -> Self { + trace!("ExponentialRetryPolicy::new called with initial_delay: {initial_delay:?}, max_retries: {max_retries}, max_elapsed: {max_elapsed:?}, max_delay: {max_delay:?}"); Self { initial_delay: initial_delay.max(Duration::milliseconds(1)), max_retries, diff --git a/sdk/typespec/typespec_client_core/src/http/policies/transport.rs b/sdk/typespec/typespec_client_core/src/http/policies/transport.rs index aa45b92539..77ec87263b 100644 --- a/sdk/typespec/typespec_client_core/src/http/policies/transport.rs +++ b/sdk/typespec/typespec_client_core/src/http/policies/transport.rs @@ -36,10 +36,7 @@ impl Policy for TransportPolicy { assert_eq!(0, next.len()); if request.body().is_empty() - && matches!( - *request.method(), - Method::Patch | Method::Post | Method::Put - ) + && matches!(request.method(), Method::Patch | Method::Post | Method::Put) { request.add_mandatory_header(EMPTY_CONTENT_LENGTH); } From cd99701f00b771324f3d3b52b8a05acdff8a8288 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 21 Jul 2025 11:48:32 -0700 Subject: [PATCH 74/84] Added support for Arc::new --- sdk/core/azure_core_macros/src/tracing_new.rs | 446 ++++++++++++++---- 1 file changed, 354 insertions(+), 92 deletions(-) diff --git a/sdk/core/azure_core_macros/src/tracing_new.rs b/sdk/core/azure_core_macros/src/tracing_new.rs index 3a1385fa55..86af571af0 100644 --- a/sdk/core/azure_core_macros/src/tracing_new.rs +++ b/sdk/core/azure_core_macros/src/tracing_new.rs @@ -6,11 +6,22 @@ use quote::{quote, ToTokens}; use syn::{ parse::Parse, spanned::Spanned, AngleBracketedGenericArguments, ExprStruct, ItemFn, Result, }; -use tracing::trace; +use tracing::{error, trace}; const INVALID_SERVICE_CLIENT_NEW_MESSAGE: &str = "new attribute must be applied to a public function which returns Self, a Result and/or Arc containing Self"; +struct NamespaceAttribute { + client_namespace: String, +} + +impl Parse for NamespaceAttribute { + fn parse(input: syn::parse::ParseStream) -> Result { + let client_namespace = input.parse::()?.value(); + Ok(NamespaceAttribute { client_namespace }) + } +} + fn parse_struct_expr( client_namespace: &str, struct_body: &ExprStruct, @@ -55,14 +66,75 @@ fn parse_struct_expr( } } -struct NamespaceAttribute { - client_namespace: String, +fn is_arc_new_call(func: &syn::Expr) -> bool { + if let syn::Expr::Path(path) = func { + if path.path.segments.len() < 2 { + return false; + } + if path.path.segments[path.path.segments.len() - 2].ident != "Arc" { + return false; + } + if path.path.segments.last().unwrap().ident != "new" { + return false; + } + return true; + } + false } -impl Parse for NamespaceAttribute { - fn parse(input: syn::parse::ParseStream) -> Result { - let client_namespace = input.parse::()?.value(); - Ok(NamespaceAttribute { client_namespace }) +// Parse a function call expression statement that initializes a struct with `Arc::new(Self {})` or `Ok(Arc::new(Self {}))`. +fn parse_call_expr(namespace: &str, call: &syn::ExprCall) -> TokenStream { + debug_assert_eq!( + call.args.len(), + 1, + "Call expression must have exactly one argument" + ); + if let syn::Expr::Path(path) = call.func.as_ref() { + if path.path.segments.last().unwrap().ident == "Ok" { + match call.args.first().unwrap() { + syn::Expr::Struct(struct_body) => { + parse_struct_expr(namespace, struct_body, call.to_token_stream(), true) + } + syn::Expr::Call(call) => { + // Let's make sure that we're doing a call to Arc::new before we recurse. + // Arc::new takes only a single argument, so we can check that first. + if call.args.len() != 1 { + trace!("Call expression does not have exactly one argument, emitting expression: {call:?}"); + return call.to_token_stream(); + } + if is_arc_new_call(call.func.as_ref()) { + let call_expr = parse_call_expr(namespace, call); + quote!(Ok(#call_expr)) + } else { + trace!("Call expression is not Arc::new(), emitting expression: {call:?}"); + call.to_token_stream() + } + } + _ => { + trace!( + "Call expression is not a struct or call, emitting expression: {call:?}" + ); + call.to_token_stream() + } + } + } else if is_arc_new_call(call.func.as_ref()) { + if let syn::Expr::Struct(struct_body) = call.args.first().unwrap() { + let struct_expr = + parse_struct_expr(namespace, struct_body, call.to_token_stream(), false); + quote! { + Arc::new(#struct_expr) + } + } else { + trace!("Call expression is not a struct, emitting expression: {call:?}"); + call.to_token_stream() + } + } else { + trace!("Call expression is not an Arc or Ok, emitting expression: {call:?}"); + call.to_token_stream() + } + } else { + trace!("Call expression is not a path, emitting expression: {call:?}"); + call.to_token_stream() } } @@ -78,12 +150,13 @@ impl Parse for NamespaceAttribute { /// 1) `Result, E>` /// pub fn parse_new(attr: TokenStream, item: TokenStream) -> Result { - if !is_new_declaration(&item) { + if let Err(reason) = is_new_declaration(&item) { return Err(syn::Error::new( item.span(), - INVALID_SERVICE_CLIENT_NEW_MESSAGE, + format!("{INVALID_SERVICE_CLIENT_NEW_MESSAGE}: {reason}"), )); } + let namespace_attrs: NamespaceAttribute = syn::parse2(attr)?; let ItemFn { @@ -98,28 +171,39 @@ pub fn parse_new(attr: TokenStream, item: TokenStream) -> Result { let body =block.stmts.iter().map(|stmt| { // Ensure that the body of the new function initializes the `tracer` field. - if let syn::Stmt::Expr(expr, _) = stmt { - if let syn::Expr::Call(c) = expr { - // If the expression is a call, we need to check if it is a struct initialization. - if c.args.len() != 1 { - trace!("Call expression does not have exactly one argument, emitting expression: {stmt:?}"); - // If the call does not have exactly one argument, just return it as is. - stmt.to_token_stream() - } else if let syn::Expr::Struct(struct_body) = &c.args[0] { - parse_struct_expr(namespace_attrs.client_namespace.as_str(), struct_body, stmt.to_token_stream(), true) - } else { - trace!("Call expression is not a struct, emitting expression: {stmt:?}"); - // If the expression is not a struct, just return it as is. - stmt.to_token_stream() + match stmt { + syn::Stmt::Expr(expr, _) => + match expr { + syn::Expr::Call(c) => { + // If the expression is a call, we need to check if it is a struct initialization. + if c.args.len() != 1 { + trace!("Call expression does not have exactly one argument, emitting statement: {stmt:?}"); + // If the call does not have exactly one argument, just return it as is. + stmt.to_token_stream() + } + else { + parse_call_expr(namespace_attrs.client_namespace.as_str(), c) + } + } + syn::Expr::Struct(struct_body) => { + // If the expression is a struct, we need to parse it. + parse_struct_expr( + namespace_attrs.client_namespace.as_str(), + struct_body, + stmt.to_token_stream(), + false, + ) + } + _ => { + // If the expression is not a struct or call, just return it as is (for + // instance an "if" statement is an expression) + stmt.to_token_stream() + } } - } else if let syn::Expr::Struct(struct_body) = expr { - parse_struct_expr(namespace_attrs.client_namespace.as_str(), struct_body, stmt.to_token_stream(), false) - } else { - // If the expression is not a struct, just return it as is. + _ => { + // If the statement is not an expression, just return it as is. stmt.to_token_stream() } - } else { - stmt.to_token_stream() } }); let output = &sig.output; @@ -132,62 +216,203 @@ pub fn parse_new(attr: TokenStream, item: TokenStream) -> Result { }) } -fn is_arc_of_self(path: &syn::Path) -> bool { +fn is_arc_of_self(path: &syn::Path) -> std::result::Result<(), String> { let segment = path.segments.last().unwrap(); if segment.ident != "Arc" { - eprintln!( + error!( "Invalid return type for new function: Arc must be the first segment, found {:?}", segment.ident ); - return false; + return Err( + "Invalid return type for new function: Arc must be the first segment".to_string(), + ); } if segment.arguments.is_empty() { - eprintln!( + error!( "Invalid return type for new function: Arc must have arguments, found {:?}", segment.arguments ); - return false; + return Err("Invalid return type for new function: Arc must have arguments".to_string()); } match &segment.arguments { syn::PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) => { if args.len() != 1 { - eprintln!( + error!( "Invalid return type for new function: Arc must have one argument, found {args:?}", ); - return false; + return Err( + "Invalid return type for new function: Arc must have one argument".to_string(), + ); } if let syn::GenericArgument::Type(syn::Type::Path(path)) = &args[0] { - path.path.is_ident("Self") + if path.path.is_ident("Self") { + Ok(()) + } else { + error!( + "Invalid return type for new function: Arc argument must be Self, found {:?}", + path.path + ); + Err( + "Invalid return type for new function: Arc argument must be Self" + .to_string(), + ) + } } else { - eprintln!( + error!( "Invalid return type for new function: Arc argument must be Self, found {:?}", args[0] ); - false + Err("Invalid return type for new function: Arc argument must be Self".to_string()) } } _ => { - eprintln!( + error!("Invalid return type for new function: Arc arguments must be angle bracketed"); + Err( "Invalid return type for new function: Arc arguments must be angle bracketed" + .to_string(), + ) + } + } +} + +fn is_valid_arc_of_self_call(expr: &syn::Expr) -> std::result::Result<(), String> { + if let syn::Expr::Struct(struct_body) = expr { + if struct_body.path.is_ident("Self") { + Ok(()) + } else { + error!( + "Invalid new function body: expected struct initialization with Self, found {:?}", + struct_body.path ); - false + Err("expected struct initialization with Self".to_string()) } + } else { + error!( + "Invalid new function body: expected call to `Arc`, found {:?}", + expr + ); + Err("expected last parameter to Arc to be Self".to_string()) } } -fn is_valid_new_return(return_type: &syn::ReturnType) -> bool { +fn is_valid_ok_call( + args: &syn::punctuated::Punctuated, +) -> std::result::Result<(), String> { + if args.len() != 1 { + error!( + "Invalid new function body: expected call to `Ok` with one argument, found {args:?}" + ); + return Err( + "Invalid new function body: expected call to `Ok` with one argument".to_string(), + ); + } + match &args[0] { + syn::Expr::Struct(struct_body) => { + if struct_body.path.is_ident("Self") { + Ok(()) + } else { + error!( + "Invalid new function body: expected struct initialization with Self, found {:?}", + struct_body.path + ); + Err("expected struct initialization with Self".to_string()) + } + } + syn::Expr::Call(call) => { + if call.args.len() == 1 { + if is_arc_new_call(call.func.as_ref()) { + is_valid_arc_of_self_call(call.args.last().unwrap()) + } else { + error!( + "Invalid new function body: expected function named Arc, found {:?}", + call.func.as_ref() + ); + Err("expected Arc path".to_string()) + } + } else { + error!( + "Invalid new function body: expected call to function with one argument, found {:?}", + args[0] + ); + Err("expected call to functions with one argument".to_string()) + } + } + _ => { + error!( + "Invalid new function body: expected a structure or call to function, found {:?}", + args[0] + ); + Err("Invalid new function body: expected a structure or call to function".to_string()) + } + } +} + +fn is_valid_new_body(stmts: &[syn::Stmt]) -> std::result::Result<(), String> { + if stmts.is_empty() { + return Err("New function body must have at least one statement".to_string()); + } + let last_stmt = stmts.last().unwrap(); + if let syn::Stmt::Expr(expr, _) = last_stmt { + match expr { + syn::Expr::Struct(struct_body) => { + if struct_body.path.is_ident("Self") { + Ok(()) + } else { + error!("Invalid new function body: expected struct initialization with Self, found {:?}", struct_body.path); + Err("Expected struct initialization with Self".to_string()) + } + } + syn::Expr::Call(call) => { + if let syn::Expr::Path(path) = call.func.as_ref() { + if path.path.is_ident("Ok") { + is_valid_ok_call(&call.args) + } else if is_arc_new_call(call.func.as_ref()) { + is_valid_arc_of_self_call(call.args.last().unwrap()) + } else { + error!( + "Invalid new function body: expected call to `Ok` or `Arc`, found {:?}", + path + ); + Err("Invalid new function body: expected call to `Ok` or `Arc`".to_string()) + } + } else { + error!( + "Invalid new function body - expected Path, got {:?}", + call.func + ); + Err("Invalid new function body - expected Path".to_string()) + } + } + _ => { + error!( + "Invalid new function body: expected call or struct statement, found {:?}", + last_stmt + ); + Err("Expected call or struct statement".to_string()) + } + } + } else { + error!( + "Invalid new function body: expected expression statement, found {:?}", + last_stmt + ); + Err("Expected final statement to be an expression".to_string()) + } +} + +fn is_valid_new_return(return_type: &syn::ReturnType) -> std::result::Result<(), String> { match return_type { - syn::ReturnType::Default => false, + syn::ReturnType::Default => Err("Default return type is not allowed".to_string()), syn::ReturnType::Type(_, ty) => { let syn::Type::Path(p) = ty.as_ref() else { - println!("Invalid return type for new function, expected path: {ty:?}"); - return false; + error!("Invalid return type for new function, expected path: {ty:?}"); + return Err("Invalid return type for new function, expected path".to_string()); }; if p.path.segments.is_empty() { - println!("Invalid return type for new function: Path is empty"); - return false; + error!("Invalid return type for new function: Path is empty"); + return Err("Invalid return type for new function: Path is empty".to_string()); } if p.path.is_ident("Self") { - true + Ok(()) } else { // segments.last to allow for std::arc::Arc or azure_core::Result let segment = p.path.segments.last().unwrap(); @@ -199,29 +424,29 @@ fn is_valid_new_return(return_type: &syn::ReturnType) -> bool { .. }) => { if args.len() != 1 && args.len() != 2 { - eprintln!("Invalid return type for new function: Result must have one or two arguments"); - return false; + error!("Invalid return type for new function: Result must have one or two arguments"); + return Err("Invalid return type for new function: Result must have one or two arguments".to_string()); } if let syn::GenericArgument::Type(syn::Type::Path(path)) = &args[0] { if path.path.is_ident("Self") { - true + Ok(()) } else { is_arc_of_self(&path.path) } } else { - eprintln!("Invalid return type for new function: Result first argument must be Self, found {:?}", args[0]); - false + error!("Invalid return type for new function: Result first argument must be Self, found {:?}", args[0]); + Err("Invalid return type for new function: Result first argument must be Self".to_string()) } } _ => { - eprintln!("Invalid return type for new function: Result arguments must be angle bracketed"); - false + error!("Invalid return type for new function: Result arguments must be angle bracketed"); + Err("Invalid return type for new function: Result arguments must be angle bracketed".to_string()) } } } else if segment.ident == "Arc" { is_arc_of_self(&p.path) } else { - false + Err("Invalid return type for new function: Expected Self, Result, or Arc".to_string()) } } } @@ -229,34 +454,27 @@ fn is_valid_new_return(return_type: &syn::ReturnType) -> bool { } /// Returns true if the item at the head of the token stream is a valid service client declaration. -fn is_new_declaration(item: &TokenStream) -> bool { +/// +/// # Returns +/// - None if the item is a valid service new declaration. +/// - Some(String) if the item is NOT a valid service new declaration +fn is_new_declaration(item: &TokenStream) -> std::result::Result<(), String> { // The item must be a function declaration. - let item_fn: ItemFn = match syn::parse2(item.clone()) { - Ok(fn_item) => fn_item, - Err(e) => { - eprintln!("could not parse new: {e}"); - return false; - } - }; + let item_fn: ItemFn = syn::parse2(item.clone()) + .map_err(|e| format!("Failed to parse item as function declaration: {e}"))?; // Service clients new functions must be public. if !matches!(item_fn.vis, syn::Visibility::Public(_)) { - eprintln!("Service client new function must be public"); - return false; - } - - // Verify that this function returns a type that is either Self, Result, Arc, or Result, E>. + error!("Service client new function must be public"); + Err("`tracing::new` function must be public".to_string()) + } else { + // Verify that this function returns a type that is either Self, Result, Arc, or Result, E>. + is_valid_new_return(&item_fn.sig.output)?; + // Look at the function body to ensure that the last statement is a struct initialization. + is_valid_new_body(&item_fn.block.stmts)?; - if !is_valid_new_return(&item_fn.sig.output) { - eprintln!( - "Invalid return type for new function: {:?}", - item_fn.sig.output - ); - return false; + Ok(()) } - // Look at the function body to ensure that the last statement is a struct initialization. - - true } #[cfg(test)] @@ -265,36 +483,80 @@ mod tests { use crate::tracing::tests::setup_tracing; #[test] - fn is_new_declaration_valid() { + fn is_new_declaration_simple_self() { + setup_tracing(); + assert!(is_new_declaration("e! {pub fn new_client(a:u32)-> Self { Self {}}}).is_ok()); + } + #[test] + fn is_new_declaration_arc_self() { setup_tracing(); - assert!(is_new_declaration( - "e! {pub fn new_client(a:u32)-> Self { Self {}}} - )); assert!(is_new_declaration( "e! {pub fn new_client(a:u32)-> Arc { Arc::new(Self {})}} - )); + ) + .is_ok()); + } + #[test] + fn is_new_declaration_arc_self_long() { + setup_tracing(); assert!(is_new_declaration( "e! {pub fn new_client(a:u32)-> std::sync::Arc { std::sync::Arc::new(Self {})}} - )); + ).is_ok()); + } + #[test] + fn is_new_declaration_result_self() { + setup_tracing(); assert!(is_new_declaration( "e! {pub fn new_client(a:u32)-> Result { Ok(Self {})}} - )); + ) + .is_ok()); + } + #[test] + fn is_new_declaration_result_self_std_result() { + setup_tracing(); assert!(is_new_declaration( "e! {pub fn new_client(a:u32)-> std::result::Result { Ok(Self {})}} - )); + ).is_ok()); + } + #[test] + fn is_new_declaration_result_arc_self() { + setup_tracing(); assert!(is_new_declaration( "e! {pub fn new_client(a:u32)-> Result> { Ok(Arc::new(Self {}) )}} - )); + ) + .is_ok()); + } + #[test] + fn is_new_declaration_result_arc_self_long() { + setup_tracing(); + assert!(is_new_declaration( + "e! {pub fn new_client(a:u32)-> Result> { Ok(std::sync::Arc::new(Self {}) )}} + ) + .is_ok()); + } + #[test] + fn is_new_declaration_invalid_return_type() { + setup_tracing(); - assert!(!is_new_declaration( + assert!(is_new_declaration( "e! {pub fn new_client(a:u32)-> u64 { Ok(Arc::new(Self {}) )}} - )); - assert!(!is_new_declaration( + ) + .is_err()); + } + #[test] + fn is_new_declaration_result_not_self() { + setup_tracing(); + assert!(is_new_declaration( "e! {pub fn new_client(a:u32)-> Result { Ok(Arc::new(Self {}) )}} - )); - assert!(!is_new_declaration( + ) + .is_err()); + } + #[test] + fn is_new_declaration_result_not_arc_self() { + setup_tracing(); + assert!(is_new_declaration( "e! {pub fn new_client(a:u32)-> Result> { Ok(Arc::new(Self {}) )}} - )); + ) + .is_err()); } #[test] From 2e2105a16bfd7c39363bf6e79f2cdb7e05bab99c Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 21 Jul 2025 14:31:34 -0700 Subject: [PATCH 75/84] Update doc/distributed-tracing-for-rust-service-clients.md Co-authored-by: Liudmila Molkova --- doc/distributed-tracing-for-rust-service-clients.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/distributed-tracing-for-rust-service-clients.md b/doc/distributed-tracing-for-rust-service-clients.md index 023d1ac4ed..e6f85f3c81 100644 --- a/doc/distributed-tracing-for-rust-service-clients.md +++ b/doc/distributed-tracing-for-rust-service-clients.md @@ -107,7 +107,7 @@ When an `azure_core::http::Pipeline` is constructed, if the client options inclu The `RequestInstrumentationPolicy` will do the following: 1) If the `Context` parameter for the `RequestInstrumentationPolicy` contains a `Tracer` value, then the `RequestInstrumentationPolicy` will use that `Tracer` value to create the span, otherwise it will use the pre-configured tracer from when the policy was created. -2) If the `Context` parameter for the `RequestInstrumentationPolicy contains a`Span` value, then the policy will use that span as the parent span for the newly created HTTP request span, otherwise it will create a new span. +2) If the `Context` parameter for the `RequestInstrumentationPolicy` contains a `Span` value, then the policy will use that span as the parent span for the newly created HTTP request span, otherwise it will create a new span. This design means that even if a service public API is not fully instrumented with a `Tracer` or a `Span`, it will still generate some HTTP request traces. From 7cf7fe75a45c37b7eb5d1d1f77b828a2b7117cd7 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 21 Jul 2025 15:06:09 -0700 Subject: [PATCH 76/84] PR feedback --- ...ibuted-tracing-for-rust-service-clients.md | 10 +- sdk/core/azure_core/src/http/pipeline.rs | 6 +- .../src/http/policies/instrumentation/mod.rs | 4 +- .../public_api_instrumentation.rs | 289 ++++++++++-------- .../request_instrumentation.rs | 108 ++++--- .../azure_core_macros/src/tracing_client.rs | 17 +- .../azure_core_macros/src/tracing_function.rs | 2 +- sdk/core/azure_core_macros/src/tracing_new.rs | 16 +- .../src/tracing_subclient.rs | 35 ++- .../src/attributes.rs | 2 +- sdk/core/azure_core_opentelemetry/src/span.rs | 23 +- .../azure_core_opentelemetry/src/telemetry.rs | 13 +- .../azure_core_opentelemetry/src/tracer.rs | 27 +- .../tests/otel_span_tests.rs | 6 +- .../tests/telemetry_service_implementation.rs | 14 +- .../tests/telemetry_service_macros.rs | 16 +- sdk/core/azure_core_test/src/tracing.rs | 73 +++-- .../src/tracing/attributes.rs | 59 +++- .../typespec_client_core/src/tracing/mod.rs | 24 +- 19 files changed, 409 insertions(+), 335 deletions(-) diff --git a/doc/distributed-tracing-for-rust-service-clients.md b/doc/distributed-tracing-for-rust-service-clients.md index 023d1ac4ed..a79bbea441 100644 --- a/doc/distributed-tracing-for-rust-service-clients.md +++ b/doc/distributed-tracing-for-rust-service-clients.md @@ -23,9 +23,8 @@ A "tracer" is a factory for "Spans". A `Tracer` is configured with three paramet * `package name` - this is typically the Cargo package name for the service client (`env!("CARGO_PKG_NAME")`) * `package version` - this is typically the version of the Cargo package for the service client (`env!("CARGO_PKG_VERSION")`) -Tracers have three mechanisms for creating spans: +Tracers have two mechanisms for creating spans: -* Create a new root span. * Create a new child span from a parent span. * Create a new child span from the "current" span (where the "current" span is tracer implementation specific). @@ -115,13 +114,16 @@ Since the namespace attribute is service-client wide, it makes sense to capture ## Convenience Macros -To facilitate the implementation of the three core requirements above, three attribute-like macros are defined for the use of each service. +To facilitate the implementation of the three core requirements above, three attribute-like macros are defined for the use of each service client. + +NOTE: These attributes are only for client library development and are not intended for external customers use - they depend heavily on code which follows the Rust API design guidelines. Those macros are: * `#[tracing::client]` - applied to each service client `struct` declaration. -* `#[tracing::new]` - applied to each service client "constructor". +* `#[tracing::new]` - applied to each service client "constructor" function. * `#[tracing::function]` - applied to each service client "public API". +* `#[tracing::subclient]` - applied to a subclient "constructor" function. ### `#[tracing::client]` diff --git a/sdk/core/azure_core/src/http/pipeline.rs b/sdk/core/azure_core/src/http/pipeline.rs index 97d60d452d..6a1c777068 100644 --- a/sdk/core/azure_core/src/http/pipeline.rs +++ b/sdk/core/azure_core/src/http/pipeline.rs @@ -65,11 +65,7 @@ impl Pipeline { .tracer_provider .as_ref() .map(|tracer_provider| { - tracer_provider.get_tracer( - None, - crate_name.unwrap_or("Unknown"), - crate_version.unwrap_or("0.1.0"), - ) + tracer_provider.get_tracer(None, crate_name.unwrap_or("Unknown"), crate_version) }) } else { None diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/mod.rs b/sdk/core/azure_core/src/http/policies/instrumentation/mod.rs index e80ac3a15e..2cd1f71672 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/mod.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/mod.rs @@ -10,7 +10,7 @@ mod request_instrumentation; // [OpenTelemetrySpans](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md) // and [Azure conventions for open telemetry spans](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.md) const AZ_NAMESPACE_ATTRIBUTE: &str = "az.namespace"; -const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client.request.id"; +const AZ_CLIENT_REQUEST_ID_ATTRIBUTE: &str = "az.client_request_id"; const ERROR_TYPE_ATTRIBUTE: &str = "error.type"; const AZ_SERVICE_REQUEST_ID_ATTRIBUTE: &str = "az.service_request.id"; const HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE: &str = "http.request.resend_count"; @@ -20,6 +20,6 @@ const SERVER_ADDRESS_ATTRIBUTE: &str = "server.address"; const SERVER_PORT_ATTRIBUTE: &str = "server.port"; const URL_FULL_ATTRIBUTE: &str = "url.full"; -pub use public_api_instrumentation::PublicApiInstrumentationInformation; pub(crate) use public_api_instrumentation::PublicApiInstrumentationPolicy; +pub use public_api_instrumentation::{create_public_api_span, PublicApiInstrumentationInformation}; pub(crate) use request_instrumentation::*; diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs index b22a99bf39..0da5824405 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs @@ -24,7 +24,7 @@ use typespec_client_core::{ /// /// If the `PublicApiInstrumentationPolicy` policy detects a `PublicApiInstrumentationInformation` in the context, /// it will create a span with the API name and any additional attributes. -#[derive(SafeDebug)] +#[derive(SafeDebug, Clone)] pub struct PublicApiInstrumentationInformation { /// The name of the API being instrumented. /// @@ -69,77 +69,77 @@ impl PublicApiInstrumentationPolicy { pub fn new(tracer: Option>) -> Self { Self { tracer } } +} - /// Creates a span for the public API instrumentation policy. - /// - /// This function creates a span for the public API instrumentation policy based on the - /// public API information in the context. - /// - /// This function assumes that the `Context` already has a `PublicApiInstrumentationInformation` value, - /// if it is not present, it will return `None`. - /// - /// # Arguments - /// - `ctx`: The context containing the public API information. - /// - `tracer`: An optional tracer to use for creating the span. - /// - /// # Returns - /// An optional span if the public API information is present and a tracer is available. - /// - /// If the context already has a span, it will return `None` to avoid nested spans. - /// If the context does not have a tracer it will use the value of the `tracer` argument. - /// If no tracer can be determined, it will return `None`. - /// - pub fn create_public_api_span( - ctx: &Context, - tracer: Option>, - ) -> Option> { - // If there is a span in the context, we're a nested call, so we just want to forward the request. - if ctx.value::>().is_some() { - trace!("PublicApiPolicy: Nested call detected, forwarding request without instrumentation."); - return None; - } +/// Creates a span for the public API instrumentation policy. +/// +/// This function creates a span for the public API instrumentation policy based on the +/// public API information in the context. +/// +/// If no PublicApiInstrumentationInformation is provided, then this function will look in the `Context` +/// for a `PublicApiInstrumentationInformation` value, if it is not present, it will return `None`. +/// +/// # Arguments +/// - `ctx`: The context containing the public API information. +/// - `tracer`: An optional tracer to use for creating the span. +/// - `public_api_instrumentation`: Optional public API instrumentation information. +/// +/// # Returns +/// An optional span if the public API information is present and a tracer is available. +/// +/// If the context already has a span, it will return `None` to avoid nested spans. +/// If the context does not have a tracer it will use the value of the `tracer` argument. +/// If no tracer can be determined, it will return `None`. +/// +pub fn create_public_api_span( + ctx: &Context, + tracer: Option>, + public_api_instrumentation: Option, +) -> Option> { + // If there is a span in the context, we're a nested call, so we just want to forward the request. + if ctx.value::>().is_some() { + trace!( + "PublicApiPolicy: Nested call detected, forwarding request without instrumentation." + ); + return None; + } - // We next confirm if the context has public API instrumentation information. - // Without a public API information, we skip instrumentation. - let info = ctx.value::()?; - - // Get the tracer from either the context or the policy. - #[allow(clippy::unnecessary_lazy_evaluations)] - let tracer = ctx.value::>().or_else(|| tracer.as_ref())?; - - // We now have public API information and a tracer. - // Calculate the span attributes based on the public API information and - // tracer. - let mut span_attributes = info - .attributes - .iter() - .map(|attr| { - // Convert the attribute to a span attribute. - Attribute { - key: attr.key, - value: attr.value.clone(), - } - }) - .collect::>(); - - if let Some(namespace) = tracer.namespace() { - // If the tracer has a namespace, we set it as an attribute. - span_attributes.push(Attribute { - key: AZ_NAMESPACE_ATTRIBUTE, - value: namespace.into(), - }); - } + // We next confirm if the context has public API instrumentation information. + // Without a public API information, we skip instrumentation. + let info = public_api_instrumentation + .or_else(|| ctx.value::().cloned())?; - // Create a span with the public API information and attributes. - let span = - tracer.start_span_with_current(info.api_name, SpanKind::Internal, span_attributes); + // Get the tracer from either the context or the policy. + let tracer = match ctx.value::>() { + Some(t) => t.clone(), + None => tracer?, + }; - // If nothing is listening to the span, we skip instrumentation. - if !span.is_recording() { - return None; - } - Some(span) + // We now have public API information and a tracer. + // Calculate the span attributes based on the public API information and + // tracer. + let mut span_attributes = info + .attributes + .iter() + .map(|attr| { + // Convert the attribute to a span attribute. + Attribute { + key: attr.key.clone(), + value: attr.value.clone(), + } + }) + .collect::>(); + + if let Some(namespace) = tracer.namespace() { + // If the tracer has a namespace, we set it as an attribute. + span_attributes.push(Attribute { + key: AZ_NAMESPACE_ATTRIBUTE.into(), + value: namespace.into(), + }); } + + // Create a span with the public API information and attributes. + Some(tracer.start_span(info.api_name, SpanKind::Internal, span_attributes)) } #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] @@ -151,7 +151,7 @@ impl Policy for PublicApiInstrumentationPolicy { request: &mut Request, next: &[Arc], ) -> PolicyResult { - let Some(span) = Self::create_public_api_span(ctx, self.tracer.clone()) else { + let Some(span) = create_public_api_span(ctx, self.tracer.clone(), None) else { return next[0].send(ctx, request, &next[1..]).await; }; @@ -160,39 +160,45 @@ impl Policy for PublicApiInstrumentationPolicy { let result = next[0].send(&ctx, request, &next[1..]).await; - match &result { - Err(e) => { - // If the request failed, we set the error type on the span. - match e.kind() { - crate::error::ErrorKind::HttpResponse { status, .. } => { - span.set_attribute(ERROR_TYPE_ATTRIBUTE, status.to_string().into()); - - // 5xx status codes SHOULD set status to Error. - // The description should not be set because it can be inferred from "http.response.status_code". - if status.is_server_error() { + // Don't bother setting attributes if the span isn't recording. + if span.is_recording() { + match &result { + Err(e) => { + // If the request failed, we set the error type on the span. + match e.kind() { + crate::error::ErrorKind::HttpResponse { status, .. } => { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, status.to_string().into()); + + // 5xx status codes SHOULD set status to Error. + // The description should not be set because it can be inferred from "http.response.status_code". + if status.is_server_error() { + span.set_status(crate::tracing::SpanStatus::Error { + description: "".to_string(), + }); + } + } + _ => { + span.set_attribute(ERROR_TYPE_ATTRIBUTE, e.kind().to_string().into()); span.set_status(crate::tracing::SpanStatus::Error { - description: "".to_string(), + description: e.kind().to_string(), }); } } - _ => { - span.set_attribute(ERROR_TYPE_ATTRIBUTE, e.kind().to_string().into()); + } + Ok(response) => { + // 5xx status codes SHOULD set status to Error. + // The description should not be set because it can be inferred from "http.response.status_code". + if response.status().is_server_error() { span.set_status(crate::tracing::SpanStatus::Error { - description: e.kind().to_string(), + description: "".to_string(), }); } - } - } - Ok(response) => { - // 5xx status codes SHOULD set status to Error. - // The description should not be set because it can be inferred from "http.response.status_code". - if response.status().is_server_error() { - span.set_status(crate::tracing::SpanStatus::Error { - description: "".to_string(), - }); - } - if response.status().is_client_error() || response.status().is_server_error() { - span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); + if response.status().is_client_error() || response.status().is_server_error() { + span.set_attribute( + ERROR_TYPE_ATTRIBUTE, + response.status().to_string().into(), + ); + } } } } @@ -208,7 +214,7 @@ mod tests { use crate::{ http::{ headers::Headers, - policies::{RequestInstrumentationPolicy, TransportPolicy}, + policies::{create_public_api_span, RequestInstrumentationPolicy, TransportPolicy}, Method, RawResponse, StatusCode, TransportOptions, }, tracing::{SpanStatus, TracerProvider}, @@ -242,7 +248,7 @@ mod tests { Some(mock_tracer_provider.get_tracer( add_tracer_to_context.then_some("test namespace"), "test_crate", - "1.0.0", + Some("1.0.0"), )) } else { None @@ -288,11 +294,8 @@ mod tests { C: FnMut(&Request) -> BoxFuture<'_, Result> + Send + Sync + 'static, { let mock_tracer_provider = Arc::new(MockTracingProvider::new()); - let mock_tracer = mock_tracer_provider.get_tracer( - namespace, - crate_name.unwrap_or("unknown"), - version.unwrap_or("unknown"), - ); + let mock_tracer = + mock_tracer_provider.get_tracer(namespace, crate_name.unwrap_or("unknown"), version); let public_api_policy = Arc::new(PublicApiInstrumentationPolicy::new(Some( mock_tracer.clone(), @@ -312,7 +315,7 @@ mod tests { let public_api_information = PublicApiInstrumentationInformation { api_name: api_name.unwrap_or("unknown"), attributes: vec![Attribute { - key: "az.fake_attribute", + key: "az.fake_attribute".into(), value: "attribute value".into(), }], }; @@ -328,41 +331,79 @@ mod tests { // Tests for the create_public_api_span function. #[test] - fn create_public_api_span() { - let tracer = Arc::new(MockTracingProvider::new()).get_tracer(Some("test"), "test", "1.0.0"); + fn create_public_api_span_tests() { + let tracer = + Arc::new(MockTracingProvider::new()).get_tracer(Some("test"), "test", Some("1.0.0")); // Test when context has no PublicApiInstrumentationInformation { let ctx = Context::default(); - let span = - PublicApiInstrumentationPolicy::create_public_api_span(&ctx, Some(tracer.clone())); + let span = create_public_api_span(&ctx, Some(tracer.clone()), None); assert!(span.is_none(), "Should return None when no API info exists"); } - // Test when context already has a span + } + + // Test when context already has a span + #[test] + fn create_public_api_span_tests_context_has_span() { + let tracer = + Arc::new(MockTracingProvider::new()).get_tracer(Some("test"), "test", Some("1.0.0")); { let existing_span = tracer.start_span("existing", SpanKind::Internal, vec![]); let ctx = Context::default().with_value(existing_span.clone()); - let span = - PublicApiInstrumentationPolicy::create_public_api_span(&ctx, Some(tracer.clone())); + let span = create_public_api_span(&ctx, Some(tracer.clone()), None); assert!( span.is_none(), "Should return None when context already has a span" ); } - // Test with API info but no tracer + } + + // Tests for the create_public_api_span function. + #[test] + fn create_public_api_span_tests_public_api_information_from_param() { + let tracer = + Arc::new(MockTracingProvider::new()).get_tracer(Some("test"), "test", Some("1.0.0")); + + // Test when context has no PublicApiInstrumentationInformation + { + let ctx = Context::default(); + let span = create_public_api_span( + &ctx, + Some(tracer.clone()), + Some(PublicApiInstrumentationInformation { + api_name: "TestClient.test_api", + attributes: vec![], + }), + ); + assert!( + span.is_some(), + "Should return Some when info exists as param" + ); + } + } + + // Test with API info but no tracer + #[test] + fn create_public_api_span_tests_public_api_info_no_tracer() { { let api_info = PublicApiInstrumentationInformation { api_name: "TestClient.test_api", attributes: vec![], }; let ctx = Context::default().with_value(api_info); - let span = PublicApiInstrumentationPolicy::create_public_api_span(&ctx, None); + let span = create_public_api_span(&ctx, None, None); assert!( span.is_none(), "Should return None when no tracer is available" ); } - // Test with API info and tracer from context + } + // Test with API info and tracer from context + #[test] + fn create_public_api_span_tests_api_info_and_tracer_from_context() { + let tracer = + Arc::new(MockTracingProvider::new()).get_tracer(Some("test"), "test", Some("1.0.0")); { let api_info = PublicApiInstrumentationInformation { api_name: "TestClient.test_api", @@ -371,24 +412,28 @@ mod tests { let ctx = Context::default() .with_value(api_info) .with_value(tracer.clone()); - let span = PublicApiInstrumentationPolicy::create_public_api_span(&ctx, None); + let span = create_public_api_span(&ctx, None, None); assert!( span.is_some(), "Should create span when API info and tracer are available" ); } - // Test with API info, tracer from parameter, and attributes + } + // Test with API info, tracer from parameter, and attributes + #[test] + fn create_public_api_span_tests_tracer_from_parameter() { + let tracer = + Arc::new(MockTracingProvider::new()).get_tracer(Some("test"), "test", Some("1.0.0")); { let api_info = PublicApiInstrumentationInformation { api_name: "TestClient.test_api", attributes: vec![Attribute { - key: "test.attribute", + key: "test.attribute".into(), value: "test_value".into(), }], }; let ctx = Context::default().with_value(api_info); - let span = - PublicApiInstrumentationPolicy::create_public_api_span(&ctx, Some(tracer.clone())); + let span = create_public_api_span(&ctx, Some(tracer.clone()), None); assert!(span.is_some(), "Should create span with attributes"); } } @@ -421,7 +466,7 @@ mod tests { mock_tracer, vec![ExpectedTracerInformation { name: "test_crate", - version: "1.0.0", + version: Some("1.0.0"), namespace: Some("test namespace"), spans: vec![], }], @@ -490,7 +535,7 @@ mod tests { mock_tracer, vec![ExpectedTracerInformation { name: "test_crate", - version: "1.0.0", + version: Some("1.0.0"), namespace: None, spans: vec![ExpectedSpanInformation { span_name: "MyClient.MyApi", @@ -533,7 +578,7 @@ mod tests { mock_tracer, vec![ExpectedTracerInformation { name: "test_crate", - version: "1.0.0", + version: Some("1.0.0"), namespace: Some("test namespace"), spans: vec![ExpectedSpanInformation { span_name: "MyClient.MyApi", @@ -576,7 +621,7 @@ mod tests { mock_tracer.clone(), vec![ExpectedTracerInformation { name: "test_crate", - version: "1.0.0", + version: Some("1.0.0"), namespace: Some("test namespace"), spans: vec![ExpectedSpanInformation { span_name: "MyClient.MyApi", @@ -622,7 +667,7 @@ mod tests { mock_tracer.clone(), vec![ExpectedTracerInformation { name: "test_crate", - version: "1.0.0", + version: Some("1.0.0"), namespace: Some("test.namespace"), spans: vec![ ExpectedSpanInformation { diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs index ee3ec68917..2934805645 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs @@ -63,28 +63,19 @@ impl Policy for RequestInstrumentationPolicy { .value::>() .or_else(|| self.tracer.as_ref()); - // If there is a span in the context, if it's not recording, just forward the request - // without instrumentation. - if let Some(span) = ctx.value::>() { - if !span.is_recording() { - // If the span is not recording, we skip instrumentation. - return next[0].send(ctx, request, &next[1..]).await; - } - } - let Some(tracer) = tracer else { return next[0].send(ctx, request, &next[1..]).await; }; let mut span_attributes = vec![Attribute { - key: HTTP_REQUEST_METHOD_ATTRIBUTE, + key: HTTP_REQUEST_METHOD_ATTRIBUTE.into(), value: request.method().to_string().into(), }]; if let Some(namespace) = tracer.namespace() { // If the tracer has a namespace, we set it as an attribute. span_attributes.push(Attribute { - key: AZ_NAMESPACE_ATTRIBUTE, + key: AZ_NAMESPACE_ATTRIBUTE.into(), value: namespace.into(), }); } @@ -94,20 +85,20 @@ impl Policy for RequestInstrumentationPolicy { // the url contains a username or password, we simply omit the URL_FULL_ATTRIBUTE. if request.url().username().is_empty() && request.url().password().is_none() { span_attributes.push(Attribute { - key: URL_FULL_ATTRIBUTE, + key: URL_FULL_ATTRIBUTE.into(), value: request.url().to_string().into(), }); } if let Some(host) = request.url().host() { span_attributes.push(Attribute { - key: SERVER_ADDRESS_ATTRIBUTE, + key: SERVER_ADDRESS_ATTRIBUTE.into(), value: host.to_string().into(), }); } if let Some(port) = request.url().port_or_known_default() { span_attributes.push(Attribute { - key: SERVER_PORT_ATTRIBUTE, + key: SERVER_PORT_ATTRIBUTE.into(), value: port.into(), }); } @@ -124,27 +115,28 @@ impl Policy for RequestInstrumentationPolicy { } else { // If no parent span exists, start a new span with the "current" span (if any). // It is up to the tracer implementation to determine what "current" means. - tracer.start_span_with_current(method_str, SpanKind::Client, span_attributes) + tracer.start_span(method_str, SpanKind::Client, span_attributes) }; - if !span.is_recording() { - // If the span is not recording, we skip instrumentation. - return next[0].send(ctx, request, &next[1..]).await; - } - - if let Some(client_request_id) = request - .headers() - .get_optional_str(&headers::CLIENT_REQUEST_ID) - { - span.set_attribute(AZ_CLIENT_REQUEST_ID_ATTRIBUTE, client_request_id.into()); - } + if (span.is_recording()) { + if let Some(client_request_id) = request + .headers() + .get_optional_str(&headers::CLIENT_REQUEST_ID) + { + span.set_attribute(AZ_CLIENT_REQUEST_ID_ATTRIBUTE, client_request_id.into()); + } - if let Some(service_request_id) = request.headers().get_optional_str(&headers::REQUEST_ID) { - span.set_attribute(AZ_SERVICE_REQUEST_ID_ATTRIBUTE, service_request_id.into()); - } + if let Some(service_request_id) = + request.headers().get_optional_str(&headers::REQUEST_ID) + { + span.set_attribute(AZ_SERVICE_REQUEST_ID_ATTRIBUTE, service_request_id.into()); + } - if let Some(retry_count) = ctx.value::() { - span.set_attribute(HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, (**retry_count).into()); + if let Some(retry_count) = ctx.value::() { + if **retry_count > 0 { + span.set_attribute(HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, (**retry_count).into()); + } + } } // Propagate the headers for distributed tracing into the request. @@ -152,28 +144,29 @@ impl Policy for RequestInstrumentationPolicy { let result = next[0].send(ctx, request, &next[1..]).await; - if let Some(err) = result.as_ref().err() { - // If the request failed, set an error type attribute. - span.set_attribute(ERROR_TYPE_ATTRIBUTE, err.kind().to_string().into()); - } - if let Ok(response) = result.as_ref() { - // If the request was successful, set the HTTP response status code. - span.set_attribute( - HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, - u16::from(response.status()).into(), - ); - - if response.status().is_server_error() || response.status().is_client_error() { - // If the response status indicates an error, set the span status to error. - // Since the reason can be inferred from the status code, description is left empty. - span.set_status(crate::tracing::SpanStatus::Error { - description: "".to_string(), - }); - // Set the error type attribute for all HTTP 4XX or 5XX errors. - span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); + if (span.is_recording()) { + if let Some(err) = result.as_ref().err() { + // If the request failed, set an error type attribute. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, err.kind().to_string().into()); + } + if let Ok(response) = result.as_ref() { + // If the request was successful, set the HTTP response status code. + span.set_attribute( + HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, + u16::from(response.status()).into(), + ); + + if response.status().is_server_error() || response.status().is_client_error() { + // If the response status indicates an error, set the span status to error. + // Since the reason can be inferred from the status code, description is left empty. + span.set_status(crate::tracing::SpanStatus::Error { + description: "".to_string(), + }); + // Set the error type attribute for all HTTP 4XX or 5XX errors. + span.set_attribute(ERROR_TYPE_ATTRIBUTE, response.status().to_string().into()); + } } } - span.end(); return result; } @@ -215,7 +208,7 @@ pub(crate) mod tests { let tracer = mock_tracer_provider.get_tracer( test_namespace, crate_name.unwrap_or("unknown"), - version.unwrap_or("unknown"), + version, ); let policy = Arc::new(RequestInstrumentationPolicy::new(Some(tracer.clone()))); @@ -259,7 +252,7 @@ pub(crate) mod tests { vec![ExpectedTracerInformation { namespace: Some("test namespace"), name: "test_crate", - version: "1.0.0", + version: Some("1.0.0"), spans: vec![ExpectedSpanInformation { span_name: "GET", status: SpanStatus::Unset, @@ -295,7 +288,8 @@ pub(crate) mod tests { assert!(policy.tracer.is_none()); let mock_tracer_provider = Arc::new(MockTracingProvider::new()); - let tracer = mock_tracer_provider.get_tracer(Some("test namespace"), "test_crate", "1.0.0"); + let tracer = + mock_tracer_provider.get_tracer(Some("test namespace"), "test_crate", Some("1.0.0")); let policy_with_tracer = RequestInstrumentationPolicy::new(Some(tracer)); assert!(policy_with_tracer.tracer.is_some()); } @@ -341,7 +335,7 @@ pub(crate) mod tests { vec![ExpectedTracerInformation { namespace: None, name: "test_crate", - version: "1.0.0", + version: Some("1.0.0"), spans: vec![ExpectedSpanInformation { span_name: "GET", status: SpanStatus::Unset, @@ -395,7 +389,7 @@ pub(crate) mod tests { vec![ExpectedTracerInformation { namespace: None, name: "unknown", - version: "unknown", + version: None, spans: vec![ExpectedSpanInformation { span_name: "GET", status: SpanStatus::Unset, @@ -444,7 +438,7 @@ pub(crate) mod tests { vec![ExpectedTracerInformation { namespace: Some("test namespace"), name: "test_crate", - version: "1.0.0", + version: Some("1.0.0"), spans: vec![ExpectedSpanInformation { span_name: "PUT", status: SpanStatus::Error { diff --git a/sdk/core/azure_core_macros/src/tracing_client.rs b/sdk/core/azure_core_macros/src/tracing_client.rs index 6caf24d8cc..c401d16419 100644 --- a/sdk/core/azure_core_macros/src/tracing_client.rs +++ b/sdk/core/azure_core_macros/src/tracing_client.rs @@ -19,11 +19,11 @@ pub fn parse_client(_attr: TokenStream, item: TokenStream) -> Result Result bool { - let item_struct: ItemStruct = match syn::parse2(item.clone()) { + let ItemStruct { vis, generics, .. } = match syn::parse2(item.clone()) { Ok(struct_item) => struct_item, Err(_) => return false, }; + if !generics.params.is_empty() { + // Service clients must not have generic type parameters. + return false; + } + // Service clients must be public structs. - if !matches!(item_struct.vis, syn::Visibility::Public(_)) { + if !matches!(vis, syn::Visibility::Public(_)) { return false; } true diff --git a/sdk/core/azure_core_macros/src/tracing_function.rs b/sdk/core/azure_core_macros/src/tracing_function.rs index f25d193381..cb29d97426 100644 --- a/sdk/core/azure_core_macros/src/tracing_function.rs +++ b/sdk/core/azure_core_macros/src/tracing_function.rs @@ -49,7 +49,7 @@ pub fn parse_function(attr: TokenStream, item: TokenStream) -> Result>(); diff --git a/sdk/core/azure_core_macros/src/tracing_new.rs b/sdk/core/azure_core_macros/src/tracing_new.rs index 86af571af0..54b113d30d 100644 --- a/sdk/core/azure_core_macros/src/tracing_new.rs +++ b/sdk/core/azure_core_macros/src/tracing_new.rs @@ -39,7 +39,7 @@ fn parse_struct_expr( tracer_provider.get_tracer( Some(#client_namespace), option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), - option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION"), ) }) } else { @@ -330,10 +330,10 @@ fn is_valid_ok_call( } } else { error!( - "Invalid new function body: expected call to function with one argument, found {:?}", - args[0] - ); - Err("expected call to functions with one argument".to_string()) + "Invalid new function body: expected call to function with one argument, found {:?}", + args[0] + ); + Err("expected call to Arc with one argument".to_string()) } } _ => { @@ -602,7 +602,7 @@ mod tests { tracer_provider.get_tracer( Some("Az.Namespace"), option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), - option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION"), ) }) } else { @@ -692,7 +692,7 @@ mod tests { tracer_provider.get_tracer( Some("Az.GeneratedNamespace"), option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), - option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION"), ) }) } else { @@ -789,7 +789,7 @@ mod tests { tracer_provider.get_tracer( Some("Az.GeneratedNamespace"), option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), - option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION"), ) }) } else { diff --git a/sdk/core/azure_core_macros/src/tracing_subclient.rs b/sdk/core/azure_core_macros/src/tracing_subclient.rs index cbea8101f0..4773d8c792 100644 --- a/sdk/core/azure_core_macros/src/tracing_subclient.rs +++ b/sdk/core/azure_core_macros/src/tracing_subclient.rs @@ -4,8 +4,10 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::{spanned::Spanned, ExprStruct, ItemFn, Result}; +use tracing::error; -const INVALID_SUBCLIENT_MESSAGE: &str = "subclient attribute must be applied to a public function named `get__client` that returns a client type"; +const INVALID_SUBCLIENT_MESSAGE: &str = + "subclient attribute must be applied to a public function which returns a client type"; /// Parse the token stream for an Azure Service subclient declaration. /// @@ -42,29 +44,30 @@ pub fn parse_subclient(_attr: TokenStream, item: TokenStream) -> Result bool { - let fn_struct: ItemFn = match syn::parse2(item.clone()) { + let ItemFn { + vis, block, sig, .. + } = match syn::parse2(item.clone()) { Ok(fn_item) => fn_item, - Err(_) => return false, + Err(e) => { + error!("Failed to parse function: {}", e); + return false; + } }; // Subclient constructors must be public functions. - if !matches!(fn_struct.vis, syn::Visibility::Public(_)) { + if !matches!(vis, syn::Visibility::Public(_)) { + error!("Subclient constructors must be public functions"); return false; } - let fn_body = fn_struct.block; // Subclient constructors must have a body with a single statement. - if fn_body.stmts.len() != 1 { - return false; - } - - // Subclient constructors must have a name that starts with `get_`. - if !fn_struct.sig.ident.to_string().starts_with("get_") { + if block.stmts.len() != 1 { + error!("Subclient constructors must have a single statement in their body"); return false; } // Subclient constructors must have a return type that is a client type. - if let syn::ReturnType::Type(_, ty) = &fn_struct.sig.output { + if let syn::ReturnType::Type(_, ty) = &sig.output { if !matches!(ty.as_ref(), syn::Type::Path(p) if p.path.segments.last().unwrap().ident.to_string().ends_with("Client")) { return false; @@ -79,11 +82,14 @@ fn is_subclient_declaration(item: &TokenStream) -> bool { #[cfg(test)] mod tests { use super::*; + use crate::tracing::tests::setup_tracing; use proc_macro2::TokenStream; use quote::quote; + use tracing::trace; #[test] fn test_is_subclient_declaration() { + setup_tracing(); assert!(is_subclient_declaration("e! { pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient { OperationTemplatesLroClient { @@ -99,7 +105,7 @@ mod tests { pub fn not_a_subclient() {} })); - assert!(!is_subclient_declaration("e! { + assert!(is_subclient_declaration("e! { pub fn operation_templates_lro_client() -> OperationTemplatesLroClient { OperationTemplatesLroClient { api_version: "2021-01-01".to_string(), @@ -113,6 +119,7 @@ mod tests { #[test] fn test_parse_subclient() { + setup_tracing(); let attr = TokenStream::new(); let item = quote! { pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient { @@ -127,7 +134,7 @@ mod tests { let actual = parse_subclient(attr.clone(), item.clone()) .expect("Failed to parse subclient declaration"); - println!("Actual:{actual}"); + trace!("Actual:{actual}"); let expected = quote! { pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient { OperationTemplatesLroClient { diff --git a/sdk/core/azure_core_opentelemetry/src/attributes.rs b/sdk/core/azure_core_opentelemetry/src/attributes.rs index 2a87b25199..086ccfa131 100644 --- a/sdk/core/azure_core_opentelemetry/src/attributes.rs +++ b/sdk/core/azure_core_opentelemetry/src/attributes.rs @@ -66,7 +66,7 @@ impl From> for AttributeArray { impl From for KeyValue { fn from(attr: OpenTelemetryAttribute) -> Self { KeyValue::new( - attr.0.key, + opentelemetry::Key::from(attr.0.key.to_string()), opentelemetry::Value::from(AttributeValue(attr.0.value)), ) } diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 2c1137f26a..331e0b3063 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -166,7 +166,8 @@ mod tests { 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("Microsoft.SpecialCase"), "test", "0.1.0"); + let tracer = + tracer_provider.get_tracer(Some("Microsoft.SpecialCase"), "test", Some("0.1.0")); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); span.end(); @@ -186,7 +187,7 @@ mod tests { 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("Microsoft.SpecialCase"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Microsoft.SpecialCase"), "test", None); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let mut request = azure_core::http::Request::new( Url::parse("http://example.com").unwrap(), @@ -208,7 +209,7 @@ mod tests { fn test_open_telemetry_span_hierarchy() { 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("Special Name"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Special Name"), "test", Some("0.1.0")); let parent_span = tracer.start_span("parent_span", SpanKind::Server, vec![]); let child_span = tracer.start_span_with_parent( "child_span", @@ -239,7 +240,7 @@ mod tests { fn test_open_telemetry_span_start_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("MyNamespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("MyNamespace"), "test", Some("0.1.0")); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let child_span = @@ -268,11 +269,11 @@ mod tests { fn test_open_telemetry_span_start_with_current() { 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("Namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Namespace"), "test", Some("0.1.0")); let span1 = tracer.start_span("span1", SpanKind::Internal, vec![]); let span2 = tracer.start_span("span2", SpanKind::Server, vec![]); let _span_guard = span2.set_current(&azure_core::http::Context::new()); - let child_span = tracer.start_span_with_current("child_span", SpanKind::Client, vec![]); + let child_span = tracer.start_span("child_span", SpanKind::Client, vec![]); child_span.end(); span2.end(); @@ -297,7 +298,7 @@ mod tests { fn test_open_telemetry_span_set_attribute() { 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("ThisNamespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("ThisNamespace"), "test", Some("0.1.0")); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.set_attribute("test_key", AttributeValue::String("test_value".to_string())); @@ -320,7 +321,7 @@ mod tests { fn test_open_telemetry_span_record_error() { 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("namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("namespace"), "test", Some("0.1.0")); let span = tracer.start_span("test_span", SpanKind::Client, vec![]); let error = Error::new(ErrorKind::NotFound, "resource not found"); @@ -346,7 +347,7 @@ mod tests { fn test_open_telemetry_span_set_status() { 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("Namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Namespace"), "test", Some("0.1.0")); // Test Unset status let span = tracer.start_span("test_span_unset", SpanKind::Server, vec![]); @@ -375,7 +376,7 @@ mod tests { async fn test_open_telemetry_span_futures() { 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("Namespace"), "test", "0.1.0"); + let tracer = tracer_provider.get_tracer(Some("Namespace"), "test", Some("0.1.0")); let future = async { let context = Context::current(); @@ -393,7 +394,7 @@ mod tests { "test_span", SpanKind::Client, vec![Attribute { - key: "test_key", + key: "test_key".into(), value: "test_value".into(), }], ); diff --git a/sdk/core/azure_core_opentelemetry/src/telemetry.rs b/sdk/core/azure_core_opentelemetry/src/telemetry.rs index c085028fec..55dc10a9f2 100644 --- a/sdk/core/azure_core_opentelemetry/src/telemetry.rs +++ b/sdk/core/azure_core_opentelemetry/src/telemetry.rs @@ -56,10 +56,13 @@ impl TracerProvider for OpenTelemetryTracerProvider { &self, namespace: Option<&'static str>, crate_name: &'static str, - crate_version: &'static str, + crate_version: Option<&'static str>, ) -> Arc { - let scope = InstrumentationScope::builder(crate_name) - .with_version(crate_version) + let mut builder = InstrumentationScope::builder(crate_name); + if let Some(crate_version) = crate_version { + builder = builder.with_version(crate_version); + } + let scope = builder .with_schema_url("https://opentelemetry.io/schemas/1.23.0") .build(); if let Some(provider) = &self.inner { @@ -99,7 +102,7 @@ mod tests { #[test] fn test_create_tracer_provider_from_global() { let tracer_provider = OpenTelemetryTracerProvider::new_from_global_provider(); - let _tracer = tracer_provider.get_tracer(Some("My Namespace"), "test", "0.1.0"); + let _tracer = tracer_provider.get_tracer(Some("My Namespace"), "test", Some("0.1.0")); } #[test] @@ -108,6 +111,6 @@ mod tests { opentelemetry::global::set_tracer_provider(provider); let tracer_provider = OpenTelemetryTracerProvider::new_from_global_provider(); - let _tracer = tracer_provider.get_tracer(Some("My Namespace"), "test", "0.1.0"); + let _tracer = tracer_provider.get_tracer(Some("My Namespace"), "test", Some("0.1.0")); } } diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index d6b14c3b9a..5cd99580af 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -6,7 +6,7 @@ use crate::{ span::{OpenTelemetrySpan, OpenTelemetrySpanKind}, }; -use azure_core::tracing::{Attribute, SpanKind, Tracer}; +use azure_core::tracing::{SpanKind, Tracer}; use opentelemetry::{ global::BoxedTracer, trace::{TraceContextExt, Tracer as OpenTelemetryTracerTrait}, @@ -43,25 +43,6 @@ impl Tracer for OpenTelemetryTracer { } fn start_span( - &self, - name: &'static str, - kind: SpanKind, - attributes: Vec, - ) -> Arc { - 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::new(); - let span = self.inner.build_with_context(span_builder, &context); - - OpenTelemetrySpan::new(context.with_span(span)) - } - - fn start_span_with_current( &self, name: &'static str, kind: SpanKind, @@ -120,7 +101,7 @@ mod tests { fn test_create_tracer() { let noop_tracer = NoopTracerProvider::new(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(noop_tracer)); - let tracer = otel_provider.get_tracer(Some("name"), "test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer(Some("name"), "test_tracer", Some("1.0.0")); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); } @@ -129,14 +110,14 @@ mod tests { fn test_create_tracer_with_sdk_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)); - let _tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); + let _tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", Some("1.0.0")); } #[test] fn test_create_span_from_tracer() { let provider = SdkTracerProvider::builder().build(); let otel_provider = OpenTelemetryTracerProvider::new(Arc::new(provider)); - let tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", "1.0.0"); + let tracer = otel_provider.get_tracer(Some("My.Namespace"), "test_tracer", Some("1.0.0")); let _span = tracer.start_span("test_span", SpanKind::Internal, vec![]); } } diff --git a/sdk/core/azure_core_opentelemetry/tests/otel_span_tests.rs b/sdk/core/azure_core_opentelemetry/tests/otel_span_tests.rs index bd8911fcaa..b0b4dc2000 100644 --- a/sdk/core/azure_core_opentelemetry/tests/otel_span_tests.rs +++ b/sdk/core/azure_core_opentelemetry/tests/otel_span_tests.rs @@ -14,7 +14,7 @@ async fn test_span_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer(Some("test_namespace"), "test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer(Some("test_namespace"), "test_tracer", Some("1.0.0")); // Create a span using the Azure tracer let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); @@ -42,7 +42,7 @@ async fn test_tracer_provider_creation() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); // Get a tracer and verify it works - let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", Some("1.0.0")); let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); span.end(); @@ -56,7 +56,7 @@ async fn test_span_attributes() -> Result<(), Box> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); // Get a tracer from the Azure provider - let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", "1.0.0"); + let tracer = azure_provider.get_tracer(Some("test.namespace"), "test_tracer", Some("1.0.0")); // Create span with multiple attributes let span = tracer.start_span("test_span", SpanKind::Internal, vec![]); diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index f0681ff4d1..68a59ac7ca 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -69,7 +69,7 @@ impl TestServiceClient { tracer_provider.get_tracer( Some("Az.TestServiceClient"), option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"), - option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"), + option_env!("CARGO_PKG_VERSION"), ) }) } else { @@ -303,7 +303,7 @@ async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -315,7 +315,6 @@ async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { ), ("server.address", "example.com".into()), ("server.port", 443.into()), - ("http.request.resend_count", 0.into()), ("http.response.status_code", 200.into()), ], }, @@ -363,7 +362,7 @@ async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result< }, attributes: vec![ ("http.request.method", "GET".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -376,7 +375,6 @@ async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result< ("server.address", "example.com".into()), ("server.port", 443.into()), ("error.type", "404".into()), - ("http.request.resend_count", 0.into()), ("http.response.status_code", 404.into()), ], }, @@ -423,7 +421,7 @@ async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Resu attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -435,7 +433,6 @@ async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Resu ), ("server.address", "example.com".into()), ("server.port", 443.into()), - ("http.request.resend_count", 0.into()), ("http.response.status_code", 200.into()), ], }, @@ -493,7 +490,7 @@ async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) - attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -505,7 +502,6 @@ async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) - ), ("server.address", "example.com".into()), ("server.port", 443.into()), - ("http.request.resend_count", 0.into()), ("http.response.status_code", 404.into()), ("error.type", "404".into()), ], diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs index 4151a96577..b455ef606e 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs @@ -305,7 +305,7 @@ mod tests { parent_span_id: None, attributes: vec![ ("http.request.method", "GET".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -353,7 +353,7 @@ mod tests { }, attributes: vec![ ("http.request.method", "GET".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -401,7 +401,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -478,7 +478,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -559,7 +559,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -586,7 +586,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -613,7 +613,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( @@ -640,7 +640,7 @@ mod tests { attributes: vec![ ("http.request.method", "GET".into()), ("az.namespace", "Az.TestServiceClient".into()), - ("az.client.request.id", "".into()), + ("az.client_request_id", "".into()), ( "url.full", format!( diff --git a/sdk/core/azure_core_test/src/tracing.rs b/sdk/core/azure_core_test/src/tracing.rs index ee75d5693e..f2cfca01e6 100644 --- a/sdk/core/azure_core_test/src/tracing.rs +++ b/sdk/core/azure_core_test/src/tracing.rs @@ -35,7 +35,7 @@ impl TracerProvider for MockTracingProvider { &self, azure_namespace: Option<&'static str>, crate_name: &'static str, - crate_version: &'static str, + crate_version: Option<&'static str>, ) -> Arc { let mut tracers = self.tracers.lock().unwrap(); let tracer = Arc::new(MockTracer { @@ -54,7 +54,7 @@ impl TracerProvider for MockTracingProvider { pub struct MockTracer { pub namespace: Option<&'static str>, pub package_name: &'static str, - pub package_version: &'static str, + pub package_version: Option<&'static str>, pub spans: Mutex>>, } @@ -63,30 +63,31 @@ impl Tracer for MockTracer { self.namespace } - fn start_span_with_current( + fn start_span_with_parent( &self, name: &str, kind: SpanKind, attributes: Vec, + _parent: Arc, ) -> Arc { - let span = Arc::new(MockSpan::new(name, kind, attributes)); + let span = Arc::new(MockSpan::new(name, kind, attributes.clone())); self.spans.lock().unwrap().push(span.clone()); span } - fn start_span_with_parent( + fn start_span( &self, - name: &str, + name: &'static str, kind: SpanKind, attributes: Vec, - _parent: Arc, - ) -> Arc { - let span = Arc::new(MockSpan::new(name, kind, attributes)); - self.spans.lock().unwrap().push(span.clone()); - span - } - - fn start_span(&self, name: &str, kind: SpanKind, attributes: Vec) -> Arc { + ) -> Arc { + let attributes = attributes + .into_iter() + .map(|attr| Attribute { + key: attr.key.clone(), + value: attr.value.clone(), + }) + .collect(); let span = Arc::new(MockSpan::new(name, kind, attributes)); self.spans.lock().unwrap().push(span.clone()); span @@ -105,6 +106,7 @@ impl MockSpan { fn new(name: &str, kind: SpanKind, attributes: Vec) -> Self { println!("Creating MockSpan: {}", name); println!("Attributes: {:?}", attributes); + println!("Converted attributes: {:?}", attributes); Self { name: name.to_string(), kind, @@ -119,7 +121,10 @@ impl Span for MockSpan { fn set_attribute(&self, key: &'static str, value: AttributeValue) { println!("{}: Setting attribute {}: {:?}", self.name, key, value); let mut attributes = self.attributes.lock().unwrap(); - attributes.push(Attribute { key, value }); + attributes.push(Attribute { + key: key.into(), + value, + }); } fn set_status(&self, status: crate::tracing::SpanStatus) { @@ -163,14 +168,16 @@ impl Span for MockSpan { impl AsAny for MockSpan { fn as_any(&self) -> &dyn std::any::Any { - self + // Convert to an object that doesn't expose the lifetime parameter + // We're essentially erasing the lifetime here to satisfy the static requirement + self as &dyn std::any::Any } } #[derive(Debug)] pub struct ExpectedTracerInformation<'a> { pub name: &'a str, - pub version: &'a str, + pub version: Option<&'a str>, pub namespace: Option<&'a str>, pub spans: Vec>, } @@ -193,40 +200,40 @@ pub fn check_instrumentation_result( "Unexpected number of tracers", ); let tracers = mock_tracer.tracers.lock().unwrap(); - for (index, expectation) in expected_tracers.iter().enumerate() { - trace!("Checking tracer {}: {}", index, expectation.name); + for (index, expected) in expected_tracers.iter().enumerate() { + trace!("Checking tracer {}: {}", index, expected.name); let tracer = &tracers[index]; - assert_eq!(tracer.package_name, expectation.name); - assert_eq!(tracer.package_version, expectation.version); - assert_eq!(tracer.namespace, expectation.namespace); + assert_eq!(tracer.package_name, expected.name); + assert_eq!(tracer.package_version, expected.version); + assert_eq!(tracer.namespace, expected.namespace); let spans = tracer.spans.lock().unwrap(); assert_eq!( spans.len(), - expectation.spans.len(), + expected.spans.len(), "Unexpected number of spans for tracer {}", - expectation.name + expected.name ); - for (span_index, span_expectation) in expectation.spans.iter().enumerate() { + for (span_index, span_expected) in expected.spans.iter().enumerate() { println!( "Checking span {} of tracer {}: {}", - span_index, expectation.name, span_expectation.span_name + span_index, expected.name, span_expected.span_name ); - check_span_information(&spans[span_index], span_expectation); + check_span_information(&spans[span_index], span_expected); } } } -fn check_span_information(span: &Arc, expectation: &ExpectedSpanInformation<'_>) { - assert_eq!(span.name, expectation.span_name); - assert_eq!(span.kind, expectation.kind); - assert_eq!(*span.state.lock().unwrap(), expectation.status); +fn check_span_information(span: &Arc, expected: &ExpectedSpanInformation<'_>) { + assert_eq!(span.name, expected.span_name); + assert_eq!(span.kind, expected.kind); + assert_eq!(*span.state.lock().unwrap(), expected.status); let attributes = span.attributes.lock().unwrap(); for (index, attr) in attributes.iter().enumerate() { println!("Attribute {}: {} = {:?}", index, attr.key, attr.value); let mut found = false; - for (key, value) in &expectation.attributes { + for (key, value) in &expected.attributes { if attr.key == *key { assert_eq!(attr.value, *value, "Attribute mismatch for key: {}", key); found = true; @@ -237,7 +244,7 @@ fn check_span_information(span: &Arc, expectation: &ExpectedSpanInform panic!("Unexpected attribute: {} = {:?}", attr.key, attr.value); } } - for (key, value) in &expectation.attributes { + for (key, value) in expected.attributes.iter() { if !attributes .iter() .any(|attr| attr.key == *key && attr.value == *value) diff --git a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs index 0360471710..9b94fb9a18 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/attributes.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/attributes.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use std::borrow::Cow; + /// An array of homogeneous attribute values. #[derive(Debug, PartialEq, Clone)] pub enum AttributeArray { @@ -39,7 +41,7 @@ pub enum AttributeValue { #[derive(Debug, PartialEq, Clone)] pub struct Attribute { /// A key-value pair attribute. - pub key: &'static str, + pub key: Cow<'static, str>, pub value: AttributeValue, } @@ -132,3 +134,58 @@ impl From> for AttributeValue { AttributeValue::Array(AttributeArray::String(value)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_attribute_value_equality() { + let attr1 = AttributeValue::String("test".into()); + let attr2 = AttributeValue::String("test".into()); + let attr3 = AttributeValue::String("different".into()); + + assert_eq!(attr1, attr2); + assert_ne!(attr1, attr3); + } + + #[test] + fn test_attribute_array_equality() { + let array1 = AttributeArray::String(vec!["test".into(), "test2".into()]); + let array2 = AttributeArray::String(vec!["test".into(), "test2".into()]); + let array3 = AttributeArray::String(vec!["different".into()]); + + assert_eq!(array1, array2); + assert_ne!(array1, array3); + } + + #[test] + fn test_attribute_key_from_string() { + let key = "test_key".to_string(); + let key = key + " value"; + let attr = Attribute { + key: key.into(), + value: AttributeValue::String("test_value".into()), + }; + assert_eq!(attr.key, "test_key value"); + } + + #[test] + fn test_attribute_equality() { + let attr1 = Attribute { + key: "test".into(), + value: AttributeValue::String("value".into()), + }; + let attr2 = Attribute { + key: "test".into(), + value: AttributeValue::String("value".into()), + }; + let attr3 = Attribute { + key: "test".into(), + value: AttributeValue::String("different".into()), + }; + + assert_eq!(attr1, attr2); + assert_ne!(attr1, attr3); + } +} diff --git a/sdk/typespec/typespec_client_core/src/tracing/mod.rs b/sdk/typespec/typespec_client_core/src/tracing/mod.rs index 36f9f661bc..3fb9a88078 100644 --- a/sdk/typespec/typespec_client_core/src/tracing/mod.rs +++ b/sdk/typespec/typespec_client_core/src/tracing/mod.rs @@ -37,14 +37,14 @@ pub trait TracerProvider: Send + Sync + Debug { &self, namespace_name: Option<&'static str>, crate_name: &'static str, - crate_version: &'static str, + crate_version: Option<&'static str>, ) -> Arc; } pub trait Tracer: Send + Sync + Debug { /// Starts a new span with the given name and type. /// - /// The newly created span will not have a parent span. + /// The newly created span will have the "current" span as a parent. /// /// # Arguments /// - `name`: The name of the span to start. @@ -61,26 +61,6 @@ pub trait Tracer: Send + Sync + Debug { attributes: Vec, ) -> Arc; - /// Starts a new span with the given name and type. - /// - /// The parent span of the newly created span will be the current span (if one - /// exists). - /// - /// # Arguments - /// - `name`: The name of the span to start. - /// - `kind`: The type of the span to start. - /// - `attributes`: A vector of attributes to associate with the span. - /// - /// # Returns - /// An `Arc` representing the started span. - /// - fn start_span_with_current( - &self, - name: &'static str, - kind: SpanKind, - attributes: Vec, - ) -> Arc; - /// Starts a new child with the given name, type, and parent span. /// /// # Arguments From c8893c5cbfeb7e60e529dd26a753632d7af2ea53 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 21 Jul 2025 15:38:54 -0700 Subject: [PATCH 77/84] PR feedback --- .../request_instrumentation.rs | 4 +- .../tests/telemetry_service_implementation.rs | 131 +++++++++--------- .../src/http/policies/retry/exponential.rs | 3 - 3 files changed, 67 insertions(+), 71 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs index 2934805645..2f52daa695 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs @@ -118,7 +118,7 @@ impl Policy for RequestInstrumentationPolicy { tracer.start_span(method_str, SpanKind::Client, span_attributes) }; - if (span.is_recording()) { + if span.is_recording() { if let Some(client_request_id) = request .headers() .get_optional_str(&headers::CLIENT_REQUEST_ID) @@ -144,7 +144,7 @@ impl Policy for RequestInstrumentationPolicy { let result = next[0].send(ctx, request, &next[1..]).await; - if (span.is_recording()) { + if span.is_recording() { if let Some(err) = result.as_ref().err() { // If the request failed, set an error type attribute. span.set_attribute(ERROR_TYPE_ATTRIBUTE, err.kind().to_string().into()); diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index 68a59ac7ca..e6b58016ef 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -290,36 +290,35 @@ async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { assert_eq!(response.status(), azure_core::http::StatusCode::Ok); let spans = otel_exporter.get_finished_spans().unwrap(); - assert_eq!(spans.len(), 1); - for span in &spans { - trace!("Span: {:?}", span); - - verify_span( - span, - ExpectedSpan { - name: "GET", - kind: OpenTelemetrySpanKind::Client, - status: OpenTelemetrySpanStatus::Unset, - parent_span_id: None, - attributes: vec![ - ("http.request.method", "GET".into()), - ("az.client_request_id", "".into()), - ( - "url.full", - format!( - "{}{}", - client.endpoint(), - "index.html?api-version=2023-10-01" - ) - .into(), - ), - ("server.address", "example.com".into()), - ("server.port", 443.into()), - ("http.response.status_code", 200.into()), - ], - }, - )?; + for (i, span) in spans.iter().enumerate() { + trace!("Span {i}: {span:?}"); } + assert_eq!(spans.len(), 1); + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + status: OpenTelemetrySpanStatus::Unset, + parent_span_id: None, + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.client_request_id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "index.html?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("http.response.status_code", 200.into()), + ], + }, + )?; Ok(()) } @@ -347,39 +346,39 @@ async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result< info!("Response: {:?}", response); let spans = otel_exporter.get_finished_spans().unwrap(); + for (i, span) in spans.iter().enumerate() { + trace!("Span {i}: {span:?}"); + } assert_eq!(spans.len(), 1); - for span in &spans { - trace!("Span: {:?}", span); - - verify_span( - span, - ExpectedSpan { - name: "GET", - kind: OpenTelemetrySpanKind::Client, - parent_span_id: None, - status: OpenTelemetrySpanStatus::Error { - description: "".into(), - }, - attributes: vec![ - ("http.request.method", "GET".into()), - ("az.client_request_id", "".into()), - ( - "url.full", - format!( - "{}{}", - client.endpoint(), - "failing_url?api-version=2023-10-01" - ) - .into(), - ), - ("server.address", "example.com".into()), - ("server.port", 443.into()), - ("error.type", "404".into()), - ("http.response.status_code", 404.into()), - ], + + verify_span( + &spans[0], + ExpectedSpan { + name: "GET", + kind: OpenTelemetrySpanKind::Client, + parent_span_id: None, + status: OpenTelemetrySpanStatus::Error { + description: "".into(), }, - )?; - } + attributes: vec![ + ("http.request.method", "GET".into()), + ("az.client_request_id", "".into()), + ( + "url.full", + format!( + "{}{}", + client.endpoint(), + "failing_url?api-version=2023-10-01" + ) + .into(), + ), + ("server.address", "example.com".into()), + ("server.port", 443.into()), + ("error.type", "404".into()), + ("http.response.status_code", 404.into()), + ], + }, + )?; Ok(()) } @@ -407,10 +406,10 @@ async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Resu info!("Response: {:?}", response); let spans = otel_exporter.get_finished_spans().unwrap(); - assert_eq!(spans.len(), 2); - for span in &spans { - trace!("Span: {:?}", span); + for (i, span) in spans.iter().enumerate() { + trace!("Span {i}: {span:?}"); } + assert_eq!(spans.len(), 2); verify_span( &spans[0], ExpectedSpan { @@ -474,10 +473,10 @@ async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) - info!("Response: {:?}", response); let spans = otel_exporter.get_finished_spans().unwrap(); - assert_eq!(spans.len(), 2); - for span in &spans { - trace!("Span: {:?}", span); + for (i, span) in spans.iter().enumerate() { + trace!("Span {i}: {span:?}"); } + assert_eq!(spans.len(), 2); verify_span( &spans[0], ExpectedSpan { diff --git a/sdk/typespec/typespec_client_core/src/http/policies/retry/exponential.rs b/sdk/typespec/typespec_client_core/src/http/policies/retry/exponential.rs index 3e7bfd9935..5845286e86 100644 --- a/sdk/typespec/typespec_client_core/src/http/policies/retry/exponential.rs +++ b/sdk/typespec/typespec_client_core/src/http/policies/retry/exponential.rs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use tracing::trace; - use super::RetryPolicy; use crate::time::Duration; @@ -28,7 +26,6 @@ impl ExponentialRetryPolicy { max_elapsed: Duration, max_delay: Duration, ) -> Self { - trace!("ExponentialRetryPolicy::new called with initial_delay: {initial_delay:?}, max_retries: {max_retries}, max_elapsed: {max_elapsed:?}, max_delay: {max_delay:?}"); Self { initial_delay: initial_delay.max(Duration::milliseconds(1)), max_retries, From aa0f99a69d55fd010f223734b76449a48272b310 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 21 Jul 2025 15:40:44 -0700 Subject: [PATCH 78/84] Removed unnecessary modification --- .../typespec_client_core/src/http/policies/transport.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/typespec/typespec_client_core/src/http/policies/transport.rs b/sdk/typespec/typespec_client_core/src/http/policies/transport.rs index 77ec87263b..aa45b92539 100644 --- a/sdk/typespec/typespec_client_core/src/http/policies/transport.rs +++ b/sdk/typespec/typespec_client_core/src/http/policies/transport.rs @@ -36,7 +36,10 @@ impl Policy for TransportPolicy { assert_eq!(0, next.len()); if request.body().is_empty() - && matches!(request.method(), Method::Patch | Method::Post | Method::Put) + && matches!( + *request.method(), + Method::Patch | Method::Post | Method::Put + ) { request.add_mandatory_header(EMPTY_CONTENT_LENGTH); } From bd42d8eb96a9ec95ca690ee02fa4159652356353 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Mon, 21 Jul 2025 16:41:38 -0700 Subject: [PATCH 79/84] Use azuresdkforcpp web server instead of actual live servers to improve test reliability --- ...ibuted-tracing-for-rust-service-clients.md | 5 ++ sdk/core/azure_core_opentelemetry/assets.json | 2 +- .../tests/telemetry_service_implementation.rs | 46 +++++------- .../tests/telemetry_service_macros.rs | 74 +++++++++---------- 4 files changed, 60 insertions(+), 67 deletions(-) diff --git a/doc/distributed-tracing-for-rust-service-clients.md b/doc/distributed-tracing-for-rust-service-clients.md index fc7cffa6d1..80093faa4d 100644 --- a/doc/distributed-tracing-for-rust-service-clients.md +++ b/doc/distributed-tracing-for-rust-service-clients.md @@ -22,6 +22,11 @@ A "tracer" is a factory for "Spans". A `Tracer` is configured with three paramet * `namespace` - the "namespace" for the service client. The namespace for all azure services are listed [on this page](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers). * `package name` - this is typically the Cargo package name for the service client (`env!("CARGO_PKG_NAME")`) * `package version` - this is typically the version of the Cargo package for the service client (`env!("CARGO_PKG_VERSION")`) +* `Schema Url` - this is typically the OpenTelemetry schema version - if not provided, a default schema version is used. + +#### Note + +Custom Schema Url support is not currently implemented. Tracers have two mechanisms for creating spans: diff --git a/sdk/core/azure_core_opentelemetry/assets.json b/sdk/core/azure_core_opentelemetry/assets.json index 8f0a2844a2..5f90bbd318 100644 --- a/sdk/core/azure_core_opentelemetry/assets.json +++ b/sdk/core/azure_core_opentelemetry/assets.json @@ -1,6 +1,6 @@ { "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "rust", - "Tag": "", + "Tag": "rust/azure_core_opentelemetry_58be03e82f", "TagPrefix": "rust/azure_core_opentelemetry" } \ No newline at end of file diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index e6b58016ef..aefa574b94 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -1,6 +1,8 @@ // Copyright (C) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// cspell: ignore azuresdkforcpp + //! This file contains an Azure SDK for Rust fake service client API. //! use azure_core::{ @@ -235,14 +237,14 @@ fn verify_span(span: &opentelemetry_sdk::trace::SpanData, expected: ExpectedSpan #[recorded::test()] async fn test_service_client_new(ctx: TestContext) -> Result<()> { let recording = ctx.recording(); - let endpoint = "https://example.com"; + let endpoint = "https://www.microsoft.com"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { ..Default::default() }; let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); - assert_eq!(client.endpoint().as_str(), "https://example.com/"); + assert_eq!(client.endpoint().as_str(), "https://www.microsoft.com/"); assert_eq!(client.api_version, "2023-10-01"); Ok(()) @@ -252,11 +254,11 @@ async fn test_service_client_new(ctx: TestContext) -> Result<()> { #[recorded::test()] async fn test_service_client_get(ctx: TestContext) -> Result<()> { let recording = ctx.recording(); - let endpoint = "https://example.com"; + let endpoint = "https://azuresdkforcpp.azurewebsites.net"; let credential = recording.credential().clone(); let client = TestServiceClient::new(endpoint, credential, None).unwrap(); - let response = client.get("index.html", None).await; + let response = client.get("get", None).await; info!("Response: {:?}", response); assert!(response.is_ok()); let response = response.unwrap(); @@ -270,7 +272,7 @@ async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); - let endpoint = "https://example.com"; + let endpoint = "https://azuresdkforcpp.azurewebsites.net"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { azure_client_options: ClientOptions { @@ -283,7 +285,7 @@ async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { }; let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); - let response = client.get("index.html", None).await; + let response = client.get("get", None).await; info!("Response: {:?}", response); assert!(response.is_ok()); let response = response.unwrap(); @@ -306,14 +308,9 @@ async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { ("az.client_request_id", "".into()), ( "url.full", - format!( - "{}{}", - client.endpoint(), - "index.html?api-version=2023-10-01" - ) - .into(), + format!("{}{}", client.endpoint(), "get?api-version=2023-10-01").into(), ), - ("server.address", "example.com".into()), + ("server.address", "azuresdkforcpp.azurewebsites.net".into()), ("server.port", 443.into()), ("http.response.status_code", 200.into()), ], @@ -324,12 +321,12 @@ async fn test_service_client_get_with_tracing(ctx: TestContext) -> Result<()> { } #[recorded::test()] -async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result<()> { +async fn test_service_client_get_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); - let endpoint = "https://example.com"; + let endpoint = "https://azuresdkforcpp.azurewebsites.net"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { azure_client_options: ClientOptions { @@ -372,7 +369,7 @@ async fn test_service_client_get_with_tracing_error(ctx: TestContext) -> Result< ) .into(), ), - ("server.address", "example.com".into()), + ("server.address", "azuresdkforcpp.azurewebsites.net".into()), ("server.port", 443.into()), ("error.type", "404".into()), ("http.response.status_code", 404.into()), @@ -389,7 +386,7 @@ async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Resu let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); - let endpoint = "https://example.com"; + let endpoint = "https://azuresdkforcpp.azurewebsites.net"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { azure_client_options: ClientOptions { @@ -402,7 +399,7 @@ async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Resu }; let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap(); - let response = client.get_with_function_tracing("index.html", None).await; + let response = client.get_with_function_tracing("get", None).await; info!("Response: {:?}", response); let spans = otel_exporter.get_finished_spans().unwrap(); @@ -423,14 +420,9 @@ async fn test_service_client_get_with_function_tracing(ctx: TestContext) -> Resu ("az.client_request_id", "".into()), ( "url.full", - format!( - "{}{}", - client.endpoint(), - "index.html?api-version=2023-10-01" - ) - .into(), + format!("{}{}", client.endpoint(), "get?api-version=2023-10-01").into(), ), - ("server.address", "example.com".into()), + ("server.address", "azuresdkforcpp.azurewebsites.net".into()), ("server.port", 443.into()), ("http.response.status_code", 200.into()), ], @@ -456,7 +448,7 @@ async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) - let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); - let endpoint = "https://example.com"; + let endpoint = "https://azuresdkforcpp.azurewebsites.net"; let credential = recording.credential().clone(); let options = TestServiceClientOptions { azure_client_options: ClientOptions { @@ -499,7 +491,7 @@ async fn test_service_client_get_with_function_tracing_error(ctx: TestContext) - ) .into(), ), - ("server.address", "example.com".into()), + ("server.address", "azuresdkforcpp.azurewebsites.net".into()), ("server.port", 443.into()), ("http.response.status_code", 404.into()), ("error.type", "404".into()), diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs index b455ef606e..d333438ba2 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs @@ -1,6 +1,7 @@ // Copyright (C) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// cspell: ignore azuresdkforcpp invalidtopleveldomain //! This file contains an Azure SDK for Rust fake service client API. //! use azure_core::{ @@ -196,7 +197,7 @@ mod tests { azure_provider: Arc, ) -> TestServiceClientWithMacros { let recording = ctx.recording(); - let endpoint = "https://example.com"; + let endpoint = "https://azuresdkforcpp.azurewebsites.net"; let credential = recording.credential().clone(); let options = TestServiceClientWithMacrosOptions { client_options: ClientOptions { @@ -265,14 +266,14 @@ mod tests { #[recorded::test()] async fn test_macro_service_client_new(ctx: TestContext) -> Result<()> { let recording = ctx.recording(); - let endpoint = "https://example.com"; + let endpoint = "https://microsoft.com"; let credential = recording.credential().clone(); let options = TestServiceClientWithMacrosOptions { ..Default::default() }; let client = TestServiceClientWithMacros::new(endpoint, credential, Some(options)).unwrap(); - assert_eq!(client.endpoint().as_str(), "https://example.com/"); + assert_eq!(client.endpoint().as_str(), "https://microsoft.com/"); assert_eq!(client.api_version, "2023-10-01"); Ok(()) @@ -285,7 +286,7 @@ mod tests { let client = create_service_client(ctx, azure_provider.clone()); - let response = client.get("index.html", None).await; + let response = client.get("get", None).await; info!("Response: {:?}", response); assert!(response.is_ok()); let response = response.unwrap(); @@ -308,16 +309,10 @@ mod tests { ("az.client_request_id", "".into()), ( "url.full", - format!( - "{}{}", - client.endpoint(), - "index.html?api-version=2023-10-01" - ) - .into(), + format!("{}{}", client.endpoint(), "get?api-version=2023-10-01").into(), ), - ("server.address", "example.com".into()), + ("server.address", "azuresdkforcpp.azurewebsites.net".into()), ("server.port", 443.into()), - ("http.request.resend_count", 0.into()), ("http.response.status_code", 200.into()), ], }, @@ -363,10 +358,9 @@ mod tests { ) .into(), ), - ("server.address", "example.com".into()), + ("server.address", "azuresdkforcpp.azurewebsites.net".into()), ("server.port", 443.into()), ("error.type", "404".into()), - ("http.request.resend_count", 0.into()), ("http.response.status_code", 404.into()), ], }, @@ -383,7 +377,7 @@ mod tests { let client = create_service_client(ctx, azure_provider.clone()); - let response = client.get_with_function_tracing("index.html", None).await; + let response = client.get_with_function_tracing("get", None).await; info!("Response: {:?}", response); let spans = otel_exporter.get_finished_spans().unwrap(); @@ -404,16 +398,10 @@ mod tests { ("az.client_request_id", "".into()), ( "url.full", - format!( - "{}{}", - client.endpoint(), - "index.html?api-version=2023-10-01" - ) - .into(), + format!("{}{}", client.endpoint(), "get?api-version=2023-10-01").into(), ), - ("server.address", "example.com".into()), + ("server.address", "azuresdkforcpp.azurewebsites.net".into()), ("server.port", 443.into()), - ("http.request.resend_count", 0.into()), ("http.response.status_code", 200.into()), ], }, @@ -427,9 +415,9 @@ mod tests { status: OpenTelemetrySpanStatus::Unset, attributes: vec![ ("az.namespace", "Az.TestServiceClient".into()), - ("a.b", 1.into()), // added by tracing macro. - ("az.telemetry", "Abc".into()), // added by tracing macro - ("string attribute", "index.html".into()), // added by tracing macro. + ("a.b", 1.into()), // added by tracing macro. + ("az.telemetry", "Abc".into()), // added by tracing macro + ("string attribute", "get".into()), // added by tracing macro. ], }, )?; @@ -438,14 +426,12 @@ mod tests { } #[recorded::test()] - async fn test_macro_service_client_get_with_function_tracing_error( - ctx: TestContext, - ) -> Result<()> { + async fn test_macro_service_client_get_function_tracing_error(ctx: TestContext) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); - let endpoint = "https://example.com"; + let endpoint = "https://azuresdkforcpp.azurewebsites.net"; let credential = recording.credential().clone(); let options = TestServiceClientWithMacrosOptions { client_options: ClientOptions { @@ -488,9 +474,8 @@ mod tests { ) .into(), ), - ("server.address", "example.com".into()), + ("server.address", "azuresdkforcpp.azurewebsites.net".into()), ("server.port", 443.into()), - ("http.request.resend_count", 0.into()), ("http.response.status_code", 404.into()), ("error.type", "404".into()), ], @@ -517,14 +502,14 @@ mod tests { } #[recorded::test()] - async fn test_macro_service_client_get_with_function_tracing_dns_error( + async fn test_macro_service_client_get_function_tracing_dns_error( ctx: TestContext, ) -> Result<()> { let (sdk_provider, otel_exporter) = create_exportable_tracer_provider(); let azure_provider = OpenTelemetryTracerProvider::new(sdk_provider); let recording = ctx.recording(); - let endpoint = "https://example.invalid_top_level_domain"; + let endpoint = "https://azuresdkforcpp.azurewebsites.invalidtopleveldomain"; let credential = recording.credential().clone(); let options = TestServiceClientWithMacrosOptions { client_options: ClientOptions { @@ -569,9 +554,11 @@ mod tests { ) .into(), ), - ("server.address", "example.invalid_top_level_domain".into()), + ( + "server.address", + "azuresdkforcpp.azurewebsites.invalidtopleveldomain".into(), + ), ("server.port", 443.into()), - ("http.request.resend_count", 0.into()), ("error.type", "Io".into()), ], }, @@ -596,7 +583,10 @@ mod tests { ) .into(), ), - ("server.address", "example.invalid_top_level_domain".into()), + ( + "server.address", + "azuresdkforcpp.azurewebsites.invalidtopleveldomain".into(), + ), ("server.port", 443.into()), ("http.request.resend_count", 1.into()), ("error.type", "Io".into()), @@ -623,7 +613,10 @@ mod tests { ) .into(), ), - ("server.address", "example.invalid_top_level_domain".into()), + ( + "server.address", + "azuresdkforcpp.azurewebsites.invalidtopleveldomain".into(), + ), ("server.port", 443.into()), ("http.request.resend_count", 2.into()), ("error.type", "Io".into()), @@ -650,7 +643,10 @@ mod tests { ) .into(), ), - ("server.address", "example.invalid_top_level_domain".into()), + ( + "server.address", + "azuresdkforcpp.azurewebsites.invalidtopleveldomain".into(), + ), ("server.port", 443.into()), ("http.request.resend_count", 3.into()), ("error.type", "Io".into()), From 446e9f1c1530ebffe529b60c2aee6b8f6ccf6973 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 22 Jul 2025 11:48:30 -0700 Subject: [PATCH 80/84] cspell fixes --- .../tests/telemetry_service_implementation.rs | 2 +- .../azure_core_opentelemetry/tests/telemetry_service_macros.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs index aefa574b94..1cc0be4ef9 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_implementation.rs @@ -1,7 +1,7 @@ // Copyright (C) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// cspell: ignore azuresdkforcpp +// cspell: ignore azuresdkforcpp azurewebsites //! This file contains an Azure SDK for Rust fake service client API. //! diff --git a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs index d333438ba2..1561fd5e8c 100644 --- a/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs +++ b/sdk/core/azure_core_opentelemetry/tests/telemetry_service_macros.rs @@ -1,7 +1,7 @@ // Copyright (C) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// cspell: ignore azuresdkforcpp invalidtopleveldomain +// cspell: ignore azuresdkforcpp invalidtopleveldomain azurewebsites //! This file contains an Azure SDK for Rust fake service client API. //! use azure_core::{ From fc570236019203ba0a7e50af2de5fbb9bdef34fd Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 22 Jul 2025 11:51:24 -0700 Subject: [PATCH 81/84] Update sdk/core/azure_core_opentelemetry/src/span.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sdk/core/azure_core_opentelemetry/src/span.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 331e0b3063..4edc18183b 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -103,7 +103,7 @@ impl Span for OpenTelemetrySpan { ); } else { // If the key is invalid, we skip it - tracing::warn!("Invalid header key: {:?}", key); + tracing::warn!("Encountered an invalid header key (key is None). Skipping this header."); } } } From 7cbcf7bc33fc7caa97faec732cfd74b05828f131 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 22 Jul 2025 11:51:34 -0700 Subject: [PATCH 82/84] Update sdk/core/azure_core_opentelemetry/src/tracer.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sdk/core/azure_core_opentelemetry/src/tracer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index 5cd99580af..b04bb08b2a 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -80,7 +80,10 @@ impl Tracer for OpenTelemetryTracer { let context = parent .as_any() .downcast_ref::() - .expect("Could not downcast parent span to OpenTelemetrySpan") + .expect(&format!( + "Could not downcast parent span to OpenTelemetrySpan. Actual type: {}", + std::any::type_name::() + )) .context() .clone(); let span = self.inner.build_with_context(span_builder, &context); From 3150ae2b30271e1f209b06fa71b8c2b18a13222f Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 22 Jul 2025 12:07:05 -0700 Subject: [PATCH 83/84] clippy and format fixes --- sdk/core/azure_core_opentelemetry/src/span.rs | 4 ++-- sdk/core/azure_core_opentelemetry/src/tracer.rs | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/sdk/core/azure_core_opentelemetry/src/span.rs b/sdk/core/azure_core_opentelemetry/src/span.rs index 4edc18183b..a9ee351527 100644 --- a/sdk/core/azure_core_opentelemetry/src/span.rs +++ b/sdk/core/azure_core_opentelemetry/src/span.rs @@ -102,8 +102,8 @@ impl Span for OpenTelemetrySpan { HeaderValue::from(value.to_str().unwrap().to_owned()), ); } else { - // If the key is invalid, we skip it - tracing::warn!("Encountered an invalid header key (key is None). Skipping this header."); + // If the key is a duplicate of the previous header, we ignore it + tracing::warn!("Duplicate header key detected. Skipping this header."); } } } diff --git a/sdk/core/azure_core_opentelemetry/src/tracer.rs b/sdk/core/azure_core_opentelemetry/src/tracer.rs index b04bb08b2a..8b8ba1869d 100644 --- a/sdk/core/azure_core_opentelemetry/src/tracer.rs +++ b/sdk/core/azure_core_opentelemetry/src/tracer.rs @@ -80,10 +80,12 @@ impl Tracer for OpenTelemetryTracer { let context = parent .as_any() .downcast_ref::() - .expect(&format!( - "Could not downcast parent span to OpenTelemetrySpan. Actual type: {}", - std::any::type_name::() - )) + .unwrap_or_else(|| { + panic!( + "Could not downcast parent span to OpenTelemetrySpan. Actual type: {}", + std::any::type_name::() + ) + }) .context() .clone(); let span = self.inner.build_with_context(span_builder, &context); From 092153691c2826f47dbaa6495aaca778223b5485 Mon Sep 17 00:00:00 2001 From: Larry Osterman Date: Tue, 22 Jul 2025 13:51:26 -0700 Subject: [PATCH 84/84] Merge with pervious PR --- .../instrumentation/public_api_instrumentation.rs | 12 ++++++------ .../instrumentation/request_instrumentation.rs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs index 0da5824405..8a36d28a79 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/public_api_instrumentation.rs @@ -451,7 +451,7 @@ mod tests { |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Get); + assert_eq!(req.method(), Method::Get); Ok(RawResponse::from_bytes( StatusCode::Ok, Headers::new(), @@ -489,7 +489,7 @@ mod tests { |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Get); + assert_eq!(req.method(), Method::Get); Ok(RawResponse::from_bytes( StatusCode::Ok, Headers::new(), @@ -520,7 +520,7 @@ mod tests { |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Get); + assert_eq!(req.method(), Method::Get); Ok(RawResponse::from_bytes( StatusCode::Ok, Headers::new(), @@ -563,7 +563,7 @@ mod tests { |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Get); + assert_eq!(req.method(), Method::Get); Ok(RawResponse::from_bytes( StatusCode::Ok, Headers::new(), @@ -606,7 +606,7 @@ mod tests { |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Get); + assert_eq!(req.method(), Method::Get); Ok(RawResponse::from_bytes( StatusCode::InternalServerError, Headers::new(), @@ -652,7 +652,7 @@ mod tests { |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Put); + assert_eq!(req.method(), Method::Put); Ok(RawResponse::from_bytes( StatusCode::Ok, Headers::new(), diff --git a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs index 2f52daa695..034b9a18d7 100644 --- a/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs +++ b/sdk/core/azure_core/src/http/policies/instrumentation/request_instrumentation.rs @@ -236,7 +236,7 @@ pub(crate) mod tests { |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Get); + assert_eq!(req.method(), Method::Get); Ok(RawResponse::from_bytes( StatusCode::Ok, Headers::new(), @@ -314,7 +314,7 @@ pub(crate) mod tests { |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("example.com")); - assert_eq!(req.method(), &Method::Get); + assert_eq!(req.method(), Method::Get); assert_eq!( req.headers() .get_optional_str(&HeaderName::from_static("traceparent")), @@ -374,7 +374,7 @@ pub(crate) mod tests { run_instrumentation_test(None, None, None, &mut request, |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("host")); - assert_eq!(req.method(), &Method::Get); + assert_eq!(req.method(), Method::Get); Ok(RawResponse::from_bytes( StatusCode::Ok, Headers::new(), @@ -422,7 +422,7 @@ pub(crate) mod tests { |req| { Box::pin(async move { assert_eq!(req.url().host_str(), Some("microsoft.com")); - assert_eq!(req.method(), &Method::Put); + assert_eq!(req.method(), Method::Put); Ok(RawResponse::from_bytes( StatusCode::NotFound, Headers::new(),