From 14e83871414417aebb8017061bd1636ccaad8dfc Mon Sep 17 00:00:00 2001 From: Fletcher Nichol Date: Thu, 11 Jan 2024 15:44:39 -0700 Subject: [PATCH] feat(sdf,telemetry): update HTTP status on all request spans This change sets the `http.response.status_code` span field on the parent span of each incoming HTTP request. Future and different work will be needed to augment the span with error-specific information as it will involve implementating another Tower middleware to intercept the error types. Signed-off-by: Fletcher Nichol --- lib/sdf-server/src/server/server.rs | 9 +- lib/telemetry-http-rs/src/lib.rs | 347 +---------------------- lib/telemetry-http-rs/src/make_span.rs | 344 ++++++++++++++++++++++ lib/telemetry-http-rs/src/on_response.rs | 110 +++++++ lib/telemetry-rs/src/lib.rs | 10 +- 5 files changed, 469 insertions(+), 351 deletions(-) create mode 100644 lib/telemetry-http-rs/src/make_span.rs create mode 100644 lib/telemetry-http-rs/src/on_response.rs diff --git a/lib/sdf-server/src/server/server.rs b/lib/sdf-server/src/server/server.rs index 95b9459596..9101e00e55 100644 --- a/lib/sdf-server/src/server/server.rs +++ b/lib/sdf-server/src/server/server.rs @@ -27,7 +27,7 @@ use si_pkg::{SiPkg, SiPkgError}; use si_posthog::{PosthogClient, PosthogConfig}; use si_std::SensitiveString; use telemetry::prelude::*; -use telemetry_http::HttpMakeSpan; +use telemetry_http::{HttpMakeSpan, HttpOnResponse}; use thiserror::Error; use tokio::{ io::{AsyncRead, AsyncWrite}, @@ -482,8 +482,11 @@ fn build_service_inner( for_tests, ); - let routes = routes(state) - .layer(TraceLayer::new_for_http().make_span_with(HttpMakeSpan::new().level(Level::INFO))); + let routes = routes(state).layer( + TraceLayer::new_for_http() + .make_span_with(HttpMakeSpan::new().level(Level::INFO)) + .on_response(HttpOnResponse::new().level(Level::DEBUG)), + ); let graceful_shutdown_rx = prepare_graceful_shutdown(shutdown_rx, shutdown_broadcast_tx)?; diff --git a/lib/telemetry-http-rs/src/lib.rs b/lib/telemetry-http-rs/src/lib.rs index ded514f963..3faa7beed3 100644 --- a/lib/telemetry-http-rs/src/lib.rs +++ b/lib/telemetry-http-rs/src/lib.rs @@ -11,347 +11,8 @@ missing_docs )] -use hyper::header::USER_AGENT; -use telemetry::prelude::*; -use tower_http::trace::MakeSpan; +mod make_span; +mod on_response; -/// An implementation of [`MakeSpan`] to generate [`Span`]s from incoming HTTP requests. -#[derive(Clone, Debug)] -pub struct HttpMakeSpan { - level: Level, - - // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes - network_protocol_name: &'static str, - network_transport: NetworkTransport, -} - -impl HttpMakeSpan { - /// Creates a new `HttpMakeSpan`. - pub fn new() -> Self { - Self::default() - } - - /// Sets the [`Level`] used for the tracing [`Span`]. - pub fn level(mut self, level: Level) -> Self { - self.level = level; - self - } - - /// Sets the network [protocol name] to be used in span metadata. - /// - /// Defaults to `"http"`. - /// - /// [protocol name]: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ - pub fn network_protocol_name(mut self, s: &'static str) -> Self { - self.network_protocol_name = s; - self - } - - /// Sets the network [protocol version] to be used in span metadata. - /// - /// Defaults to `tcp`. - /// - /// [protocol version]: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ - pub fn network_transport(mut self, nt: NetworkTransport) -> Self { - self.network_transport = nt; - self - } - - fn span_from_request(&mut self, request: &hyper::Request) -> Span { - #[derive(Clone, Copy, Debug)] - enum InnerMethod { - Options, - Get, - Post, - Put, - Delete, - Head, - Trace, - Connect, - Patch, - Other, - } - impl From<&str> for InnerMethod { - fn from(value: &str) -> Self { - match value { - "OPTIONS" => Self::Options, - "GET" => Self::Get, - "POST" => Self::Post, - "PUT" => Self::Put, - "DELETE" => Self::Delete, - "HEAD" => Self::Head, - "TRACE" => Self::Trace, - "CONNECT" => Self::Connect, - "PATCH" => Self::Patch, - _ => Self::Other, - } - } - } - impl InnerMethod { - fn as_str(&self) -> &'static str { - match self { - Self::Options => "OPTIONS", - Self::Get => "GET", - Self::Post => "POST", - Self::Put => "PUT", - Self::Delete => "DELETE", - Self::Head => "HEAD", - Self::Trace => "TRACE", - Self::Connect => "CONNECT", - Self::Patch => "PATCH", - // > If the HTTP request method is not known to instrumentation, it MUST set - // > the http.request.method attribute to _OTHER. - // - // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes - Self::Other => "_OTHER", - } - } - } - - enum InnerLevel { - Error, - Warn, - Info, - Debug, - Trace, - } - impl From for InnerLevel { - fn from(value: Level) -> Self { - match value { - Level::ERROR => InnerLevel::Error, - Level::WARN => InnerLevel::Warn, - Level::INFO => InnerLevel::Info, - Level::DEBUG => InnerLevel::Debug, - _ => InnerLevel::Trace, - } - } - } - - let uri = request.uri(); - - let http_request_method = InnerMethod::from(request.method().as_str()); - let network_protocol_version = HttpVersion::from(request.version()); - - // This ugly macro is needed, unfortunately, because `tracing::span!` required the level - // argument to be static. Meaning we can't just pass `self.level` and a dynamic name. - macro_rules! inner { - ($level:expr, $name:expr) => { - ::telemetry::tracing::span!( - $level, - $name, - - // Common HTTP attributes - // - // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes - - http.request.method = http_request_method.as_str(), - // http.response.header. - http.response.status_code = Empty, - // network.peer.address = Empty, - // network.peer.port = Empty, - network.protocol.name = self.network_protocol_name, - network.protocol.version = network_protocol_version.as_str(), - network.transport = self.network_transport.as_str(), - - // HTTP Server semantic conventions - // - // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server - - // client.address = Empty, - // client.port = Empty, - // http.request.header. - // network.local.address = Empty, - // network.local.port = Empty, - // server.address = Empty, - // server.port = Empty, - url.path = uri.path(), - url.query = uri.query(), - url.scheme = Empty, - user_agent.original = Empty, - - // Set special `otel.*` fields which tracing-opentelemetry will use when - // transmitting traces via OpenTelemetry protocol - // - // See: - // https://docs.rs/tracing-opentelemetry/0.22.0/tracing_opentelemetry/#special-fields - // - - otel.kind = SpanKind::Server.as_str(), - // TODO(fnichol): would love this to be "{method} {route}" but should limit - // detail in route to preserve low cardinality. - // - // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#method-placeholder - // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name - otel.name = $name, - // Default for OpenTelemetry status is `Unset` which should map to an empty/unset - // tracing value. - // - // See: https://docs.rs/opentelemetry/0.21.0/opentelemetry/trace/enum.Status.html - otel.status_code = Empty, - // Only set if status_code == Error - otel.status_message = Empty, - ) - }; - } - - let span = match (InnerLevel::from(self.level), http_request_method) { - (InnerLevel::Error, InnerMethod::Options) => inner!(Level::ERROR, "OPTIONS"), - (InnerLevel::Error, InnerMethod::Get) => inner!(Level::ERROR, "GET"), - (InnerLevel::Error, InnerMethod::Post) => inner!(Level::ERROR, "POST"), - (InnerLevel::Error, InnerMethod::Put) => inner!(Level::ERROR, "PUT"), - (InnerLevel::Error, InnerMethod::Delete) => inner!(Level::ERROR, "DELETE"), - (InnerLevel::Error, InnerMethod::Head) => inner!(Level::ERROR, "HEAD"), - (InnerLevel::Error, InnerMethod::Trace) => inner!(Level::ERROR, "TRACE"), - (InnerLevel::Error, InnerMethod::Connect) => inner!(Level::ERROR, "CONNECT"), - (InnerLevel::Error, InnerMethod::Patch) => inner!(Level::ERROR, "PATCH"), - (InnerLevel::Error, InnerMethod::Other) => inner!(Level::ERROR, "HTTP"), - (InnerLevel::Warn, InnerMethod::Options) => inner!(Level::WARN, "OPTIONS"), - (InnerLevel::Warn, InnerMethod::Get) => inner!(Level::WARN, "GET"), - (InnerLevel::Warn, InnerMethod::Post) => inner!(Level::WARN, "POST"), - (InnerLevel::Warn, InnerMethod::Put) => inner!(Level::WARN, "PUT"), - (InnerLevel::Warn, InnerMethod::Delete) => inner!(Level::WARN, "DELETE"), - (InnerLevel::Warn, InnerMethod::Head) => inner!(Level::WARN, "HEAD"), - (InnerLevel::Warn, InnerMethod::Trace) => inner!(Level::WARN, "TRACE"), - (InnerLevel::Warn, InnerMethod::Connect) => inner!(Level::WARN, "CONNECT"), - (InnerLevel::Warn, InnerMethod::Patch) => inner!(Level::WARN, "PATCH"), - (InnerLevel::Warn, InnerMethod::Other) => inner!(Level::WARN, "HTTP"), - (InnerLevel::Info, InnerMethod::Options) => inner!(Level::INFO, "OPTIONS"), - (InnerLevel::Info, InnerMethod::Get) => inner!(Level::INFO, "GET"), - (InnerLevel::Info, InnerMethod::Post) => inner!(Level::INFO, "POST"), - (InnerLevel::Info, InnerMethod::Put) => inner!(Level::INFO, "PUT"), - (InnerLevel::Info, InnerMethod::Delete) => inner!(Level::INFO, "DELETE"), - (InnerLevel::Info, InnerMethod::Head) => inner!(Level::INFO, "HEAD"), - (InnerLevel::Info, InnerMethod::Trace) => inner!(Level::INFO, "TRACE"), - (InnerLevel::Info, InnerMethod::Connect) => inner!(Level::INFO, "CONNECT"), - (InnerLevel::Info, InnerMethod::Patch) => inner!(Level::INFO, "PATCH"), - (InnerLevel::Info, InnerMethod::Other) => inner!(Level::INFO, "HTTP"), - (InnerLevel::Debug, InnerMethod::Options) => inner!(Level::DEBUG, "OPTIONS"), - (InnerLevel::Debug, InnerMethod::Get) => inner!(Level::DEBUG, "GET"), - (InnerLevel::Debug, InnerMethod::Post) => inner!(Level::DEBUG, "POST"), - (InnerLevel::Debug, InnerMethod::Put) => inner!(Level::DEBUG, "PUT"), - (InnerLevel::Debug, InnerMethod::Delete) => inner!(Level::DEBUG, "DELETE"), - (InnerLevel::Debug, InnerMethod::Head) => inner!(Level::DEBUG, "HEAD"), - (InnerLevel::Debug, InnerMethod::Trace) => inner!(Level::DEBUG, "TRACE"), - (InnerLevel::Debug, InnerMethod::Connect) => inner!(Level::DEBUG, "CONNECT"), - (InnerLevel::Debug, InnerMethod::Patch) => inner!(Level::DEBUG, "PATCH"), - (InnerLevel::Debug, InnerMethod::Other) => inner!(Level::DEBUG, "HTTP"), - (InnerLevel::Trace, InnerMethod::Options) => inner!(Level::TRACE, "OPTIONS"), - (InnerLevel::Trace, InnerMethod::Get) => inner!(Level::TRACE, "GET"), - (InnerLevel::Trace, InnerMethod::Post) => inner!(Level::TRACE, "POST"), - (InnerLevel::Trace, InnerMethod::Put) => inner!(Level::TRACE, "PUT"), - (InnerLevel::Trace, InnerMethod::Delete) => inner!(Level::TRACE, "DELETE"), - (InnerLevel::Trace, InnerMethod::Head) => inner!(Level::TRACE, "HEAD"), - (InnerLevel::Trace, InnerMethod::Trace) => inner!(Level::TRACE, "TRACE"), - (InnerLevel::Trace, InnerMethod::Connect) => inner!(Level::TRACE, "CONNECT"), - (InnerLevel::Trace, InnerMethod::Patch) => inner!(Level::TRACE, "PATCH"), - (InnerLevel::Trace, InnerMethod::Other) => inner!(Level::TRACE, "HTTP"), - }; - - if let Some(url_scheme) = uri.scheme() { - span.record("url.scheme", url_scheme.as_str()); - } - if let Some(user_agent_original) = request.headers().get(USER_AGENT) { - span.record( - "user_agent.original", - user_agent_original.to_str().unwrap_or("invalid-ascii"), - ); - } - - span - } -} - -impl Default for HttpMakeSpan { - #[inline] - fn default() -> Self { - Self { - level: Level::INFO, - network_protocol_name: "http", - network_transport: NetworkTransport::default(), - } - } -} - -impl MakeSpan for HttpMakeSpan { - fn make_span(&mut self, request: &hyper::Request) -> Span { - self.span_from_request(request) - } -} - -/// Represents the [OSI transport layer] as described in the OpenTelemetry [network] specification. -/// -/// [network]: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ -/// [OSI transport layer]: https://osi-model.com/transport-layer/ -#[remain::sorted] -#[derive(Clone, Copy, Debug)] -pub enum NetworkTransport { - /// Named or anonymous pipe - Pipe, - /// TCP - Tcp, - /// UDP - Udp, - /// Unix domain socket - Unix, -} - -impl NetworkTransport { - fn as_str(&self) -> &'static str { - match self { - Self::Pipe => "pipe", - Self::Tcp => "tcp", - Self::Udp => "udp", - Self::Unix => "unix", - } - } -} - -impl Default for NetworkTransport { - #[inline] - fn default() -> Self { - Self::Tcp - } -} - -#[derive(Clone, Copy, Debug)] -enum HttpVersion { - Http09, - Http10, - Http11, - Http2, - Http3, - Unknown, -} - -impl Default for HttpVersion { - #[inline] - fn default() -> Self { - Self::Http11 - } -} - -impl From for HttpVersion { - fn from(value: hyper::Version) -> Self { - match value { - hyper::Version::HTTP_09 => Self::Http09, - hyper::Version::HTTP_10 => Self::Http10, - hyper::Version::HTTP_11 => Self::Http11, - hyper::Version::HTTP_2 => Self::Http2, - hyper::Version::HTTP_3 => Self::Http3, - _ => Self::Unknown, - } - } -} - -impl HttpVersion { - fn as_str(&self) -> &'static str { - match self { - Self::Http09 => "0.9", - Self::Http10 => "1.0", - Self::Http11 => "1.1", - Self::Http2 => "2", - Self::Http3 => "3", - Self::Unknown => "_UNKNOWN", - } - } -} +pub use make_span::{HttpMakeSpan, NetworkTransport}; +pub use on_response::HttpOnResponse; diff --git a/lib/telemetry-http-rs/src/make_span.rs b/lib/telemetry-http-rs/src/make_span.rs new file mode 100644 index 0000000000..db2104b5e6 --- /dev/null +++ b/lib/telemetry-http-rs/src/make_span.rs @@ -0,0 +1,344 @@ +use hyper::header::USER_AGENT; +use telemetry::prelude::*; +use tower_http::trace::MakeSpan; + +/// An implementation of [`MakeSpan`] to generate [`Span`]s from incoming HTTP requests. +#[derive(Clone, Debug)] +pub struct HttpMakeSpan { + level: Level, + + // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes + network_protocol_name: &'static str, + network_transport: NetworkTransport, +} + +impl Default for HttpMakeSpan { + #[inline] + fn default() -> Self { + Self { + level: Level::INFO, + network_protocol_name: "http", + network_transport: NetworkTransport::default(), + } + } +} + +impl HttpMakeSpan { + /// Creates a new `HttpMakeSpan`. + pub fn new() -> Self { + Self::default() + } + + /// Sets the [`Level`] used for the tracing [`Span`]. + pub fn level(mut self, level: Level) -> Self { + self.level = level; + self + } + + /// Sets the network [protocol name] to be used in span metadata. + /// + /// Defaults to `"http"`. + /// + /// [protocol name]: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ + pub fn network_protocol_name(mut self, s: &'static str) -> Self { + self.network_protocol_name = s; + self + } + + /// Sets the network [protocol version] to be used in span metadata. + /// + /// Defaults to `tcp`. + /// + /// [protocol version]: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ + pub fn network_transport(mut self, nt: NetworkTransport) -> Self { + self.network_transport = nt; + self + } + + fn span_from_request(&mut self, request: &hyper::Request) -> Span { + #[derive(Clone, Copy, Debug)] + enum InnerMethod { + Options, + Get, + Post, + Put, + Delete, + Head, + Trace, + Connect, + Patch, + Other, + } + impl From<&str> for InnerMethod { + fn from(value: &str) -> Self { + match value { + "OPTIONS" => Self::Options, + "GET" => Self::Get, + "POST" => Self::Post, + "PUT" => Self::Put, + "DELETE" => Self::Delete, + "HEAD" => Self::Head, + "TRACE" => Self::Trace, + "CONNECT" => Self::Connect, + "PATCH" => Self::Patch, + _ => Self::Other, + } + } + } + impl InnerMethod { + fn as_str(&self) -> &'static str { + match self { + Self::Options => "OPTIONS", + Self::Get => "GET", + Self::Post => "POST", + Self::Put => "PUT", + Self::Delete => "DELETE", + Self::Head => "HEAD", + Self::Trace => "TRACE", + Self::Connect => "CONNECT", + Self::Patch => "PATCH", + // > If the HTTP request method is not known to instrumentation, it MUST set + // > the http.request.method attribute to _OTHER. + // + // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes + Self::Other => "_OTHER", + } + } + } + + enum InnerLevel { + Error, + Warn, + Info, + Debug, + Trace, + } + impl From for InnerLevel { + fn from(value: Level) -> Self { + match value { + Level::ERROR => InnerLevel::Error, + Level::WARN => InnerLevel::Warn, + Level::INFO => InnerLevel::Info, + Level::DEBUG => InnerLevel::Debug, + _ => InnerLevel::Trace, + } + } + } + + let uri = request.uri(); + + let http_request_method = InnerMethod::from(request.method().as_str()); + let network_protocol_version = HttpVersion::from(request.version()); + + // This ugly macro is needed, unfortunately, because `tracing::span!` required the level + // argument to be static. Meaning we can't just pass `self.level` and a dynamic name. + macro_rules! inner { + ($level:expr, $name:expr) => { + ::telemetry::tracing::span!( + $level, + $name, + + // Common HTTP attributes + // + // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes + + http.request.method = http_request_method.as_str(), + // http.response.header. + http.response.status_code = Empty, + // network.peer.address = Empty, + // network.peer.port = Empty, + network.protocol.name = self.network_protocol_name, + network.protocol.version = network_protocol_version.as_str(), + network.transport = self.network_transport.as_str(), + + // HTTP Server semantic conventions + // + // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server + + // client.address = Empty, + // client.port = Empty, + // http.request.header. + // network.local.address = Empty, + // network.local.port = Empty, + // server.address = Empty, + // server.port = Empty, + url.path = uri.path(), + url.query = uri.query(), + url.scheme = Empty, + user_agent.original = Empty, + + // Set special `otel.*` fields which tracing-opentelemetry will use when + // transmitting traces via OpenTelemetry protocol + // + // See: + // https://docs.rs/tracing-opentelemetry/0.22.0/tracing_opentelemetry/#special-fields + // + + otel.kind = SpanKind::Server.as_str(), + // TODO(fnichol): would love this to be "{method} {route}" but should limit + // detail in route to preserve low cardinality. + // + // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#method-placeholder + // See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name + otel.name = $name, + // Default for OpenTelemetry status is `Unset` which should map to an empty/unset + // tracing value. + // + // See: https://docs.rs/opentelemetry/0.21.0/opentelemetry/trace/enum.Status.html + otel.status_code = Empty, + // Only set if status_code == Error + otel.status_message = Empty, + ) + }; + } + + let span = match (InnerLevel::from(self.level), http_request_method) { + (InnerLevel::Error, InnerMethod::Options) => inner!(Level::ERROR, "OPTIONS"), + (InnerLevel::Error, InnerMethod::Get) => inner!(Level::ERROR, "GET"), + (InnerLevel::Error, InnerMethod::Post) => inner!(Level::ERROR, "POST"), + (InnerLevel::Error, InnerMethod::Put) => inner!(Level::ERROR, "PUT"), + (InnerLevel::Error, InnerMethod::Delete) => inner!(Level::ERROR, "DELETE"), + (InnerLevel::Error, InnerMethod::Head) => inner!(Level::ERROR, "HEAD"), + (InnerLevel::Error, InnerMethod::Trace) => inner!(Level::ERROR, "TRACE"), + (InnerLevel::Error, InnerMethod::Connect) => inner!(Level::ERROR, "CONNECT"), + (InnerLevel::Error, InnerMethod::Patch) => inner!(Level::ERROR, "PATCH"), + (InnerLevel::Error, InnerMethod::Other) => inner!(Level::ERROR, "HTTP"), + (InnerLevel::Warn, InnerMethod::Options) => inner!(Level::WARN, "OPTIONS"), + (InnerLevel::Warn, InnerMethod::Get) => inner!(Level::WARN, "GET"), + (InnerLevel::Warn, InnerMethod::Post) => inner!(Level::WARN, "POST"), + (InnerLevel::Warn, InnerMethod::Put) => inner!(Level::WARN, "PUT"), + (InnerLevel::Warn, InnerMethod::Delete) => inner!(Level::WARN, "DELETE"), + (InnerLevel::Warn, InnerMethod::Head) => inner!(Level::WARN, "HEAD"), + (InnerLevel::Warn, InnerMethod::Trace) => inner!(Level::WARN, "TRACE"), + (InnerLevel::Warn, InnerMethod::Connect) => inner!(Level::WARN, "CONNECT"), + (InnerLevel::Warn, InnerMethod::Patch) => inner!(Level::WARN, "PATCH"), + (InnerLevel::Warn, InnerMethod::Other) => inner!(Level::WARN, "HTTP"), + (InnerLevel::Info, InnerMethod::Options) => inner!(Level::INFO, "OPTIONS"), + (InnerLevel::Info, InnerMethod::Get) => inner!(Level::INFO, "GET"), + (InnerLevel::Info, InnerMethod::Post) => inner!(Level::INFO, "POST"), + (InnerLevel::Info, InnerMethod::Put) => inner!(Level::INFO, "PUT"), + (InnerLevel::Info, InnerMethod::Delete) => inner!(Level::INFO, "DELETE"), + (InnerLevel::Info, InnerMethod::Head) => inner!(Level::INFO, "HEAD"), + (InnerLevel::Info, InnerMethod::Trace) => inner!(Level::INFO, "TRACE"), + (InnerLevel::Info, InnerMethod::Connect) => inner!(Level::INFO, "CONNECT"), + (InnerLevel::Info, InnerMethod::Patch) => inner!(Level::INFO, "PATCH"), + (InnerLevel::Info, InnerMethod::Other) => inner!(Level::INFO, "HTTP"), + (InnerLevel::Debug, InnerMethod::Options) => inner!(Level::DEBUG, "OPTIONS"), + (InnerLevel::Debug, InnerMethod::Get) => inner!(Level::DEBUG, "GET"), + (InnerLevel::Debug, InnerMethod::Post) => inner!(Level::DEBUG, "POST"), + (InnerLevel::Debug, InnerMethod::Put) => inner!(Level::DEBUG, "PUT"), + (InnerLevel::Debug, InnerMethod::Delete) => inner!(Level::DEBUG, "DELETE"), + (InnerLevel::Debug, InnerMethod::Head) => inner!(Level::DEBUG, "HEAD"), + (InnerLevel::Debug, InnerMethod::Trace) => inner!(Level::DEBUG, "TRACE"), + (InnerLevel::Debug, InnerMethod::Connect) => inner!(Level::DEBUG, "CONNECT"), + (InnerLevel::Debug, InnerMethod::Patch) => inner!(Level::DEBUG, "PATCH"), + (InnerLevel::Debug, InnerMethod::Other) => inner!(Level::DEBUG, "HTTP"), + (InnerLevel::Trace, InnerMethod::Options) => inner!(Level::TRACE, "OPTIONS"), + (InnerLevel::Trace, InnerMethod::Get) => inner!(Level::TRACE, "GET"), + (InnerLevel::Trace, InnerMethod::Post) => inner!(Level::TRACE, "POST"), + (InnerLevel::Trace, InnerMethod::Put) => inner!(Level::TRACE, "PUT"), + (InnerLevel::Trace, InnerMethod::Delete) => inner!(Level::TRACE, "DELETE"), + (InnerLevel::Trace, InnerMethod::Head) => inner!(Level::TRACE, "HEAD"), + (InnerLevel::Trace, InnerMethod::Trace) => inner!(Level::TRACE, "TRACE"), + (InnerLevel::Trace, InnerMethod::Connect) => inner!(Level::TRACE, "CONNECT"), + (InnerLevel::Trace, InnerMethod::Patch) => inner!(Level::TRACE, "PATCH"), + (InnerLevel::Trace, InnerMethod::Other) => inner!(Level::TRACE, "HTTP"), + }; + + if let Some(url_scheme) = uri.scheme() { + span.record("url.scheme", url_scheme.as_str()); + } + if let Some(user_agent_original) = request.headers().get(USER_AGENT) { + span.record( + "user_agent.original", + user_agent_original.to_str().unwrap_or("invalid-ascii"), + ); + } + + span + } +} + +impl MakeSpan for HttpMakeSpan { + fn make_span(&mut self, request: &hyper::Request) -> Span { + self.span_from_request(request) + } +} + +/// Represents the [OSI transport layer] as described in the OpenTelemetry [network] specification. +/// +/// [network]: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ +/// [OSI transport layer]: https://osi-model.com/transport-layer/ +#[remain::sorted] +#[derive(Clone, Copy, Debug)] +pub enum NetworkTransport { + /// Named or anonymous pipe + Pipe, + /// TCP + Tcp, + /// UDP + Udp, + /// Unix domain socket + Unix, +} + +impl NetworkTransport { + fn as_str(&self) -> &'static str { + match self { + Self::Pipe => "pipe", + Self::Tcp => "tcp", + Self::Udp => "udp", + Self::Unix => "unix", + } + } +} + +impl Default for NetworkTransport { + #[inline] + fn default() -> Self { + Self::Tcp + } +} + +#[derive(Clone, Copy, Debug)] +enum HttpVersion { + Http09, + Http10, + Http11, + Http2, + Http3, + Unknown, +} + +impl Default for HttpVersion { + #[inline] + fn default() -> Self { + Self::Http11 + } +} + +impl From for HttpVersion { + fn from(value: hyper::Version) -> Self { + match value { + hyper::Version::HTTP_09 => Self::Http09, + hyper::Version::HTTP_10 => Self::Http10, + hyper::Version::HTTP_11 => Self::Http11, + hyper::Version::HTTP_2 => Self::Http2, + hyper::Version::HTTP_3 => Self::Http3, + _ => Self::Unknown, + } + } +} + +impl HttpVersion { + fn as_str(&self) -> &'static str { + match self { + Self::Http09 => "0.9", + Self::Http10 => "1.0", + Self::Http11 => "1.1", + Self::Http2 => "2", + Self::Http3 => "3", + Self::Unknown => "_UNKNOWN", + } + } +} diff --git a/lib/telemetry-http-rs/src/on_response.rs b/lib/telemetry-http-rs/src/on_response.rs new file mode 100644 index 0000000000..1693171710 --- /dev/null +++ b/lib/telemetry-http-rs/src/on_response.rs @@ -0,0 +1,110 @@ +use std::{fmt, time::Duration}; + +use telemetry::{prelude::*, OtelStatusCode}; +use tower_http::{trace::OnResponse, LatencyUnit}; + +/// An implementation of [`OnResponse`] to update span fields for HTTP responses. +#[derive(Clone, Debug)] +pub struct HttpOnResponse { + level: Level, + latency_unit: LatencyUnit, +} + +impl Default for HttpOnResponse { + #[inline] + fn default() -> Self { + Self { + level: Level::DEBUG, + latency_unit: LatencyUnit::Millis, + } + } +} + +impl HttpOnResponse { + /// Creates a new `HttpOnResponse`. + pub fn new() -> Self { + Self::default() + } + + /// Sets the [`Level`] used for the tracing [`Span`]. + pub fn level(mut self, level: Level) -> Self { + self.level = level; + self + } +} + +impl OnResponse for HttpOnResponse { + fn on_response(self, response: &hyper::Response, latency: Duration, span: &Span) { + let status = response.status(); + span.record("http.response.status_code", status.as_u16()); + + // TODO(fnichol): set response headers if useful? + + // In OpenTelemetry HTTP spans, only HTTP/5xx errors should set an `ERROR` value for + // `otel.status_code` (except for other errors such as transient/network errors). + // + // > Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // > unless there was another error (e.g., network error receiving the response body; or + // > 3xx codes with max redirects exceeded), in which case status MUST be set to Error. + // + // > For HTTP status codes in the 4xx range span status MUST be left unset in case of + // > SpanKind.SERVER and MUST be set to Error in case of SpanKind.CLIENT. + // + // See: + if status.is_server_error() { + span.record("otel.status_code", OtelStatusCode::Error.as_str()); + } + + macro_rules! inner_event { + ($level:expr, $($tt:tt)*) => { + match $level { + ::telemetry::tracing::Level::ERROR => { + ::telemetry::tracing::error!($($tt)*); + } + ::telemetry::tracing::Level::WARN => { + ::telemetry::tracing::warn!($($tt)*); + } + ::telemetry::tracing::Level::INFO => { + ::telemetry::tracing::info!($($tt)*); + } + ::telemetry::tracing::Level::DEBUG => { + ::telemetry::tracing::debug!($($tt)*); + } + ::telemetry::tracing::Level::TRACE => { + ::telemetry::tracing::trace!($($tt)*); + } + } + }; + } + + let latency = Latency { + unit: self.latency_unit, + duration: latency, + }; + + inner_event!( + self.level, + %latency, + status = status.as_u16(), + "finished processing request", + ); + } +} + +// From `tower_http::trace` +struct Latency { + unit: LatencyUnit, + duration: Duration, +} + +impl fmt::Display for Latency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.unit { + LatencyUnit::Seconds => write!(f, "{} s", self.duration.as_secs_f64()), + LatencyUnit::Millis => write!(f, "{} ms", self.duration.as_millis()), + LatencyUnit::Micros => write!(f, "{} μs", self.duration.as_micros()), + LatencyUnit::Nanos => write!(f, "{} ns", self.duration.as_nanos()), + _ => write!(f, "{:?} (unknown)", self.duration), + } + } +} diff --git a/lib/telemetry-rs/src/lib.rs b/lib/telemetry-rs/src/lib.rs index 5ed69b804e..ce966faab3 100644 --- a/lib/telemetry-rs/src/lib.rs +++ b/lib/telemetry-rs/src/lib.rs @@ -33,7 +33,7 @@ pub mod prelude { #[remain::sorted] #[derive(Clone, Copy, Debug)] -enum StatusCode { +pub enum OtelStatusCode { Error, Ok, // Unset is not currently used, although represents a valid state. @@ -41,8 +41,8 @@ enum StatusCode { Unset, } -impl StatusCode { - fn as_str(&self) -> &'static str { +impl OtelStatusCode { + pub fn as_str(&self) -> &'static str { match self { Self::Error => "ERROR", Self::Ok => "OK", @@ -97,14 +97,14 @@ pub trait SpanExt { impl SpanExt for tracing::Span { fn record_ok(&self) { - self.record("otel.status_code", StatusCode::Ok.as_str()); + self.record("otel.status_code", OtelStatusCode::Ok.as_str()); } fn record_err(&self, err: E) -> E where E: Debug + Display, { - self.record("otel.status_code", StatusCode::Error.as_str()); + self.record("otel.status_code", OtelStatusCode::Error.as_str()); self.record("otel.status_message", err.to_string().as_str()); err }