#![allow(clippy::module_name_repetitions, clippy::unreadable_literal)]
pub const AXUM_HTTP_REQUESTS_PENDING: &str = match option_env!("AXUM_HTTP_REQUESTS_PENDING") {
Some(n) => n,
None => "axum_http_requests_pending",
};
pub const AXUM_HTTP_REQUESTS_DURATION_SECONDS: &str =
match option_env!("AXUM_HTTP_REQUESTS_DURATION_SECONDS") {
Some(n) => n,
None => "axum_http_requests_duration_seconds",
};
pub const AXUM_HTTP_REQUESTS_TOTAL: &str = match option_env!("AXUM_HTTP_REQUESTS_TOTAL") {
Some(n) => n,
None => "axum_http_requests_total",
};
use std::borrow::Cow;
use std::collections::HashMap;
use std::time::Instant;
mod builder;
pub mod lifecycle;
mod utils;
use axum::extract::MatchedPath;
pub use builder::EndpointLabel;
pub use builder::PrometheusMetricLayerBuilder;
use builder::{LayerOnly, Paired};
use lifecycle::layer::LifeCycleLayer;
use lifecycle::{service::LifeCycle, Callbacks};
use metrics::{decrement_gauge, histogram, increment_counter, increment_gauge};
use tower::Layer;
use tower_http::classify::{ClassifiedResponse, SharedClassifier, StatusInRangeAsFailures};
pub use utils::SECONDS_DURATION_BUCKETS;
use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};
pub use metrics;
pub use metrics_exporter_prometheus;
#[derive(Clone, Default)]
pub struct Traffic<'a> {
ignore_patterns: matchit::Router<()>,
group_patterns: HashMap<&'a str, matchit::Router<()>>,
endpoint_label: EndpointLabel,
}
impl<'a> Traffic<'a> {
pub(crate) fn new() -> Self {
Traffic::default()
}
pub(crate) fn with_ignore_pattern(&mut self, ignore_pattern: &'a str) {
self.ignore_patterns
.insert(ignore_pattern, ())
.expect("good route specs");
}
pub(crate) fn with_ignore_patterns(&mut self, ignore_patterns: &'a [&'a str]) {
for pattern in ignore_patterns {
self.with_ignore_pattern(pattern);
}
}
pub(crate) fn with_group_patterns_as(&mut self, group_pattern: &'a str, patterns: &'a [&str]) {
self.group_patterns
.entry(group_pattern)
.and_modify(|router| {
for pattern in patterns {
router.insert(*pattern, ()).expect("good route specs");
}
})
.or_insert_with(|| {
let mut inner_router = matchit::Router::new();
for pattern in patterns {
inner_router.insert(*pattern, ()).expect("good route specs");
}
inner_router
});
}
pub(crate) fn ignores(&self, path: &str) -> bool {
self.ignore_patterns.at(path).is_ok()
}
pub(crate) fn apply_group_pattern(&self, path: &'a str) -> &'a str {
self.group_patterns
.iter()
.find_map(|(&group, router)| router.at(path).ok().and(Some(group)))
.unwrap_or(path)
}
pub(crate) fn with_endpoint_label_type(&mut self, endpoint_label: EndpointLabel) {
self.endpoint_label = endpoint_label;
}
}
#[derive(Debug, Clone)]
pub struct MetricsData {
pub endpoint: String,
pub start: Instant,
pub method: &'static str,
}
impl<'a, FailureClass> Callbacks<FailureClass> for Traffic<'a> {
type Data = Option<MetricsData>;
fn prepare<B>(&mut self, request: &http::Request<B>) -> Self::Data {
let now = std::time::Instant::now();
let exact_endpoint = request.uri().path();
if self.ignores(exact_endpoint) {
return None;
}
let endpoint = match self.endpoint_label {
EndpointLabel::Exact => Cow::from(exact_endpoint),
EndpointLabel::MatchedPath => Cow::from(
request
.extensions()
.get::<MatchedPath>()
.map_or(exact_endpoint, MatchedPath::as_str),
),
EndpointLabel::MatchedPathWithFallbackFn(fallback_fn) => {
if let Some(mp) = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str)
{
Cow::from(mp)
} else {
Cow::from(fallback_fn(exact_endpoint))
}
}
};
let endpoint = self.apply_group_pattern(&endpoint).to_owned();
let method = utils::as_label(request.method());
let labels = [
("method", method.to_owned()),
("endpoint", endpoint.clone()),
];
increment_counter!(AXUM_HTTP_REQUESTS_TOTAL, &labels);
increment_gauge!(AXUM_HTTP_REQUESTS_PENDING, 1.0, &labels);
Some(MetricsData {
endpoint,
start: now,
method,
})
}
fn on_response<B>(
&mut self,
res: &http::Response<B>,
_cls: ClassifiedResponse<FailureClass, ()>,
data: &mut Self::Data,
) {
if let Some(data) = data {
let duration_seconds = data.start.elapsed().as_secs_f64();
decrement_gauge!(
AXUM_HTTP_REQUESTS_PENDING,
1.0,
&[
("method", data.method.to_string()),
("endpoint", data.endpoint.to_string())
]
);
histogram!(
AXUM_HTTP_REQUESTS_DURATION_SECONDS,
duration_seconds,
&[
("method", data.method.to_string()),
("status", res.status().as_u16().to_string()),
("endpoint", data.endpoint.to_string()),
]
);
}
}
}
#[derive(Clone)]
pub struct PrometheusMetricLayer<'a> {
pub(crate) inner_layer: LifeCycleLayer<SharedClassifier<StatusInRangeAsFailures>, Traffic<'a>>,
}
impl<'a> PrometheusMetricLayer<'a> {
pub fn new() -> Self {
let make_classifier =
StatusInRangeAsFailures::new_for_client_and_server_errors().into_make_classifier();
let inner_layer = LifeCycleLayer::new(make_classifier, Traffic::new());
Self { inner_layer }
}
pub(crate) fn from_builder(builder: PrometheusMetricLayerBuilder<'a, LayerOnly>) -> Self {
let make_classifier =
StatusInRangeAsFailures::new_for_client_and_server_errors().into_make_classifier();
let inner_layer = LifeCycleLayer::new(make_classifier, builder.traffic);
Self { inner_layer }
}
pub(crate) fn pair_from_builder(
builder: PrometheusMetricLayerBuilder<'a, Paired>,
) -> (Self, PrometheusHandle) {
let make_classifier =
StatusInRangeAsFailures::new_for_client_and_server_errors().into_make_classifier();
let inner_layer = LifeCycleLayer::new(make_classifier, builder.traffic);
(
Self { inner_layer },
builder
.metric_handle
.unwrap_or_else(Self::make_default_handle),
)
}
pub fn pair() -> (Self, PrometheusHandle) {
(Self::new(), Self::make_default_handle())
}
pub(crate) fn make_default_handle() -> PrometheusHandle {
PrometheusBuilder::new()
.set_buckets_for_metric(
Matcher::Full(AXUM_HTTP_REQUESTS_DURATION_SECONDS.to_string()),
SECONDS_DURATION_BUCKETS,
)
.unwrap()
.install_recorder()
.unwrap()
}
}
impl<'a> Default for PrometheusMetricLayer<'a> {
fn default() -> Self {
Self::new()
}
}
impl<'a, S> Layer<S> for PrometheusMetricLayer<'a> {
type Service = LifeCycle<S, SharedClassifier<StatusInRangeAsFailures>, Traffic<'a>>;
fn layer(&self, inner: S) -> Self::Service {
self.inner_layer.layer(inner)
}
}