Skip to content

Commit e34c154

Browse files
authored
feat: otel axum spanner (#87)
1 parent 271b621 commit e34c154

File tree

3 files changed

+78
-5
lines changed

3 files changed

+78
-5
lines changed

Cargo.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ name = "init4-bin-base"
44
description = "Internal utilities for binaries produced by the init4 team"
55
keywords = ["init4", "bin", "base"]
66

7-
version = "0.15.0"
7+
version = "0.15.1"
88
edition = "2021"
99
rust-version = "1.85"
1010
authors = ["init4", "James Prestwich", "evalir"]
@@ -30,6 +30,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
3030
# OTLP
3131
opentelemetry_sdk = "0.30.0"
3232
opentelemetry = "0.30.0"
33+
opentelemetry-http = "0.30.0"
3334
opentelemetry-otlp = "0.30.0"
3435
opentelemetry-semantic-conventions = { version = "0.30.0", features = ["semconv_experimental"] }
3536
tracing-opentelemetry = "0.31.0"
@@ -47,12 +48,12 @@ oauth2 = { version = "5.0.0", optional = true }
4748
tokio = { version = "1.36.0", optional = true }
4849

4950
# Other
50-
thiserror = "2.0.11"
51+
axum = "0.8.1"
5152
serde = { version = "1", features = ["derive"] }
53+
thiserror = "2.0.11"
54+
tower = "0.5.2"
5255
async-trait = { version = "0.1.80", optional = true }
5356
eyre = { version = "0.6.12", optional = true }
54-
axum = { version = "0.8.1", optional = true }
55-
tower = { version = "0.5.2", optional = true }
5657

5758
# AWS
5859
aws-config = { version = "1.1.7", optional = true }
@@ -72,7 +73,7 @@ tokio = { version = "1.43.0", features = ["macros"] }
7273
default = ["alloy", "rustls"]
7374
alloy = ["dep:alloy"]
7475
aws = ["alloy", "alloy?/signer-aws", "dep:async-trait", "dep:aws-config", "dep:aws-sdk-kms"]
75-
perms = ["dep:oauth2", "dep:tokio", "dep:reqwest", "dep:signet-tx-cache", "dep:eyre", "dep:axum", "dep:tower"]
76+
perms = ["dep:oauth2", "dep:tokio", "dep:reqwest", "dep:signet-tx-cache", "dep:eyre"]
7677
rustls = ["dep:rustls", "rustls/aws-lc-rs"]
7778

7879
[[example]]

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ pub mod utils {
3131
/// Prometheus metrics utilities.
3232
pub mod metrics;
3333

34+
/// Axum OpenTelemetry utilities.
35+
pub mod otel_axum;
36+
3437
/// OpenTelemetry utilities.
3538
pub mod otlp;
3639

src/utils/otel_axum.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use axum::extract::{MatchedPath, Request};
2+
use tower::{Layer, Service};
3+
use tracing::{info_span, instrument::Instrumented, Instrument};
4+
use tracing_opentelemetry::OpenTelemetrySpanExt;
5+
6+
/// A [`Layer`] that adds OpenTelemetry spans to Axum requests.
7+
#[derive(Debug, Clone, Copy)]
8+
pub struct OtelAxumSpanLayer;
9+
10+
/// A simple service
11+
#[derive(Debug, Clone)]
12+
pub struct OtelAxumSpanner<S> {
13+
inner: S,
14+
}
15+
16+
impl<S> Layer<S> for OtelAxumSpanLayer {
17+
type Service = OtelAxumSpanner<S>;
18+
19+
fn layer(&self, inner: S) -> Self::Service {
20+
OtelAxumSpanner { inner }
21+
}
22+
}
23+
24+
impl<S, Body> Service<Request<Body>> for OtelAxumSpanner<S>
25+
where
26+
S: Service<Request<Body>>,
27+
{
28+
type Response = S::Response;
29+
type Error = S::Error;
30+
type Future = Instrumented<S::Future>;
31+
32+
fn poll_ready(
33+
&mut self,
34+
cx: &mut std::task::Context<'_>,
35+
) -> std::task::Poll<Result<(), Self::Error>> {
36+
self.inner.poll_ready(cx)
37+
}
38+
39+
fn call(&mut self, req: Request<Body>) -> Self::Future {
40+
let parent_context = opentelemetry::global::get_text_map_propagator(|propagator| {
41+
propagator.extract(&opentelemetry_http::HeaderExtractor(req.headers()))
42+
});
43+
44+
let method = req.method().to_string();
45+
let uri = req.uri().clone();
46+
let route = req
47+
.extensions()
48+
.get::<MatchedPath>()
49+
.map(|r| r.as_str())
50+
.unwrap_or_else(|| uri.path());
51+
let name = format!("{method} {route}");
52+
let name = name.trim();
53+
54+
let span = info_span!(
55+
"Http Request",
56+
"otel.name" = name,
57+
"otel.target" = name,
58+
"otel.kind" = "server",
59+
"http.request.method" = method,
60+
"url.path" = uri.path(),
61+
"url.scheme" = uri.scheme_str().unwrap_or(""),
62+
"http.route" = route,
63+
"http.response.status_code" = tracing::field::Empty,
64+
);
65+
span.set_parent(parent_context);
66+
67+
self.inner.call(req).instrument(span)
68+
}
69+
}

0 commit comments

Comments
 (0)