Skip to main content

rama_http/layer/
opentelemetry.rs

1//! Http OpenTelemetry [`Layer`] Support for Rama.
2//!
3//! [`Layer`]: rama_core::Layer
4
5use crate::service::web::response::IntoResponse;
6use crate::{
7    Request, Response, StreamingBody,
8    body::{Frame, SizeHint},
9};
10use pin_project_lite::pin_project;
11use rama_core::bytes::Bytes;
12use rama_core::error::BoxError;
13use rama_core::extensions::ExtensionsRef;
14use rama_core::telemetry::opentelemetry::metrics::UpDownCounter;
15use rama_core::telemetry::opentelemetry::semantic_conventions::metric::{
16    HTTP_SERVER_ACTIVE_REQUESTS, HTTP_SERVER_REQUEST_BODY_SIZE,
17};
18use rama_core::telemetry::opentelemetry::{
19    AttributesFactory, InstrumentationScope, KeyValue, MeterOptions, global,
20    metrics::{Counter, Histogram, Meter},
21    semantic_conventions,
22};
23use rama_core::{Layer, Service};
24use rama_net::{AuthorityInputExt, HttpVersionInputExt, ProtocolInputExt};
25use rama_utils::macros::define_inner_service_accessors;
26use std::sync::atomic::{self, AtomicUsize};
27use std::{borrow::Cow, fmt, sync::Arc};
28use tokio::time::Instant;
29
30// Follows the experimental semantic conventions for HTTP metrics:
31// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md
32
33use semantic_conventions::attribute::{
34    HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, NETWORK_PROTOCOL_VERSION, SERVER_PORT,
35    URL_SCHEME,
36};
37
38const HTTP_SERVER_DURATION: &str = "http.requests.duration";
39const HTTP_SERVER_TOTAL_REQUESTS: &str = "http.requests.total";
40const HTTP_SERVER_TOTAL_FAILURES: &str = "http.failures.total";
41const HTTP_SERVER_TOTAL_RESPONSES: &str = "http.responses.total";
42
43const HTTP_REQUEST_HOST: &str = "http.request.host";
44
45/// Records http server metrics
46///
47/// See the [spec] for details.
48///
49/// [spec]: https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-metrics.md#http-server
50#[derive(Clone, Debug)]
51struct Metrics {
52    http_server_duration: Histogram<f64>,
53    http_server_total_requests: Counter<u64>,
54    http_server_total_responses: Counter<u64>,
55    http_server_total_failures: Counter<u64>,
56    http_server_active_requests: UpDownCounter<i64>,
57    http_server_request_body_size: Histogram<u64>,
58}
59
60impl Metrics {
61    /// Create a new [`RequestMetrics`]
62    #[must_use]
63    fn new(meter: &Meter, prefix: Option<&str>) -> Self {
64        let http_server_duration = meter
65            .f64_histogram(match &prefix {
66                Some(prefix) => Cow::Owned(format!("{prefix}.{HTTP_SERVER_DURATION}")),
67                None => Cow::Borrowed(HTTP_SERVER_DURATION),
68            })
69            .with_description("Measures the duration of inbound HTTP requests.")
70            .with_unit("s")
71            .build();
72
73        let http_server_total_requests = meter
74            .u64_counter(match &prefix {
75                Some(prefix) => Cow::Owned(format!("{prefix}.{HTTP_SERVER_TOTAL_REQUESTS}")),
76                None => Cow::Borrowed(HTTP_SERVER_TOTAL_REQUESTS),
77            })
78            .with_description("Measures the total number of HTTP requests have been seen.")
79            .build();
80
81        let http_server_total_responses = meter
82            .u64_counter(match &prefix {
83                Some(prefix) => Cow::Owned(format!("{prefix}.{HTTP_SERVER_TOTAL_RESPONSES}")),
84                None => Cow::Borrowed(HTTP_SERVER_TOTAL_RESPONSES),
85            })
86            .with_description("Measures the total number of HTTP responses have been seen.")
87            .build();
88
89        let http_server_total_failures = meter
90            .u64_counter(match &prefix {
91                Some(prefix) => Cow::Owned(format!("{prefix}.{HTTP_SERVER_TOTAL_FAILURES}")),
92                None => Cow::Borrowed(HTTP_SERVER_TOTAL_FAILURES),
93            })
94            .with_description(
95                "Measures the total number of failed HTTP requests that have been seen.",
96            )
97            .build();
98
99        let http_server_active_requests = meter
100            .i64_up_down_counter(match &prefix {
101                Some(prefix) => Cow::Owned(format!("{prefix}.{HTTP_SERVER_ACTIVE_REQUESTS}")),
102                None => Cow::Borrowed(HTTP_SERVER_ACTIVE_REQUESTS),
103            })
104            .with_description("Measures the number of active HTTP server requests.")
105            .build();
106
107        let http_server_request_body_size = meter
108            .u64_histogram(match &prefix {
109                Some(prefix) => Cow::Owned(format!("{prefix}.{HTTP_SERVER_REQUEST_BODY_SIZE}")),
110                None => Cow::Borrowed(HTTP_SERVER_REQUEST_BODY_SIZE),
111            })
112            .with_description("Measures the HTTP request body size.")
113            .with_unit("B")
114            .build();
115
116        Self {
117            http_server_total_requests,
118            http_server_total_responses,
119            http_server_total_failures,
120            http_server_duration,
121            http_server_active_requests,
122            http_server_request_body_size,
123        }
124    }
125}
126
127/// A layer that records http server metrics using OpenTelemetry.
128#[derive(Debug, Clone)]
129pub struct RequestMetricsLayer<F = ()> {
130    metrics: Arc<Metrics>,
131    base_attributes: Vec<KeyValue>,
132    attributes_factory: F,
133}
134
135impl RequestMetricsLayer<()> {
136    /// Create a new [`RequestMetricsLayer`] using the global [`Meter`] provider,
137    /// with the default name and version.
138    #[must_use]
139    pub fn new() -> Self {
140        Self::custom(MeterOptions::default())
141    }
142
143    /// Create a new [`RequestMetricsLayer`] using the global [`Meter`] provider,
144    /// with a custom name and version.
145    #[must_use]
146    pub fn custom(opts: MeterOptions) -> Self {
147        let attributes = opts.attributes.unwrap_or_default();
148        let meter = get_versioned_meter();
149        let metrics = Metrics::new(&meter, opts.metric_prefix.as_deref());
150
151        Self {
152            metrics: Arc::new(metrics),
153            base_attributes: attributes,
154            attributes_factory: (),
155        }
156    }
157
158    /// Attach an [`AttributesFactory`] to this [`RequestMetricsLayer`], allowing
159    /// you to inject custom attributes.
160    pub fn with_attributes<F>(self, attributes: F) -> RequestMetricsLayer<F> {
161        RequestMetricsLayer {
162            metrics: self.metrics,
163            base_attributes: self.base_attributes,
164            attributes_factory: attributes,
165        }
166    }
167}
168
169impl Default for RequestMetricsLayer {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175fn get_versioned_meter() -> Meter {
176    global::meter_with_scope(
177        InstrumentationScope::builder(const_format::formatcp!(
178            "{}-network-http",
179            rama_utils::info::NAME
180        ))
181        .with_version(rama_utils::info::VERSION)
182        .with_schema_url(semantic_conventions::SCHEMA_URL)
183        .build(),
184    )
185}
186
187impl<S, F: Clone> Layer<S> for RequestMetricsLayer<F> {
188    type Service = RequestMetricsService<S, F>;
189
190    fn layer(&self, inner: S) -> Self::Service {
191        RequestMetricsService {
192            inner,
193            metrics: self.metrics.clone(),
194            base_attributes: self.base_attributes.clone(),
195            attributes_factory: self.attributes_factory.clone(),
196        }
197    }
198
199    fn into_layer(self, inner: S) -> Self::Service {
200        RequestMetricsService {
201            inner,
202            metrics: self.metrics,
203            base_attributes: self.base_attributes,
204            attributes_factory: self.attributes_factory,
205        }
206    }
207}
208
209/// A [`Service`] that records HTTP server metrics using OpenTelemetry.
210#[derive(Debug, Clone)]
211pub struct RequestMetricsService<S, F = ()> {
212    inner: S,
213    metrics: Arc<Metrics>,
214    base_attributes: Vec<KeyValue>,
215    attributes_factory: F,
216}
217
218impl<S> RequestMetricsService<S, ()> {
219    /// Create a new [`RequestMetricsService`].
220    pub fn new(inner: S) -> Self {
221        RequestMetricsLayer::new().into_layer(inner)
222    }
223
224    define_inner_service_accessors!();
225}
226
227impl<S, F> RequestMetricsService<S, F> {
228    fn compute_attributes<Body>(&self, req: &Request<Body>) -> Vec<KeyValue>
229    where
230        F: AttributesFactory,
231    {
232        let mut attributes = self
233            .attributes_factory
234            .attributes(5 + self.base_attributes.len(), req.extensions());
235        attributes.extend(self.base_attributes.iter().cloned());
236
237        // server info
238        let authority = req.authority();
239        let protocol = req.protocol();
240        let http_version = req.http_version();
241
242        if let Some(authority) = authority.as_ref() {
243            attributes.push(KeyValue::new(HTTP_REQUEST_HOST, authority.host.to_string()));
244            if let Some(port) = authority.port.as_u16() {
245                attributes.push(KeyValue::new(SERVER_PORT, port as i64));
246            }
247        }
248
249        // Request Info
250        if let Some(protocol) = protocol.as_ref() {
251            attributes.push(KeyValue::new(URL_SCHEME, protocol.to_string()));
252        }
253
254        attributes.push(KeyValue::new(HTTP_REQUEST_METHOD, req.method().to_string()));
255        if let Some(http_version) = http_version.map(|v| match v {
256            rama_http_types::Version::HTTP_09 => "0.9",
257            rama_http_types::Version::HTTP_10 => "1.0",
258            rama_http_types::Version::HTTP_11 => "1.1",
259            rama_http_types::Version::HTTP_2 => "2",
260            rama_http_types::Version::HTTP_3 => "3",
261        }) {
262            attributes.push(KeyValue::new(NETWORK_PROTOCOL_VERSION, http_version));
263        }
264
265        attributes
266    }
267}
268
269impl<S, F, Body> Service<Request<Body>> for RequestMetricsService<S, F>
270where
271    S: Service<Request, Output: IntoResponse>,
272    F: AttributesFactory,
273    Body: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
274{
275    type Output = Response;
276    type Error = S::Error;
277
278    async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
279        let mut attributes: Vec<KeyValue> = self.compute_attributes(&req);
280
281        self.metrics.http_server_total_requests.add(1, &attributes);
282        self.metrics.http_server_active_requests.add(1, &attributes);
283
284        // used to compute the duration of the request
285        let timer = Instant::now();
286
287        let polled_body_size: Arc<AtomicUsize> = Default::default();
288        let req = req.map(|body| {
289            crate::Body::new(BodyTracker {
290                inner: body,
291                polled_size: polled_body_size.clone(),
292            })
293        });
294
295        let result = self.inner.serve(req).await;
296
297        self.metrics
298            .http_server_active_requests
299            .add(-1, &attributes);
300        self.metrics.http_server_request_body_size.record(
301            polled_body_size.load(atomic::Ordering::Relaxed) as u64,
302            &attributes,
303        );
304
305        match result {
306            Ok(res) => {
307                let res = res.into_response();
308
309                attributes.push(KeyValue::new(
310                    HTTP_RESPONSE_STATUS_CODE,
311                    res.status().as_u16() as i64,
312                ));
313
314                self.metrics.http_server_total_responses.add(1, &attributes);
315                self.metrics
316                    .http_server_duration
317                    .record(timer.elapsed().as_secs_f64(), &attributes);
318
319                Ok(res)
320            }
321            Err(err) => {
322                self.metrics.http_server_total_failures.add(1, &attributes);
323
324                Err(err)
325            }
326        }
327    }
328}
329
330pin_project! {
331    /// Wrapper around the incoming Request body used
332    /// to track the request body size.
333    pub struct BodyTracker<B> {
334        #[pin]
335        inner: B,
336        polled_size: Arc<AtomicUsize>,
337    }
338}
339
340impl<B: fmt::Debug> fmt::Debug for BodyTracker<B> {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        f.debug_struct("BodyTracker")
343            .field("inner", &self.inner)
344            .field("polled_size", &self.polled_size)
345            .finish()
346    }
347}
348
349impl<B> StreamingBody for BodyTracker<B>
350where
351    B: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
352{
353    type Data = B::Data;
354    type Error = B::Error;
355
356    fn poll_frame(
357        self: std::pin::Pin<&mut Self>,
358        cx: &mut std::task::Context<'_>,
359    ) -> std::task::Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
360        let this = self.project();
361        match this.inner.poll_frame(cx) {
362            std::task::Poll::Ready(opt) => {
363                if let Some(Ok(frame)) = &opt
364                    && let Some(data) = frame.data_ref()
365                {
366                    this.polled_size
367                        .fetch_add(data.len(), atomic::Ordering::Relaxed);
368                }
369                std::task::Poll::Ready(opt)
370            }
371            std::task::Poll::Pending => std::task::Poll::Pending,
372        }
373    }
374
375    fn is_end_stream(&self) -> bool {
376        self.inner.is_end_stream()
377    }
378
379    fn size_hint(&self) -> SizeHint {
380        self.inner.size_hint()
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use rama_core::extensions::Extensions;
387
388    use super::*;
389
390    #[test]
391    fn test_default_svc_compute_attributes_default() {
392        let svc = RequestMetricsService::new(());
393        let req = Request::builder()
394            .uri("http://www.example.com")
395            .body(())
396            .unwrap();
397
398        let attributes = svc.compute_attributes(&req);
399
400        assert!(
401            attributes
402                .iter()
403                .any(|attr| attr.key.as_str() == HTTP_REQUEST_HOST)
404        );
405    }
406
407    #[test]
408    fn test_custom_svc_compute_attributes_default() {
409        let svc = RequestMetricsLayer::custom(MeterOptions {
410            metric_prefix: Some("foo".to_owned()),
411            ..Default::default()
412        })
413        .into_layer(());
414        let req = Request::builder()
415            .uri("http://www.example.com")
416            .body(())
417            .unwrap();
418
419        let attributes = svc.compute_attributes(&req);
420
421        assert!(
422            attributes
423                .iter()
424                .any(|attr| attr.key.as_str() == HTTP_REQUEST_HOST)
425        );
426    }
427
428    #[test]
429    fn test_custom_svc_compute_attributes_attributes_vec() {
430        let svc = RequestMetricsLayer::custom(MeterOptions {
431            metric_prefix: Some("foo".to_owned()),
432            ..Default::default()
433        })
434        .with_attributes(vec![KeyValue::new("test", "attribute_fn")])
435        .into_layer(());
436        let req = Request::builder()
437            .uri("http://www.example.com")
438            .body(())
439            .unwrap();
440
441        let attributes = svc.compute_attributes(&req);
442        assert!(
443            attributes
444                .iter()
445                .any(|attr| attr.key.as_str() == HTTP_REQUEST_HOST)
446        );
447        assert!(
448            attributes
449                .iter()
450                .any(|attr| attr.key.as_str() == "test" && attr.value.as_str() == "attribute_fn")
451        );
452    }
453
454    #[test]
455    fn test_custom_svc_compute_attributes_attribute_fn() {
456        let svc = RequestMetricsLayer::custom(MeterOptions {
457            metric_prefix: Some("foo".to_owned()),
458            ..Default::default()
459        })
460        .with_attributes(|size_hint: usize, _extensions: &Extensions| {
461            let mut attributes = Vec::with_capacity(size_hint + 1);
462            attributes.push(KeyValue::new("test", "attribute_fn"));
463            attributes
464        })
465        .into_layer(());
466        let req = Request::builder()
467            .uri("http://www.example.com")
468            .body(())
469            .unwrap();
470
471        let attributes = svc.compute_attributes(&req);
472
473        assert!(
474            attributes
475                .iter()
476                .any(|attr| attr.key.as_str() == HTTP_REQUEST_HOST)
477        );
478        assert!(
479            attributes
480                .iter()
481                .any(|attr| attr.key.as_str() == "test" && attr.value.as_str() == "attribute_fn")
482        );
483    }
484}