1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#![forbid(future_incompatible, unsafe_code)]

//! Tide middleware for [`prometheus`] with a few default metrics.
//!
//! ## Example
//!
//! ```rust
//! # async_std::task::block_on(async {
//! let mut server = tide::new();
//!
//! server.with(tide_prometheus::Prometheus::new("tide"));
//!
//! // Optionally serve these metrics on the same server:
//! server.at("/metrics").get(tide_prometheus::metrics_endpoint);
//! # });
//! ```
//!
//! ## Metrics
//!
//! The `{prefix}` below is the string you put in [`Prometheus::new`].
//!
//! * `{prefix}_http_requests` ([`prometheus::IntCounterVec`]) with labels:
//!   * `method` as the request method.
//!   * `status` as the response status.
//!
//! ## Features
//!
//! * `process` will enable the [`prometheus`] `process` feature, recording
//! various metrics of the process.

pub use prometheus;

use prometheus::Encoder;

/// Tide middleware for [`prometheus`] with a few default metrics.
#[derive(Clone, Debug)]
pub struct Prometheus {
  /// The `{prefix}_http_requests` counter.
  http_requests: prometheus::IntCounterVec,
}

impl Prometheus {
  /// Creates a new Prometheus middleware. This also creates and registers the
  /// metrics in the [`prometheus::default_registry`].
  pub fn new(prefix: &str) -> Self {
    Self {
      http_requests: Self::http_requests(prefix),
    }
  }

  /// Creates, registers and returns the `{prefix}_http_requests` counter.
  fn http_requests(prefix: &str) -> prometheus::IntCounterVec {
    let name = format!("{}_http_requests", prefix);
    let opts = prometheus::Opts::new(name, "Counts http requests");
    prometheus::register_int_counter_vec!(opts, &["method", "code"]).unwrap()
  }
}

#[tide::utils::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State>
  for Prometheus
{
  async fn handle(
    &self,
    request: tide::Request<State>,
    next: tide::Next<'_, State>,
  ) -> tide::Result {
    let method = request.method();
    let response = next.run(request).await;
    let status_code = response.status().to_string();

    self
      .http_requests
      .with_label_values(&[method.as_ref(), &status_code])
      .inc();

    Ok(response)
  }
}

/// A convencience [`tide::Endpoint`] that gathers the metrics with
/// [`prometheus::gather`] and then returns them inside the response body as
/// specified by [the Prometheus docs](https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format).
///
/// ## Example
///
/// ```rust
/// # async_std::task::block_on(async {
/// let mut server = tide::new();
/// server.with(tide_prometheus::Prometheus::new("tide"));
/// server.at("/metrics").get(tide_prometheus::metrics_endpoint);
/// # });
/// ```
///
/// Note that serving the metrics on the same server they're counted on will
/// make Prometheus's scraping also get counted in them. If you want to avoid
/// this you can use something like
/// [`tide-fluent-routes`](https://crates.io/crates/tide-fluent-routes) and have
/// separate trees for your main routes and the one for the metrics endpoint. Or
/// you can just run a completely separate server in the same program.
pub async fn metrics_endpoint<State>(
  _request: tide::Request<State>,
) -> tide::Result {
  let encoder = prometheus::TextEncoder::new();
  let metric_families = prometheus::gather();

  let mut buffer = vec![];
  encoder.encode(&metric_families, &mut buffer)?;
  let metrics = String::from_utf8(buffer)?;

  Ok(
    tide::Response::builder(tide::StatusCode::Ok)
      .content_type(prometheus::TEXT_FORMAT)
      .body(metrics)
      .build(),
  )
}