Skip to content

Commit 85de5a9

Browse files
committed
Propagate OpenTelemetry trace headers into request headers
1 parent 6d9c143 commit 85de5a9

File tree

4 files changed

+83
-4
lines changed

4 files changed

+83
-4
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/core/azure_core/src/http/policies/request_instrumentation.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ impl Policy for RequestInstrumentationPolicy {
188188
span.set_attribute(HTTP_REQUEST_RESEND_COUNT_ATTRIBUTE, retry_count.0.into());
189189
}
190190

191+
// Propagate the headers for distributed tracing into the request.
192+
span.propagate_headers(request);
193+
191194
let result = next[0].send(ctx, request, &next[1..]).await;
192195

193196
if let Some(err) = result.as_ref().err() {
@@ -242,6 +245,7 @@ mod tests {
242245
use azure_core_test::http::MockHttpClient;
243246
use futures::future::BoxFuture;
244247
use std::sync::{Arc, Mutex};
248+
use typespec_client_core::http::headers::HeaderName;
245249

246250
#[derive(Debug)]
247251
struct MockTracingProvider {
@@ -386,7 +390,15 @@ mod tests {
386390
todo!()
387391
}
388392

389-
fn propagate_headers(&self, _request: &mut Request) {}
393+
/// Insert two dummy headers for distributed tracing.
394+
// cspell: ignore traceparent tracestate
395+
fn propagate_headers(&self, request: &mut Request) {
396+
request.insert_header(
397+
HeaderName::from_static("traceparent"),
398+
"00-<trace_id>-<span_id>-01",
399+
);
400+
request.insert_header(HeaderName::from_static("tracestate"), "<key>=<value>");
401+
}
390402
}
391403

392404
impl AsAny for MockSpan {
@@ -546,6 +558,11 @@ mod tests {
546558
Box::pin(async move {
547559
assert_eq!(req.url().host_str(), Some("example.com"));
548560
assert_eq!(req.method(), &Method::Get);
561+
assert_eq!(
562+
req.headers()
563+
.get_optional_str(HeaderName::from_static("traceparent")),
564+
Some("00-<trace_id>-<span_id>-01")
565+
);
549566
Ok(RawResponse::from_bytes(
550567
StatusCode::Ok,
551568
Headers::new(),

sdk/core/azure_core_opentelemetry/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ edition.workspace = true
1818
azure_core.workspace = true
1919
opentelemetry = { version = "0.30", features = ["trace"] }
2020
opentelemetry-http = "0.30.0"
21+
opentelemetry_sdk = "0.30"
22+
reqwest.workspace = true
2123
tracing.workspace = true
2224
typespec_client_core.workspace = true
2325

26+
2427
[dev-dependencies]
2528
azure_core_test = { workspace = true, features = ["tracing"] }
2629
azure_core_test_macros.workspace = true

sdk/core/azure_core_opentelemetry/src/span.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ use azure_core::{
88
tracing::{AsAny, AttributeValue, Span, SpanGuard, SpanStatus},
99
Result,
1010
};
11-
use opentelemetry::trace::TraceContextExt;
11+
use opentelemetry::{propagation::TextMapPropagator, trace::TraceContextExt};
12+
use opentelemetry_http::HeaderInjector;
13+
use opentelemetry_sdk::propagation::TraceContextPropagator;
14+
use reqwest::header::HeaderMap;
1215
use std::{error::Error as StdError, sync::Arc};
1316

1417
/// newtype for Azure Core SpanKind to enable conversion to OpenTelemetry SpanKind
@@ -76,7 +79,34 @@ impl Span for OpenTelemetrySpan {
7679
self.context.span().set_status(otel_status);
7780
}
7881

79-
fn propagate_headers(&self, _request: &mut azure_core::http::Request) {}
82+
fn propagate_headers(&self, request: &mut azure_core::http::Request) {
83+
// A TraceContextPropagator is used to inject trace context information into HTTP headers.
84+
let trace_propagator = TraceContextPropagator::new();
85+
// We need to map between a reqwest header map (which is what the OpenTelemetry SDK requires)
86+
// and the Azure Core request headers.
87+
//
88+
// We start with an empty header map and inject the OpenTelemetry headers into it.
89+
let mut header_map = HeaderMap::new();
90+
trace_propagator.inject_context(&self.context, &mut HeaderInjector(&mut header_map));
91+
92+
// We then insert each of the headers from the OpenTelemetry header map into the
93+
// Request's header map.
94+
for (key, value) in header_map.into_iter() {
95+
// Note: The OpenTelemetry HeaderInjector will always produce unique header names, so we don't need to
96+
// handle the multiple headers case here.
97+
if let Some(key) = key {
98+
// Convert HeaderName to &str for insertion.
99+
let value_str = value.to_str().unwrap().to_string();
100+
request.insert_header(
101+
azure_core::http::headers::HeaderName::from(key.to_string()),
102+
azure_core::http::headers::HeaderValue::from(value_str),
103+
);
104+
} else {
105+
// If the key is invalid, we skip it
106+
tracing::warn!("Invalid header key: {:?}", key);
107+
}
108+
}
109+
}
80110

81111
fn set_current(
82112
&self,
@@ -117,7 +147,7 @@ impl Drop for OpenTelemetrySpanGuard {
117147
#[cfg(test)]
118148
mod tests {
119149
use crate::telemetry::OpenTelemetryTracerProvider;
120-
use azure_core::http::Context as AzureContext;
150+
use azure_core::http::{Context as AzureContext, Url};
121151
use azure_core::tracing::{Attribute, AttributeValue, SpanKind, SpanStatus, TracerProvider};
122152
use opentelemetry::trace::TraceContextExt;
123153
use opentelemetry::{Context, Key, KeyValue, Value};
@@ -158,6 +188,34 @@ mod tests {
158188
}
159189
}
160190

191+
// cspell: ignore traceparent tracestate
192+
#[test]
193+
fn test_open_telemetry_span_propagate() {
194+
let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider();
195+
196+
let tracer_provider = OpenTelemetryTracerProvider::new(otel_tracer_provider);
197+
assert!(tracer_provider.is_ok());
198+
let tracer =
199+
tracer_provider
200+
.unwrap()
201+
.get_tracer(Some("Microsoft.SpecialCase"), "test", "0.1.0");
202+
let span = tracer.start_span("test_span", SpanKind::Client, vec![]);
203+
let mut request = azure_core::http::Request::new(
204+
Url::parse("http://example.com").unwrap(),
205+
azure_core::http::Method::Get,
206+
);
207+
span.propagate_headers(&mut request);
208+
trace!("Request headers after propagation: {:?}", request.headers());
209+
let traceparent = azure_core::http::headers::HeaderName::from("traceparent");
210+
let tracestate = azure_core::http::headers::HeaderName::from("tracestate");
211+
request.headers().get_as::<String, _>(&traceparent).unwrap();
212+
request.headers().get_as::<String, _>(&tracestate).unwrap();
213+
span.end();
214+
215+
let finished_spans = otel_exporter.get_finished_spans().unwrap();
216+
assert_eq!(finished_spans.len(), 1);
217+
}
218+
161219
#[test]
162220
fn test_open_telemetry_span_hierarchy() {
163221
let (otel_tracer_provider, otel_exporter) = create_exportable_tracer_provider();

0 commit comments

Comments
 (0)