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
18pub 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
227pub 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 pub fn new(meter: impl Into<Metrics>) -> Self {
255 meter.into()
256 }
257
258 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 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 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 pub fn with_duration_histogram_boundaries(mut self, boundaries: Vec<f64>) -> Self {
315 self.histograms.set_duration_boundaries(boundaries);
316 self
317 }
318
319 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 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}