From e0f5e3594dbf3f4b5fa26989c6d1a391949787ca Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 15 Apr 2024 02:36:56 +0000 Subject: [PATCH] feat: add Prometheus metrics --- Cargo.lock | 68 +++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + README.md | 8 ++++++ src/lib.rs | 1 + src/main.rs | 21 ++++++++++++++-- src/metrics.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++ src/server.rs | 1 + src/state.rs | 10 ++++++++ 8 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 src/metrics.rs diff --git a/Cargo.lock b/Cargo.lock index 1f01efb..d415ce7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -673,6 +673,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + [[package]] name = "erased-serde" version = "0.3.31" @@ -1285,6 +1291,7 @@ dependencies = [ "humantime", "hyper", "log", + "prometheus-client", "serde", "sled", "structopt", @@ -1383,7 +1390,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.9", ] [[package]] @@ -1395,11 +1412,24 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -1522,6 +1552,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prometheus-client" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ca959da22a332509f2a73ae9e5f23f9dcfc31fd3a54d71f159495bd5909baa" +dependencies = [ + "dtoa", + "itoa", + "parking_lot 0.12.1", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "quote" version = "1.0.33" @@ -1611,6 +1664,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "ring" version = "0.16.20" @@ -1875,7 +1937,7 @@ dependencies = [ "fxhash", "libc", "log", - "parking_lot", + "parking_lot 0.11.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ad0a205..70896ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ femme = "2.1.0" humantime = "2.0.1" hyper = { version = "0.14", features = ["tcp"] } log = "0.4.11" +prometheus-client = "0.22.2" serde = { version = "1.0.114", features = ["derive"] } sled = "0.34.2" structopt = "0.3.15" diff --git a/README.md b/README.md index 586659b..c19bc7e 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,11 @@ $ ./target/release/notifiers --certificate-file --password ```sh $ curl -X POST -d '{ "token": "" }' http://localhost:9000/register ``` + +### Enabling metrics + +To enable OpenMetrics (Prometheus) metrics endpoint, +run with `--metrics` argument, +e.g. `--metrics 127.0.0.1:9001`. +Metrics can then be retrieved with +`curl http://127.0.0.1:9001/metrics`. diff --git a/src/lib.rs b/src/lib.rs index ba839ed..c365371 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod metrics; pub mod notifier; pub mod server; pub mod state; diff --git a/src/main.rs b/src/main.rs index e6c062b..b258131 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ use std::path::PathBuf; +use std::sync::Arc; use anyhow::{Context, Result}; use async_std::prelude::*; use structopt::StructOpt; -use notifiers::{notifier, server, state}; +use notifiers::{metrics, notifier, server, state}; #[derive(Debug, StructOpt)] struct Opt { @@ -23,6 +24,10 @@ struct Opt { /// The port on which to start the server. #[structopt(long, default_value = "9000")] port: u16, + /// The host and port on which to start the metrics server. + /// For example, `127.0.0.1:9001`. + #[structopt(long)] + metrics: Option, /// The path to the database file. #[structopt(long, default_value = "notifiers.db", parse(from_os_str))] db: PathBuf, @@ -37,11 +42,23 @@ async fn main() -> Result<()> { let opt = Opt::from_args(); let certificate = std::fs::File::open(&opt.certificate_file).context("invalid certificate")?; - let state = state::State::new(&opt.db, certificate, &opt.password, opt.topic.clone())?; + let metrics_state = Arc::new(metrics::Metrics::new()); + + let state = state::State::new( + &opt.db, + certificate, + &opt.password, + opt.topic.clone(), + metrics_state.clone(), + )?; let state2 = state.clone(); let host = opt.host.clone(); let port = opt.port; + + if let Some(metrics_address) = opt.metrics.clone() { + async_std::task::spawn(async move { metrics::start(metrics_state, metrics_address).await }); + } let server = async_std::task::spawn(async move { server::start(state2, host, port).await }); let notif = async_std::task::spawn(async move { notifier::start(state, opt.interval).await }); diff --git a/src/metrics.rs b/src/metrics.rs new file mode 100644 index 0000000..2f6e3be --- /dev/null +++ b/src/metrics.rs @@ -0,0 +1,60 @@ +//! Prometheus (OpenMetrics) metrics server. +//! +//! It is listening on its own address +//! to allow exposting it on a private network only +//! independently of the main service. + +use std::sync::Arc; + +use prometheus_client::encoding::text::encode; +use prometheus_client::metrics::counter::Counter; +use prometheus_client::registry::Registry; + +use anyhow::Result; + +#[derive(Debug, Default)] +pub struct Metrics { + pub registry: Registry, + + pub direct_notifications_total: Counter, +} + +impl Metrics { + pub fn new() -> Self { + let mut registry = Registry::default(); + let direct_notifications_total = Counter::default(); + registry.register( + "direct_notifications", + "Number of direct notifications", + direct_notifications_total.clone(), + ); + Self { + registry, + direct_notifications_total, + } + } + + /// Counts direct notification. + pub fn inc_direct_notification(&self) { + self.direct_notifications_total.inc(); + } +} + +type State = Arc; + +pub async fn start(state: State, server: String) -> Result<()> { + let mut app = tide::with_state(state); + app.at("/metrics").get(metrics); + app.listen(server).await?; + Ok(()) +} + +async fn metrics(req: tide::Request) -> tide::Result { + let mut encoded = String::new(); + encode(&mut encoded, &req.state().registry).unwrap(); + let response = tide::Response::builder(tide::StatusCode::Ok) + .body(encoded) + .content_type("application/openmetrics-text; version=1.0.0; charset=utf-8") + .build(); + Ok(response) +} diff --git a/src/server.rs b/src/server.rs index 3d7dd95..2f358dc 100644 --- a/src/server.rs +++ b/src/server.rs @@ -73,6 +73,7 @@ async fn notify_device(mut req: tide::Request) -> tide::Result { info!("delivered notification for {}", device_token); + req.state().metrics().inc_direct_notification(); } _ => { warn!("unexpected status: {:?}", res); diff --git a/src/state.rs b/src/state.rs index 0a79615..ebe3768 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,6 +6,8 @@ use async_std::sync::Arc; use log::*; use std::io::Seek; +use crate::metrics::Metrics; + #[derive(Debug, Clone)] pub struct State { inner: Arc, @@ -20,6 +22,8 @@ pub struct InnerState { sandbox_client: Client, topic: Option, + + metrics: Arc, } impl State { @@ -28,6 +32,7 @@ impl State { mut certificate: std::fs::File, password: &str, topic: Option, + metrics: Arc, ) -> Result { let db = sled::open(db)?; let production_client = @@ -45,6 +50,7 @@ impl State { production_client, sandbox_client, topic, + metrics, }), }) } @@ -64,4 +70,8 @@ impl State { pub fn topic(&self) -> Option<&str> { self.inner.topic.as_deref() } + + pub fn metrics(&self) -> &Metrics { + self.inner.metrics.as_ref() + } }