tide_prometheus/
lib.rs

1#![forbid(future_incompatible, unsafe_code)]
2
3//! Tide middleware for [`prometheus`] with a few default metrics.
4//!
5//! ## Example
6//!
7//! ```rust
8//! # async_std::task::block_on(async {
9//! let mut server = tide::new();
10//!
11//! server.with(tide_prometheus::Prometheus::new("tide"));
12//!
13//! // Optionally serve these metrics on the same server:
14//! server.at("/metrics").get(tide_prometheus::metrics_endpoint);
15//! # });
16//! ```
17//!
18//! ## Metrics
19//!
20//! The `{prefix}` below is the string you put in [`Prometheus::new`].
21//!
22//! * `{prefix}_http_requests` ([`prometheus::IntCounterVec`]) with labels:
23//!   * `method` as the request method.
24//!   * `status` as the response status.
25//!
26//! ## Features
27//!
28//! * `process` will enable the [`prometheus`] `process` feature, recording
29//! various metrics of the process.
30
31pub use prometheus;
32
33use prometheus::Encoder;
34
35/// Tide middleware for [`prometheus`] with a few default metrics.
36#[derive(Clone, Debug)]
37pub struct Prometheus {
38  /// The `{prefix}_http_requests` counter.
39  http_requests: prometheus::IntCounterVec,
40}
41
42impl Prometheus {
43  /// Creates a new Prometheus middleware. This also creates and registers the
44  /// metrics in the [`prometheus::default_registry`].
45  pub fn new(prefix: &str) -> Self {
46    Self {
47      http_requests: Self::http_requests(prefix),
48    }
49  }
50
51  /// Creates, registers and returns the `{prefix}_http_requests` counter.
52  fn http_requests(prefix: &str) -> prometheus::IntCounterVec {
53    let name = format!("{}_http_requests", prefix);
54    let opts = prometheus::Opts::new(name, "Counts http requests");
55    prometheus::register_int_counter_vec!(opts, &["method", "code"]).unwrap()
56  }
57}
58
59#[tide::utils::async_trait]
60impl<State: Clone + Send + Sync + 'static> tide::Middleware<State>
61  for Prometheus
62{
63  async fn handle(
64    &self,
65    request: tide::Request<State>,
66    next: tide::Next<'_, State>,
67  ) -> tide::Result {
68    let method = request.method();
69    let response = next.run(request).await;
70    let status_code = response.status().to_string();
71
72    self
73      .http_requests
74      .with_label_values(&[method.as_ref(), &status_code])
75      .inc();
76
77    Ok(response)
78  }
79}
80
81/// A convencience [`tide::Endpoint`] that gathers the metrics with
82/// [`prometheus::gather`] and then returns them inside the response body as
83/// specified by [the Prometheus docs](https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format).
84///
85/// ## Example
86///
87/// ```rust
88/// # async_std::task::block_on(async {
89/// let mut server = tide::new();
90/// server.with(tide_prometheus::Prometheus::new("tide"));
91/// server.at("/metrics").get(tide_prometheus::metrics_endpoint);
92/// # });
93/// ```
94///
95/// Note that serving the metrics on the same server they're counted on will
96/// make Prometheus's scraping also get counted in them. If you want to avoid
97/// this you can use something like
98/// [`tide-fluent-routes`](https://crates.io/crates/tide-fluent-routes) and have
99/// separate trees for your main routes and the one for the metrics endpoint. Or
100/// you can just run a completely separate server in the same program.
101pub async fn metrics_endpoint<State>(
102  _request: tide::Request<State>,
103) -> tide::Result {
104  let encoder = prometheus::TextEncoder::new();
105  let metric_families = prometheus::gather();
106
107  let mut buffer = vec![];
108  encoder.encode(&metric_families, &mut buffer)?;
109  let metrics = String::from_utf8(buffer)?;
110
111  Ok(
112    tide::Response::builder(tide::StatusCode::Ok)
113      .content_type(prometheus::TEXT_FORMAT)
114      .body(metrics)
115      .build(),
116  )
117}