diff --git a/Cargo.toml b/Cargo.toml index f48d531..bc4db8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ name = "init4-bin-base" description = "Internal utilities for binaries produced by the init4 team" keywords = ["init4", "bin", "base"] -version = "0.15.0" +version = "0.15.1" edition = "2021" rust-version = "1.85" authors = ["init4", "James Prestwich", "evalir"] @@ -30,6 +30,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re # OTLP opentelemetry_sdk = "0.30.0" opentelemetry = "0.30.0" +opentelemetry-http = "0.30.0" opentelemetry-otlp = "0.30.0" opentelemetry-semantic-conventions = { version = "0.30.0", features = ["semconv_experimental"] } tracing-opentelemetry = "0.31.0" @@ -47,12 +48,12 @@ oauth2 = { version = "5.0.0", optional = true } tokio = { version = "1.36.0", optional = true } # Other -thiserror = "2.0.11" +axum = "0.8.1" serde = { version = "1", features = ["derive"] } +thiserror = "2.0.11" +tower = "0.5.2" async-trait = { version = "0.1.80", optional = true } eyre = { version = "0.6.12", optional = true } -axum = { version = "0.8.1", optional = true } -tower = { version = "0.5.2", optional = true } # AWS aws-config = { version = "1.1.7", optional = true } @@ -72,7 +73,7 @@ tokio = { version = "1.43.0", features = ["macros"] } default = ["alloy", "rustls"] alloy = ["dep:alloy"] aws = ["alloy", "alloy?/signer-aws", "dep:async-trait", "dep:aws-config", "dep:aws-sdk-kms"] -perms = ["dep:oauth2", "dep:tokio", "dep:reqwest", "dep:signet-tx-cache", "dep:eyre", "dep:axum", "dep:tower"] +perms = ["dep:oauth2", "dep:tokio", "dep:reqwest", "dep:signet-tx-cache", "dep:eyre"] rustls = ["dep:rustls", "rustls/aws-lc-rs"] [[example]] diff --git a/src/lib.rs b/src/lib.rs index 57d7052..a114ff5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,9 @@ pub mod utils { /// Prometheus metrics utilities. pub mod metrics; + /// Axum OpenTelemetry utilities. + pub mod otel_axum; + /// OpenTelemetry utilities. pub mod otlp; diff --git a/src/utils/otel_axum.rs b/src/utils/otel_axum.rs new file mode 100644 index 0000000..5875e25 --- /dev/null +++ b/src/utils/otel_axum.rs @@ -0,0 +1,69 @@ +use axum::extract::{MatchedPath, Request}; +use tower::{Layer, Service}; +use tracing::{info_span, instrument::Instrumented, Instrument}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +/// A [`Layer`] that adds OpenTelemetry spans to Axum requests. +#[derive(Debug, Clone, Copy)] +pub struct OtelAxumSpanLayer; + +/// A simple service +#[derive(Debug, Clone)] +pub struct OtelAxumSpanner { + inner: S, +} + +impl Layer for OtelAxumSpanLayer { + type Service = OtelAxumSpanner; + + fn layer(&self, inner: S) -> Self::Service { + OtelAxumSpanner { inner } + } +} + +impl Service> for OtelAxumSpanner +where + S: Service>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = Instrumented; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let parent_context = opentelemetry::global::get_text_map_propagator(|propagator| { + propagator.extract(&opentelemetry_http::HeaderExtractor(req.headers())) + }); + + let method = req.method().to_string(); + let uri = req.uri().clone(); + let route = req + .extensions() + .get::() + .map(|r| r.as_str()) + .unwrap_or_else(|| uri.path()); + let name = format!("{method} {route}"); + let name = name.trim(); + + let span = info_span!( + "Http Request", + "otel.name" = name, + "otel.target" = name, + "otel.kind" = "server", + "http.request.method" = method, + "url.path" = uri.path(), + "url.scheme" = uri.scheme_str().unwrap_or(""), + "http.route" = route, + "http.response.status_code" = tracing::field::Empty, + ); + span.set_parent(parent_context); + + self.inner.call(req).instrument(span) + } +}