use apollo_http_shared::header::header_attr;
use apollo_http_shared::protocol::version_str;
use apollo_opentelemetry::tower::MakeSpan;
use opentelemetry::KeyValue;
use opentelemetry::trace::{SpanBuilder, SpanKind, Status, TraceContextExt};
use opentelemetry_semantic_conventions::attribute as semconv;
use crate::config::SpansConfig;
use crate::protocol::{HttpBody, Origin, normalize_method};
#[derive(Clone)]
pub(crate) struct HttpSpans {
spans_config: SpansConfig,
}
impl HttpSpans {
pub(crate) fn new(spans_config: SpansConfig) -> Self {
Self { spans_config }
}
pub(crate) fn request_body_size(&self) -> bool {
self.spans_config.request_body_size
}
pub(crate) fn response_body_size(&self) -> bool {
self.spans_config.response_body_size
}
pub(crate) fn on_response(
&self,
protocol_version: http::Version,
origin: &Origin,
status: http::StatusCode,
resp_headers: &http::HeaderMap,
) {
let status_code = i64::from(status.as_u16());
let cx = opentelemetry::Context::current();
cx.span()
.set_attribute(KeyValue::new(semconv::SERVER_ADDRESS, origin.address()));
if let Some(port) = origin.port() {
cx.span()
.set_attribute(KeyValue::new(semconv::SERVER_PORT, port));
}
cx.span().set_attribute(KeyValue::new(
semconv::HTTP_RESPONSE_STATUS_CODE,
status_code,
));
cx.span().set_attribute(KeyValue::new(
semconv::NETWORK_PROTOCOL_VERSION,
version_str(protocol_version),
));
for name in &self.spans_config.response_headers {
if let Some(kv) = header_attr("http.response.header", resp_headers, name.as_str()) {
cx.span().set_attribute(kv);
}
}
if status.is_client_error() || status.is_server_error() {
cx.span()
.set_attribute(KeyValue::new(semconv::ERROR_TYPE, status_code.to_string()));
cx.span().set_status(Status::error(""));
}
}
pub(crate) fn on_error(&self, origin: &Origin, error: &str) {
let cx = opentelemetry::Context::current();
cx.span()
.set_attribute(KeyValue::new(semconv::SERVER_ADDRESS, origin.address()));
if let Some(port) = origin.port() {
cx.span()
.set_attribute(KeyValue::new(semconv::SERVER_PORT, port));
}
cx.span()
.set_attribute(KeyValue::new(semconv::ERROR_TYPE, "_OTHER"));
cx.span().set_status(Status::error(error.to_owned()));
}
}
impl MakeSpan<http::Request<HttpBody>> for HttpSpans {
fn make_span(&self, req: &http::Request<HttpBody>) -> SpanBuilder {
let method = normalize_method(req.method());
let mut attrs = vec![
KeyValue::new(semconv::HTTP_REQUEST_METHOD, method),
KeyValue::new(semconv::URL_FULL, req.uri().to_string()),
];
if method == "_OTHER" {
attrs.push(KeyValue::new(
semconv::HTTP_REQUEST_METHOD_ORIGINAL,
req.method().to_string(),
));
}
if self.spans_config.url_scheme {
attrs.push(KeyValue::new(
semconv::URL_SCHEME,
req.uri().scheme_str().unwrap_or("").to_owned(),
));
}
if self.spans_config.user_agent
&& let Some(ua) = req
.headers()
.get(http::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
{
attrs.push(KeyValue::new(semconv::USER_AGENT_ORIGINAL, ua.to_owned()));
}
if self.spans_config.network_transport {
attrs.push(KeyValue::new(semconv::NETWORK_TRANSPORT, "tcp"));
}
if let Some(addr) = &self.spans_config.network_local_address {
attrs.push(KeyValue::new(semconv::NETWORK_LOCAL_ADDRESS, addr.clone()));
}
if let Some(port) = self.spans_config.network_local_port {
attrs.push(KeyValue::new(semconv::NETWORK_LOCAL_PORT, i64::from(port)));
}
for name in &self.spans_config.request_headers {
if let Some(kv) = header_attr("http.request.header", req.headers(), name.as_str()) {
attrs.push(kv);
}
}
let span_name = if method == "_OTHER" { "HTTP" } else { method };
SpanBuilder::from_name(span_name.to_owned())
.with_kind(SpanKind::Client)
.with_attributes(attrs)
}
}
#[cfg(test)]
mod tests {
use apollo_opentelemetry::tower::MakeSpan as _;
use bytes::Bytes;
use http_body_util::{BodyExt as _, Full};
use opentelemetry::Value;
use opentelemetry::trace::SpanBuilder;
use crate::config::SpansConfig;
use crate::protocol::HttpBody;
fn empty_body() -> HttpBody {
Full::new(Bytes::new())
.map_err(|e: std::convert::Infallible| match e {})
.boxed()
}
fn make_builder(config: SpansConfig, req: &http::Request<HttpBody>) -> SpanBuilder {
super::HttpSpans::new(config).make_span(req)
}
fn attr<'a>(builder: &'a SpanBuilder, key: &str) -> Option<&'a Value> {
builder
.attributes
.as_ref()?
.iter()
.find(|kv| kv.key.as_str() == key)
.map(|kv| &kv.value)
}
fn has_attr(builder: &SpanBuilder, key: &str) -> bool {
attr(builder, key).is_some()
}
#[test]
fn standard_method_sets_method_attr_and_span_name() {
let req = http::Request::builder()
.method(http::Method::GET)
.uri("http://example.com/path")
.body(empty_body())
.unwrap();
let builder = make_builder(SpansConfig::default(), &req);
assert_eq!(builder.name.as_ref(), "GET");
assert_eq!(
attr(&builder, "http.request.method"),
Some(&Value::from("GET"))
);
assert_eq!(
attr(&builder, "url.full"),
Some(&Value::from("http://example.com/path")),
);
assert!(!has_attr(&builder, "http.request.method_original"));
}
#[test]
fn non_standard_method_uses_other_and_preserves_original() {
let method = http::Method::from_bytes(b"PURGE").unwrap();
let req = http::Request::builder()
.method(method)
.uri("http://example.com/")
.body(empty_body())
.unwrap();
let builder = make_builder(SpansConfig::default(), &req);
assert_eq!(builder.name.as_ref(), "HTTP");
assert_eq!(
attr(&builder, "http.request.method"),
Some(&Value::from("_OTHER"))
);
assert_eq!(
attr(&builder, "http.request.method_original"),
Some(&Value::from("PURGE")),
);
}
#[test]
fn url_scheme_absent_by_default() {
let req = http::Request::builder()
.method(http::Method::GET)
.uri("http://example.com/")
.body(empty_body())
.unwrap();
let builder = make_builder(SpansConfig::default(), &req);
assert!(!has_attr(&builder, "url.scheme"));
}
#[test]
fn url_scheme_opt_in_adds_attribute() {
let req = http::Request::builder()
.method(http::Method::GET)
.uri("https://example.com/")
.body(empty_body())
.unwrap();
let builder = make_builder(
SpansConfig {
url_scheme: true,
..Default::default()
},
&req,
);
assert_eq!(attr(&builder, "url.scheme"), Some(&Value::from("https")));
}
#[test]
fn user_agent_absent_by_default() {
let req = http::Request::builder()
.method(http::Method::GET)
.uri("http://example.com/")
.header(http::header::USER_AGENT, "my-agent/1.0")
.body(empty_body())
.unwrap();
let builder = make_builder(SpansConfig::default(), &req);
assert!(!has_attr(&builder, "user_agent.original"));
}
#[test]
fn user_agent_opt_in_captures_header() {
let req = http::Request::builder()
.method(http::Method::GET)
.uri("http://example.com/")
.header(http::header::USER_AGENT, "my-agent/1.0")
.body(empty_body())
.unwrap();
let builder = make_builder(
SpansConfig {
user_agent: true,
..Default::default()
},
&req,
);
assert_eq!(
attr(&builder, "user_agent.original"),
Some(&Value::from("my-agent/1.0")),
);
}
#[test]
fn network_transport_opt_in_is_tcp() {
let req = http::Request::builder()
.method(http::Method::GET)
.uri("http://example.com/")
.body(empty_body())
.unwrap();
let builder = make_builder(
SpansConfig {
network_transport: true,
..Default::default()
},
&req,
);
assert_eq!(
attr(&builder, "network.transport"),
Some(&Value::from("tcp"))
);
}
#[test]
fn network_local_address_opt_in() {
let req = http::Request::builder()
.method(http::Method::GET)
.uri("http://example.com/")
.body(empty_body())
.unwrap();
let builder = make_builder(
SpansConfig {
network_local_address: Some("10.0.0.1".to_owned()),
..Default::default()
},
&req,
);
assert_eq!(
attr(&builder, "network.local.address"),
Some(&Value::from("10.0.0.1")),
);
}
#[test]
fn network_local_port_opt_in() {
let req = http::Request::builder()
.method(http::Method::GET)
.uri("http://example.com/")
.body(empty_body())
.unwrap();
let builder = make_builder(
SpansConfig {
network_local_port: Some(8080),
..Default::default()
},
&req,
);
assert_eq!(
attr(&builder, "network.local.port"),
Some(&Value::I64(8080))
);
}
}