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}