1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
use std::{
convert::Infallible,
pin::Pin,
task::{Context, Poll},
time::Instant,
};
use axum::{extract::Request, http::Method, response::Response};
use futures_util::Future;
use synd_o11y::metric;
use tower::{Layer, Service};
use crate::config;
#[derive(Clone)]
pub struct RequestMetricsLayer {}
impl RequestMetricsLayer {
pub fn new() -> Self {
Self {}
}
}
impl<S> Layer<S> for RequestMetricsLayer {
type Service = RequestMetricsService<S>;
fn layer(&self, inner: S) -> Self::Service {
RequestMetricsService { inner }
}
}
#[derive(Clone)]
pub struct RequestMetricsService<S> {
inner: S,
}
impl<S> Service<Request> for RequestMetricsService<S>
where
S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + 'static,
S::Future: Send,
{
type Response = Response;
type Error = Infallible;
type Future =
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
let start = Instant::now();
let path = req.uri().path().to_owned();
let method = req.method().clone();
let mut this = self.clone();
Box::pin(async move {
let response = this.inner.call(req).await.unwrap();
let status = response.status().as_u16();
// Ignore health check
// Metrics related to health checks is ignored as the are collected
// by the service performing the health check
if path != config::serve::HEALTH_CHECK_PATH {
// https://opentelemetry.io/docs/specs/semconv/http/http-metrics/
// Considiering the case of not found(404), recording the path as
// an attribute leads to an inability to control cardinality.
// Therefore, the path is not recorded.
metric!(
monotonic_counter.http.server.request = 1,
http.response.status.code = status
);
}
// instrument graphql latency
if path == "/graphql" && method == Method::POST {
// f64 is matter
// The type of the field that MetricsVisitor visits when on_event() is called is pre defined.
// If u128 which is returned from elapsed() is used, it will not be visited, resulting in no metrics recorded.
// Spec say "When instruments are measuring durations, seconds SHOULD be used"
// https://opentelemetry.io/docs/specs/semconv/general/metrics/#instrument-units
let elapsed: f64 = start.elapsed().as_secs_f64();
// is there any semantic conventions?
metric!(histogram.graphql.duration = elapsed);
}
Ok(response)
})
}
}