use crate::telemetry::logging::targets;
use super::spans::{attributes, kind::HiveSpanKind};
use crate::config::telemetry::tracing::SpansSemanticConventionsMode;
use opentelemetry::KeyValue;
use opentelemetry_sdk::error::OTelSdkResult;
use opentelemetry_sdk::trace::{SpanData, SpanExporter};
use std::collections::HashSet;
use std::fmt::Debug;
use std::str::FromStr;
const SERVER_SPEC_TO_DEPRECATED: &[(&str, &str)] = &[
(
attributes::HTTP_REQUEST_METHOD,
attributes::DEPRECATED_HTTP_METHOD,
),
(attributes::URL_FULL, attributes::DEPRECATED_HTTP_URL),
(attributes::SERVER_ADDRESS, attributes::DEPRECATED_HTTP_HOST),
(attributes::URL_SCHEME, attributes::DEPRECATED_HTTP_SCHEME),
(
attributes::NETWORK_PROTOCOL_VERSION,
attributes::DEPRECATED_HTTP_FLAVOR,
),
(
attributes::HTTP_REQUEST_BODY_SIZE,
attributes::DEPRECATED_HTTP_REQUEST_CONTENT_LENGTH,
),
(
attributes::USER_AGENT_ORIGINAL,
attributes::DEPRECATED_HTTP_USER_AGENT,
),
(attributes::URL_PATH, attributes::DEPRECATED_HTTP_TARGET),
(
attributes::HTTP_RESPONSE_STATUS_CODE,
attributes::DEPRECATED_HTTP_STATUS_CODE,
),
(
attributes::HTTP_RESPONSE_BODY_SIZE,
attributes::DEPRECATED_HTTP_RESPONSE_CONTENT_LENGTH,
),
];
const CLIENT_SPEC_TO_DEPRECATED: &[(&str, &str)] = &[
(
attributes::HTTP_REQUEST_METHOD,
attributes::DEPRECATED_HTTP_METHOD,
),
(attributes::URL_FULL, attributes::DEPRECATED_HTTP_URL),
(
attributes::SERVER_ADDRESS,
attributes::DEPRECATED_NET_PEER_NAME,
),
(
attributes::SERVER_PORT,
attributes::DEPRECATED_NET_PEER_PORT,
),
(
attributes::NETWORK_PROTOCOL_VERSION,
attributes::DEPRECATED_HTTP_FLAVOR,
),
(
attributes::HTTP_REQUEST_BODY_SIZE,
attributes::DEPRECATED_HTTP_REQUEST_CONTENT_LENGTH,
),
(
attributes::HTTP_RESPONSE_STATUS_CODE,
attributes::DEPRECATED_HTTP_STATUS_CODE,
),
(
attributes::HTTP_RESPONSE_BODY_SIZE,
attributes::DEPRECATED_HTTP_RESPONSE_CONTENT_LENGTH,
),
];
fn get_server_deprecated_key(spec_key: &str) -> Option<&'static str> {
SERVER_SPEC_TO_DEPRECATED
.iter()
.find(|(k, _)| *k == spec_key)
.map(|(_, v)| *v)
}
fn get_client_deprecated_key(spec_key: &str) -> Option<&'static str> {
CLIENT_SPEC_TO_DEPRECATED
.iter()
.find(|(k, _)| *k == spec_key)
.map(|(_, v)| *v)
}
#[derive(Debug)]
pub struct HttpCompatibilityExporter<E: SpanExporter> {
inner: E,
mode: SpansSemanticConventionsMode,
server_deprecated_keys: HashSet<&'static str>,
client_deprecated_keys: HashSet<&'static str>,
}
impl<E: SpanExporter> HttpCompatibilityExporter<E> {
pub fn new(inner: E, mode: &SpansSemanticConventionsMode) -> Self {
let mut server_deprecated = HashSet::with_capacity(SERVER_SPEC_TO_DEPRECATED.len());
for (_spec_key, deprecated_key) in SERVER_SPEC_TO_DEPRECATED {
server_deprecated.insert(*deprecated_key);
}
let mut client_deprecated = HashSet::with_capacity(CLIENT_SPEC_TO_DEPRECATED.len());
for (_spec_key, deprecated_key) in CLIENT_SPEC_TO_DEPRECATED {
client_deprecated.insert(*deprecated_key);
}
Self {
inner,
mode: *mode,
server_deprecated_keys: server_deprecated,
client_deprecated_keys: client_deprecated,
}
}
fn process_span_spec_compliant(
&self,
span: &mut SpanData,
deprecated_keys: &HashSet<&'static str>,
) {
span.attributes
.retain(|attr| !deprecated_keys.contains(attr.key.as_str()));
}
fn process_span_deprecated_only(
&self,
span: &mut SpanData,
deprecated_keys: &HashSet<&'static str>,
get_deprecated_key: fn(&str) -> Option<&'static str>,
) {
let mut deprecated_attrs = Vec::with_capacity(SERVER_SPEC_TO_DEPRECATED.len());
span.attributes.retain(|attr| {
if let Some(deprecated_key) = get_deprecated_key(attr.key.as_str()) {
deprecated_attrs.push(KeyValue::new(deprecated_key, attr.value.clone()));
false
} else if deprecated_keys.contains(attr.key.as_str()) {
false
} else {
true
}
});
span.attributes.extend(deprecated_attrs);
}
fn process_span_spec_and_deprecated(
&self,
span: &mut SpanData,
get_deprecated_key: fn(&str) -> Option<&'static str>,
) {
let mut deprecated_attrs = Vec::with_capacity(SERVER_SPEC_TO_DEPRECATED.len());
for attr in span.attributes.iter() {
if let Some(deprecated_key) = get_deprecated_key(attr.key.as_str()) {
deprecated_attrs.push(KeyValue::new(deprecated_key, attr.value.clone()));
}
}
if !deprecated_attrs.is_empty() {
span.attributes.extend(deprecated_attrs);
}
}
fn process_span(&self, span: &mut SpanData) {
let kind_attr = span
.attributes
.iter()
.find(|kv| kv.key.as_str() == "hive.kind");
let Some(kv) = kind_attr else {
return;
};
let opentelemetry::Value::String(kind_attribute_value) = &kv.value else {
return;
};
let Ok(kind) = HiveSpanKind::from_str(kind_attribute_value.as_str()) else {
return;
};
match kind {
HiveSpanKind::HttpServerRequest => match self.mode {
SpansSemanticConventionsMode::SpecCompliant => {
self.process_span_spec_compliant(span, &self.server_deprecated_keys);
}
SpansSemanticConventionsMode::Deprecated => {
self.process_span_deprecated_only(
span,
&self.server_deprecated_keys,
get_server_deprecated_key,
);
}
SpansSemanticConventionsMode::SpecAndDeprecated => {
self.process_span_spec_and_deprecated(span, get_server_deprecated_key);
}
},
HiveSpanKind::HttpClientRequest | HiveSpanKind::HttpInflightRequest => {
match self.mode {
SpansSemanticConventionsMode::SpecCompliant => {
self.process_span_spec_compliant(span, &self.client_deprecated_keys);
}
SpansSemanticConventionsMode::Deprecated => {
self.process_span_deprecated_only(
span,
&self.client_deprecated_keys,
get_client_deprecated_key,
);
}
SpansSemanticConventionsMode::SpecAndDeprecated => {
self.process_span_spec_and_deprecated(span, get_client_deprecated_key);
}
}
}
_ => {}
};
}
}
impl<E: SpanExporter> SpanExporter for HttpCompatibilityExporter<E> {
async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
let processed_batch = batch
.into_iter()
.map(|mut span| {
self.process_span(&mut span);
span
})
.collect();
self.inner.export(processed_batch).await
}
fn shutdown(&self) -> OTelSdkResult {
tracing::debug!(
target: targets::TELEMETRY,
layer = "http_compat_exporter",
"shutdown scheduled"
);
let result = self.inner.shutdown();
tracing::info!(
target: targets::TELEMETRY,
layer = "http_compat_exporter",
"shutdown completed"
);
result
}
fn force_flush(&self) -> OTelSdkResult {
self.inner.force_flush()
}
fn set_resource(&mut self, res: &opentelemetry_sdk::Resource) {
self.inner.set_resource(res);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::telemetry::ClientIpHeaderConfig;
use crate::telemetry::traces::spans::http_request::{
HttpClientRequestSpan, HttpServerRequestSpan,
};
use http::header::FORWARDED;
use http_body_util::Full;
use ntex::http::header::{HOST, USER_AGENT};
use ntex::util::Bytes;
use opentelemetry::trace::{Tracer, TracerProvider};
use opentelemetry_sdk::trace::{
InMemorySpanExporter, InMemorySpanExporterBuilder, SdkTracerProvider, SimpleSpanProcessor,
};
use tracing_subscriber::layer::SubscriberExt;
fn find_attribute<'a>(
span_data: &'a SpanData,
key: &'static str,
) -> Option<&'a opentelemetry::Value> {
span_data
.attributes
.iter()
.find(|kv| kv.key.as_str() == key)
.map(|kv| &kv.value)
}
fn setup_test_pipeline(
mode: SpansSemanticConventionsMode,
) -> (SdkTracerProvider, InMemorySpanExporter) {
let memory_exporter = InMemorySpanExporterBuilder::new().build();
let compatibility_exporter = HttpCompatibilityExporter::new(memory_exporter.clone(), &mode);
let processor = SimpleSpanProcessor::new(compatibility_exporter);
let provider = SdkTracerProvider::builder()
.with_span_processor(processor)
.build();
(provider, memory_exporter)
}
fn forwarded_ip_header_config() -> Option<ClientIpHeaderConfig> {
Some(ClientIpHeaderConfig::HeaderName(FORWARDED.as_str().into()))
}
fn setup_tracing_subscriber(provider: &SdkTracerProvider) -> impl Drop {
let otel_tracer = provider.tracer("http-tracer");
let telemetry_layer = tracing_opentelemetry::layer().with_tracer(otel_tracer);
let subscriber = tracing_subscriber::registry().with(telemetry_layer);
tracing::subscriber::set_default(subscriber)
}
fn assert_attribute_mapping(
span: &SpanData,
deprecated_key: &'static str,
stable_key: &'static str,
) {
let stable = find_attribute(span, stable_key);
let deprecated = find_attribute(span, deprecated_key);
assert_eq!(
deprecated, stable,
"Deprecated attribute '{}' and stable attribute '{}' mismatch",
deprecated_key, stable_key
);
}
#[test]
fn test_http_server_span_spec_compliant() {
let (provider, memory_exporter) =
setup_test_pipeline(SpansSemanticConventionsMode::SpecCompliant);
let tracer = provider.tracer("test-tracer");
let _guard = setup_tracing_subscriber(&provider);
let req = ntex::web::test::TestRequest::default();
let req = req
.uri("/test?q=1")
.version(ntex::http::Version::HTTP_11)
.method(ntex::http::Method::GET)
.header(HOST, "example.com:8080")
.header(USER_AGENT, "test-agent");
let body = Bytes::from_static(b"test body");
let http_req = req.to_http_request();
let http_res =
ntex::web::HttpResponse::build(ntex::http::StatusCode::OK).body("response body");
tracer.in_span("root", |_cx| {
let span =
HttpServerRequestSpan::from_request(&http_req, &forwarded_ip_header_config());
span.record_body_size(body.len());
span.record_response(&http_res);
});
drop(_guard);
provider.force_flush().unwrap();
let spans = memory_exporter.get_finished_spans().unwrap();
assert_eq!(spans.len(), 2);
let span = &spans[0];
assert!(find_attribute(span, "server.address").is_some());
assert!(find_attribute(span, "http.request.method").is_some());
assert!(find_attribute(span, "network.protocol.version").is_some());
assert!(find_attribute(span, "user_agent.original").is_some());
assert!(find_attribute(span, "http.request.body.size").is_some());
assert!(find_attribute(span, "url.full").is_some());
assert!(find_attribute(span, "url.path").is_some());
assert!(find_attribute(span, "http.response.status_code").is_some());
assert!(find_attribute(span, "http.host").is_none());
assert!(find_attribute(span, "http.method").is_none());
assert!(find_attribute(span, "http.flavor").is_none());
assert!(find_attribute(span, "http.user_agent").is_none());
assert!(find_attribute(span, "http.request_content_length").is_none());
assert!(find_attribute(span, "http.url").is_none());
assert!(find_attribute(span, "http.scheme").is_none());
assert!(find_attribute(span, "http.target").is_none());
assert!(find_attribute(span, "http.status_code").is_none());
}
#[test]
fn test_http_server_span_deprecated_only() {
let (provider, memory_exporter) =
setup_test_pipeline(SpansSemanticConventionsMode::Deprecated);
let tracer = provider.tracer("test-tracer");
let _guard = setup_tracing_subscriber(&provider);
let req = ntex::web::test::TestRequest::default();
let req = req
.uri("/test?q=1")
.version(ntex::http::Version::HTTP_11)
.method(ntex::http::Method::GET)
.header(HOST, "example.com:8080")
.header(USER_AGENT, "test-agent");
let body = Bytes::from_static(b"test body");
let http_req = req.to_http_request();
let http_res =
ntex::web::HttpResponse::build(ntex::http::StatusCode::OK).body("response body");
tracer.in_span("root", |_cx| {
let span =
HttpServerRequestSpan::from_request(&http_req, &forwarded_ip_header_config());
span.record_body_size(body.len());
span.record_response(&http_res);
});
drop(_guard);
provider.force_flush().unwrap();
let spans = memory_exporter.get_finished_spans().unwrap();
assert_eq!(spans.len(), 2);
let span = &spans[0];
assert!(find_attribute(span, "http.host").is_some());
assert!(find_attribute(span, "http.method").is_some());
assert!(find_attribute(span, "http.flavor").is_some());
assert!(find_attribute(span, "http.user_agent").is_some());
assert!(find_attribute(span, "http.request_content_length").is_some());
assert!(find_attribute(span, "http.url").is_some());
assert!(find_attribute(span, "http.target").is_some());
assert!(find_attribute(span, "http.status_code").is_some());
assert!(find_attribute(span, "server.address").is_none());
assert!(find_attribute(span, "http.request.method").is_none());
assert!(find_attribute(span, "network.protocol.version").is_none());
assert!(find_attribute(span, "user_agent.original").is_none());
assert!(find_attribute(span, "http.request.body.size").is_none());
assert!(find_attribute(span, "url.full").is_none());
assert!(find_attribute(span, "url.path").is_none());
assert!(find_attribute(span, "http.response.status_code").is_none());
}
#[test]
fn test_http_server_span_spec_and_deprecated() {
let (provider, memory_exporter) =
setup_test_pipeline(SpansSemanticConventionsMode::SpecAndDeprecated);
let tracer = provider.tracer("test-tracer");
let _guard = setup_tracing_subscriber(&provider);
let req = ntex::web::test::TestRequest::default();
let req = req
.uri("/test?q=1")
.version(ntex::http::Version::HTTP_11)
.method(ntex::http::Method::GET)
.header(HOST, "example.com:8080")
.header(USER_AGENT, "test-agent");
let body = Bytes::from_static(b"test body");
let http_req = req.to_http_request();
let http_res =
ntex::web::HttpResponse::build(ntex::http::StatusCode::OK).body("response body");
tracer.in_span("root", |_cx| {
let span =
HttpServerRequestSpan::from_request(&http_req, &forwarded_ip_header_config());
span.record_body_size(body.len());
span.record_response(&http_res);
});
drop(_guard);
provider.force_flush().unwrap();
let spans = memory_exporter.get_finished_spans().unwrap();
assert_eq!(spans.len(), 2);
let span = &spans[0];
assert_attribute_mapping(span, "http.host", "server.address");
assert_attribute_mapping(span, "http.method", "http.request.method");
assert_attribute_mapping(span, "http.flavor", "network.protocol.version");
assert_attribute_mapping(span, "http.user_agent", "user_agent.original");
assert_attribute_mapping(
span,
"http.request_content_length",
"http.request.body.size",
);
assert_attribute_mapping(span, "http.url", "url.full");
assert_attribute_mapping(span, "http.scheme", "url.scheme");
assert_attribute_mapping(span, "http.target", "url.path");
assert_attribute_mapping(span, "http.status_code", "http.response.status_code");
assert_attribute_mapping(
span,
"http.response_content_length",
"http.response.body.size",
);
}
#[test]
fn test_http_client_span_spec_compliant_mode() {
let (provider, memory_exporter) =
setup_test_pipeline(SpansSemanticConventionsMode::SpecCompliant);
let tracer = provider.tracer("test-tracer");
let _guard = setup_tracing_subscriber(&provider);
let request = http::Request::builder()
.method("POST")
.uri("https://api.example.com:443/v1/users")
.header(HOST, "api.example.com:443")
.version(http::Version::HTTP_2)
.body(Full::from("dummy body"))
.unwrap();
let response = http::Response::builder()
.status(200)
.body(Full::from("response body"))
.unwrap();
tracer.in_span("root", |_cx| {
let span = HttpClientRequestSpan::from_request(&request);
span.record_response(&response);
});
drop(_guard);
provider.force_flush().unwrap();
let spans = memory_exporter.get_finished_spans().unwrap();
let span = &spans[0];
assert!(find_attribute(span, "http.request.method").is_some());
assert!(find_attribute(span, "url.full").is_some());
assert!(find_attribute(span, "server.address").is_some());
assert!(find_attribute(span, "server.port").is_some());
assert!(find_attribute(span, "network.protocol.version").is_some());
assert!(find_attribute(span, "http.response.status_code").is_some());
assert!(find_attribute(span, "http.response.body.size").is_some());
assert!(find_attribute(span, "http.method").is_none());
assert!(find_attribute(span, "http.url").is_none());
assert!(find_attribute(span, "net.peer.name").is_none());
assert!(find_attribute(span, "net.peer.port").is_none());
assert!(find_attribute(span, "http.flavor").is_none());
assert!(find_attribute(span, "http.status_code").is_none());
assert!(find_attribute(span, "http.response_content_length").is_none());
}
#[test]
fn test_http_client_span_deprecated_mode() {
let (provider, memory_exporter) =
setup_test_pipeline(SpansSemanticConventionsMode::Deprecated);
let tracer = provider.tracer("test-tracer");
let _guard = setup_tracing_subscriber(&provider);
let request = http::Request::builder()
.method("POST")
.uri("https://api.example.com:443/v1/users")
.header(HOST, "api.example.com:443")
.version(http::Version::HTTP_2)
.body(Full::from("dummy body"))
.unwrap();
let response = http::Response::builder()
.status(200)
.body(Full::from("response body"))
.unwrap();
tracer.in_span("root", |_cx| {
let span = HttpClientRequestSpan::from_request(&request);
span.record_response(&response);
});
drop(_guard);
provider.force_flush().unwrap();
let spans = memory_exporter.get_finished_spans().unwrap();
let span = &spans[0];
assert!(find_attribute(span, "http.method").is_some());
assert!(find_attribute(span, "http.url").is_some());
assert!(find_attribute(span, "net.peer.name").is_some());
assert!(find_attribute(span, "net.peer.port").is_some());
assert!(find_attribute(span, "http.flavor").is_some());
assert!(find_attribute(span, "http.status_code").is_some());
assert!(find_attribute(span, "http.response_content_length").is_some());
assert!(find_attribute(span, "http.request.method").is_none());
assert!(find_attribute(span, "url.full").is_none());
assert!(find_attribute(span, "server.address").is_none());
assert!(find_attribute(span, "server.port").is_none());
assert!(find_attribute(span, "network.protocol.version").is_none());
assert!(find_attribute(span, "http.response.status_code").is_none());
assert!(find_attribute(span, "http.response.body.size").is_none());
}
#[test]
fn test_http_client_span_spec_and_deprecated_mode() {
let (provider, memory_exporter) =
setup_test_pipeline(SpansSemanticConventionsMode::SpecAndDeprecated);
let tracer = provider.tracer("test-tracer");
let _guard = setup_tracing_subscriber(&provider);
let request = http::Request::builder()
.method("POST")
.uri("https://api.example.com:443/v1/users")
.header(HOST, "api.example.com:443")
.version(http::Version::HTTP_2)
.body(Full::from("dummy body"))
.unwrap();
let response = http::Response::builder()
.status(200)
.body(Full::from("response body"))
.unwrap();
tracer.in_span("root", |_cx| {
let span = HttpClientRequestSpan::from_request(&request);
span.record_response(&response);
});
drop(_guard);
provider.force_flush().unwrap();
let spans = memory_exporter.get_finished_spans().unwrap();
let span = &spans[0];
assert!(find_attribute(span, "http.request.method").is_some());
assert!(find_attribute(span, "http.method").is_some());
assert!(find_attribute(span, "url.full").is_some());
assert!(find_attribute(span, "http.url").is_some());
assert!(find_attribute(span, "server.address").is_some());
assert!(find_attribute(span, "net.peer.name").is_some());
assert!(find_attribute(span, "server.port").is_some());
assert!(find_attribute(span, "net.peer.port").is_some());
assert!(find_attribute(span, "network.protocol.version").is_some());
assert!(find_attribute(span, "http.flavor").is_some());
assert!(find_attribute(span, "http.response.status_code").is_some());
assert!(find_attribute(span, "http.status_code").is_some());
assert!(find_attribute(span, "http.response.body.size").is_some());
assert!(find_attribute(span, "http.response_content_length").is_some());
assert_attribute_mapping(span, "net.peer.name", "server.address");
assert_attribute_mapping(span, "net.peer.port", "server.port");
assert_attribute_mapping(span, "http.flavor", "network.protocol.version");
assert_attribute_mapping(span, "http.method", "http.request.method");
assert_attribute_mapping(span, "http.url", "url.full");
assert_attribute_mapping(span, "http.status_code", "http.response.status_code");
assert_attribute_mapping(
span,
"http.response_content_length",
"http.response.body.size",
);
}
}