Skip to main content

trillium_opentelemetry/
metrics.rs

1use opentelemetry::{
2    KeyValue, global,
3    metrics::{Histogram, Meter},
4};
5use opentelemetry_semantic_conventions as semconv;
6use std::{
7    borrow::Cow,
8    fmt::{self, Debug, Formatter},
9    sync::Arc,
10    time::Instant,
11};
12use trillium::{Conn, Handler, Info, KnownHeaderName, Status, Transport, log};
13
14type StringExtractionFn = dyn Fn(&Conn) -> Option<Cow<'static, str>> + Send + Sync + 'static;
15type StringAndPortExtractionFn =
16    dyn Fn(&Conn) -> Option<(Cow<'static, str>, u16)> + Send + Sync + 'static;
17
18/// Trillium handler that instruments http.server.request.duration, http.server.request.body.size,
19/// and http.server.response.body.size as per [semantic conventions for http][http-metrics].
20///
21/// [http-metrics]: https://opentelemetry.io/docs/specs/semconv/http/http-metrics/
22pub struct Metrics {
23    pub(crate) route: Option<Arc<StringExtractionFn>>,
24    pub(crate) error_type: Option<Arc<StringExtractionFn>>,
25    pub(crate) server_address_and_port: Option<Arc<StringAndPortExtractionFn>>,
26    pub(crate) histograms: Histograms,
27}
28
29#[derive(Clone, Debug)]
30pub(crate) enum Histograms {
31    Uninitialized {
32        meter: Meter,
33        duration_histogram_boundaries: Option<Vec<f64>>,
34        request_size_histogram_boundaries: Option<Vec<f64>>,
35        response_size_histogram_boundaries: Option<Vec<f64>>,
36    },
37    Initialized {
38        duration_histogram: Histogram<f64>,
39        request_size_histogram: Histogram<u64>,
40        response_size_histogram: Histogram<u64>,
41    },
42}
43
44impl Histograms {
45    fn init(&mut self) {
46        match self {
47            Self::Uninitialized {
48                meter,
49                duration_histogram_boundaries,
50                request_size_histogram_boundaries,
51                response_size_histogram_boundaries,
52            } => {
53                let mut duration_histogram_builder = meter
54                    .f64_histogram(semconv::metric::HTTP_SERVER_REQUEST_DURATION)
55                    .with_description("Measures the duration of inbound HTTP requests.")
56                    .with_unit("s");
57                duration_histogram_builder.boundaries = duration_histogram_boundaries.take();
58
59                let mut request_size_histogram_builder = meter
60                    .u64_histogram(semconv::metric::HTTP_SERVER_REQUEST_BODY_SIZE)
61                    .with_description("Measures the size of HTTP request messages (compressed).")
62                    .with_unit("By");
63                request_size_histogram_builder.boundaries =
64                    request_size_histogram_boundaries.take();
65
66                let mut response_size_histogram_builder = meter
67                    .u64_histogram(semconv::metric::HTTP_SERVER_RESPONSE_BODY_SIZE)
68                    .with_description("Measures the size of HTTP response messages (compressed).")
69                    .with_unit("By");
70                response_size_histogram_builder.boundaries =
71                    response_size_histogram_boundaries.take();
72
73                *self = Self::Initialized {
74                    duration_histogram: duration_histogram_builder.build(),
75                    request_size_histogram: request_size_histogram_builder.build(),
76                    response_size_histogram: response_size_histogram_builder.build(),
77                }
78            }
79
80            Self::Initialized { .. } => {
81                log::warn!("Attempted to initialize the Metrics handler twice");
82            }
83        }
84    }
85
86    fn set_request_size_boundaries(&mut self, boundaries: Vec<f64>) {
87        match self {
88            Self::Uninitialized {
89                request_size_histogram_boundaries,
90                ..
91            } => {
92                *request_size_histogram_boundaries = Some(boundaries);
93            }
94
95            Self::Initialized { .. } => {
96                log::warn!(
97                    "Attempted to set histogram boundaries on a Metrics handler that was already initialized"
98                );
99            }
100        }
101    }
102
103    fn set_response_size_boundaries(&mut self, boundaries: Vec<f64>) {
104        match self {
105            Self::Uninitialized {
106                response_size_histogram_boundaries,
107                ..
108            } => {
109                *response_size_histogram_boundaries = Some(boundaries);
110            }
111
112            Self::Initialized { .. } => {
113                log::warn!(
114                    "Attempted to set histogram boundaries on a Metrics handler that was already initialized"
115                );
116            }
117        }
118    }
119
120    fn set_duration_boundaries(&mut self, boundaries: Vec<f64>) {
121        match self {
122            Self::Uninitialized {
123                duration_histogram_boundaries,
124                ..
125            } => {
126                *duration_histogram_boundaries = Some(boundaries);
127            }
128            Self::Initialized { .. } => {
129                log::warn!(
130                    "Attempted to set histogram boundaries on a Metrics handler that was already initialized"
131                );
132            }
133        }
134    }
135
136    fn record_duration(&self, duration_s: f64, attributes: &[KeyValue]) {
137        match self {
138            Self::Initialized {
139                duration_histogram, ..
140            } => {
141                duration_histogram.record(duration_s, attributes);
142            }
143            Self::Uninitialized { .. } => {
144                log::error!("Attempted to record a duration on an uninitialized Metrics handler");
145            }
146        }
147    }
148    fn record_response_len(&self, response_len: u64, attributes: &[KeyValue]) {
149        match self {
150            Self::Initialized {
151                response_size_histogram,
152                ..
153            } => {
154                response_size_histogram.record(response_len, attributes);
155            }
156
157            Self::Uninitialized { .. } => {
158                log::error!(
159                    "Attempted to record a response length on an uninitialized Metrics handler"
160                );
161            }
162        }
163    }
164
165    fn record_request_len(&self, request_len: u64, attributes: &[KeyValue]) {
166        match self {
167            Self::Initialized {
168                request_size_histogram,
169                ..
170            } => {
171                request_size_histogram.record(request_len, attributes);
172            }
173
174            Self::Uninitialized { .. } => {
175                log::error!(
176                    "Attempted to record a request length on an uninitialized Metrics handler"
177                );
178            }
179        }
180    }
181}
182
183impl From<Histograms> for Metrics {
184    fn from(value: Histograms) -> Self {
185        Metrics {
186            route: None,
187            error_type: None,
188            server_address_and_port: None,
189            histograms: value,
190        }
191    }
192}
193
194impl From<Meter> for Histograms {
195    fn from(meter: Meter) -> Self {
196        Histograms::Uninitialized {
197            meter,
198            duration_histogram_boundaries: None,
199            request_size_histogram_boundaries: None,
200            response_size_histogram_boundaries: None,
201        }
202    }
203}
204
205impl Debug for Metrics {
206    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
207        f.debug_struct("Metrics")
208            .field(
209                "route",
210                &match self.route {
211                    Some(_) => "Some(..)",
212                    _ => "None",
213                },
214            )
215            .field(
216                "error_type",
217                &match self.error_type {
218                    Some(_) => "Some(..)",
219                    _ => "None",
220                },
221            )
222            .field("histograms", &self.histograms)
223            .finish()
224    }
225}
226
227/// Constructs a [`Metrics`] handler from a `&'static str`, [`Meter`], or [`&Meter`][Meter].
228///
229/// Alias for [`Metrics::new`] and [`Metrics::from`]
230pub fn metrics(meter: impl Into<Metrics>) -> Metrics {
231    meter.into()
232}
233
234impl From<&'static str> for Metrics {
235    fn from(value: &'static str) -> Self {
236        global::meter(value).into()
237    }
238}
239
240impl From<Meter> for Metrics {
241    fn from(value: Meter) -> Self {
242        Histograms::from(value).into()
243    }
244}
245
246impl From<&Meter> for Metrics {
247    fn from(meter: &Meter) -> Self {
248        meter.clone().into()
249    }
250}
251
252impl Metrics {
253    /// Constructs a new [`Metrics`] handler from a `&'static str`, [`&Meter`][Meter] or [`Meter`]
254    pub fn new(meter: impl Into<Metrics>) -> Self {
255        meter.into()
256    }
257
258    /// provides a route specification to the metrics collector.
259    ///
260    /// in order to avoid forcing anyone to use a particular router, this is provided as a
261    /// configuration hook.
262    ///
263    /// for use with [`trillium-router`](https://docs.trillium.rs/trillium_router/index.html),
264    /// ```
265    /// use trillium_router::RouterConnExt;
266    /// trillium_opentelemetry::Metrics::new(&opentelemetry::global::meter("example"))
267    ///     .with_route(|conn| conn.route().map(|r| r.to_string().into()));
268    /// ```
269    pub fn with_route<F>(mut self, route: F) -> Self
270    where
271        F: Fn(&Conn) -> Option<Cow<'static, str>> + Send + Sync + 'static,
272    {
273        self.route = Some(Arc::new(route));
274        self
275    }
276
277    /// Provides an optional low-cardinality error type specification to the metrics collector.
278    ///
279    /// The implementation of this is application specific, but will often look like checking the
280    /// [`Conn::state`] for an error enum and mapping that to a low-cardinality `&'static str`.
281    pub fn with_error_type<F>(mut self, error_type: F) -> Self
282    where
283        F: Fn(&Conn) -> Option<Cow<'static, str>> + Send + Sync + 'static,
284    {
285        self.error_type = Some(Arc::new(error_type));
286        self
287    }
288
289    /// Provides a callback for `server.address` and `server.port` attributes to the metrics
290    /// collector.
291    ///
292    /// These should be set based on request headers according to the [OpenTelemetry HTTP semantic
293    /// conventions][semconv-server-address-port].
294    ///
295    /// It is not recommended to enable this when the server is exposed to clients outside of your
296    /// control, as request headers could arbitrarily increase the cardinality of these attributes.
297    ///
298    /// [semconv-server-address-port]:
299    ///     https://opentelemetry.io/docs/specs/semconv/http/http-spans/#setting-serveraddress-and-serverport-attributes
300    pub fn with_server_address_and_port<F>(mut self, server_address_and_port: F) -> Self
301    where
302        F: Fn(&Conn) -> Option<(Cow<'static, str>, u16)> + Send + Sync + 'static,
303    {
304        self.server_address_and_port = Some(Arc::new(server_address_and_port));
305        self
306    }
307
308    /// Sets histogram boundaries for request durations (in seconds).
309    ///
310    /// This sets the histogram bucket boundaries for the [`http.server.request.duration`][semconv]
311    /// metric.
312    ///
313    /// [semconv]: https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration
314    pub fn with_duration_histogram_boundaries(mut self, boundaries: Vec<f64>) -> Self {
315        self.histograms.set_duration_boundaries(boundaries);
316        self
317    }
318
319    /// Sets histogram boundaries for request sizes (in bytes).
320    ///
321    /// This sets the histogram bucket boundaries for the [`http.server.request.body.size`][semconv]
322    /// metric.
323    ///
324    /// [semconv]: https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestbodysize
325    pub fn with_request_size_histogram_boundaries(mut self, boundaries: Vec<f64>) -> Self {
326        self.histograms.set_request_size_boundaries(boundaries);
327        self
328    }
329
330    /// Sets histogram boundaries for response sizes (in bytes).
331    ///
332    /// This sets the histogram bucket boundaries for the [`http.server.response.body.size`][semconv]
333    /// metric.
334    ///
335    /// [semconv]: https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverresponsebodysize
336    pub fn with_response_size_histogram_boundaries(mut self, boundaries: Vec<f64>) -> Self {
337        self.histograms.set_response_size_boundaries(boundaries);
338        self
339    }
340}
341
342struct MetricsWasRun;
343
344impl Handler for Metrics {
345    async fn run(&self, conn: Conn) -> Conn {
346        conn.with_state(MetricsWasRun)
347    }
348
349    async fn init(&mut self, _: &mut Info) {
350        self.histograms.init();
351    }
352
353    async fn before_send(&self, mut conn: Conn) -> Conn {
354        if conn.state::<MetricsWasRun>().is_none() {
355            return conn;
356        }
357
358        let Metrics {
359            route,
360            error_type,
361            server_address_and_port,
362            histograms,
363        } = self;
364        let error_type = error_type.as_ref().and_then(|et| et(&conn)).or_else(|| {
365            let status = conn.status().unwrap_or(Status::NotFound);
366            if status.is_server_error() {
367                Some((status as u16).to_string().into())
368            } else {
369                None
370            }
371        });
372        let status: i64 = (conn.status().unwrap_or(Status::NotFound) as u16).into();
373        let route = route.as_ref().and_then(|r| r(&conn));
374        let start_time = conn.start_time();
375        let method = conn.method().as_str();
376        let request_len = conn
377            .request_headers()
378            .get_str(KnownHeaderName::ContentLength)
379            .and_then(|src| src.parse::<u64>().ok());
380        let response_len = conn.response_len();
381        let scheme = if conn.is_secure() { "https" } else { "http" };
382        let version = conn.http_version().as_str().strip_prefix("HTTP/").unwrap();
383        let server_address_and_port = server_address_and_port.as_ref().and_then(|f| f(&conn));
384
385        let mut attributes = vec![
386            KeyValue::new(semconv::attribute::HTTP_REQUEST_METHOD, method),
387            KeyValue::new(semconv::attribute::HTTP_RESPONSE_STATUS_CODE, status),
388            KeyValue::new(semconv::attribute::NETWORK_PROTOCOL_NAME, "http"),
389            KeyValue::new(semconv::attribute::URL_SCHEME, scheme),
390            KeyValue::new(semconv::attribute::NETWORK_PROTOCOL_VERSION, version),
391        ];
392
393        if let Some(error_type) = error_type {
394            attributes.push(KeyValue::new("error.type", error_type));
395        }
396
397        if let Some(route) = route {
398            attributes.push(KeyValue::new(semconv::attribute::HTTP_ROUTE, route))
399        };
400
401        if let Some((address, port)) = server_address_and_port {
402            attributes.push(KeyValue::new(semconv::attribute::SERVER_ADDRESS, address));
403            attributes.push(KeyValue::new(
404                semconv::attribute::SERVER_PORT,
405                i64::from(port),
406            ));
407        }
408
409        let histograms = histograms.clone();
410        let inner: &mut trillium_http::Conn<Box<dyn Transport>> = conn.as_mut();
411        inner.after_send(move |_| {
412            let duration_s = (Instant::now() - start_time).as_secs_f64();
413
414            histograms.record_duration(duration_s, &attributes);
415
416            if let Some(response_len) = response_len {
417                histograms.record_response_len(response_len, &attributes);
418            }
419
420            if let Some(request_len) = request_len {
421                histograms.record_request_len(request_len, &attributes);
422            }
423        });
424
425        conn
426    }
427}