diff --git a/Cargo.lock b/Cargo.lock index 56d2ec56..958e93d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,9 +374,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -392,9 +392,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.91" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd97381a8cc6493395a5afc4c691c1084b3768db713b73aa215217aa245d153" +checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" [[package]] name = "cexpr" @@ -449,9 +449,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "futures-core", @@ -916,9 +916,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06fddc2749e0528d2813f95e050e87e52c8cbbae56223b9babf73b3e53b0cc6" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -1177,9 +1177,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736f15a50e749d033164c56c09783b6102c4ff8da79ad77dbddbbaea0f8567f7" +checksum = "908bb38696d7a037a01ebcc68a00634112ac2bbf8ca74e30a2c3d2f4f021302b" dependencies = [ "futures-util", "http", @@ -1876,7 +1876,7 @@ dependencies = [ "http", "http-body-util", "hyper", - "hyper-rustls 0.27.0", + "hyper-rustls 0.27.1", "hyper-staticfile", "hyper-tls", "hyper-util", @@ -1889,7 +1889,6 @@ dependencies = [ "matches", "mustache", "percent-encoding", - "prometheus", "rand_core", "redis", "reqwest", @@ -1931,20 +1930,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prometheus" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" -dependencies = [ - "cfg-if", - "fnv", - "lazy_static", - "memchr", - "parking_lot", - "thiserror", -] - [[package]] name = "psm" version = "0.1.21" @@ -1956,9 +1941,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] diff --git a/Cargo.toml b/Cargo.toml index 1fa3f7e1..60df0892 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,10 +93,6 @@ features = ["builder"] version = "0.4.11" features = ["std", "release_max_level_info"] -[dependencies.prometheus] -version = "0.13.0" -default-features = false - [dependencies.rand_core] optional = true version = "0.6.4" diff --git a/src/agents/fetch.rs b/src/agents/fetch.rs index 91b5be6b..c5b85e6b 100644 --- a/src/agents/fetch.rs +++ b/src/agents/fetch.rs @@ -1,7 +1,7 @@ +use crate::metrics::Histogram; use crate::utils::agent::{Agent, Context, Handler, Message}; use headers::{CacheControl, HeaderMapExt}; use http::StatusCode; -use prometheus::Histogram; use reqwest::{Client, Method, Request}; use std::time::Duration; use thiserror::Error; diff --git a/src/agents/store/mod.rs b/src/agents/store/mod.rs index e79b3a57..43176786 100644 --- a/src/agents/store/mod.rs +++ b/src/agents/store/mod.rs @@ -1,10 +1,10 @@ use crate::agents::key_manager::rotating::{KeySet, RotatingKeys}; use crate::config::LimitInput; use crate::crypto::SigningAlgorithm; +use crate::metrics::Histogram; use crate::utils::agent::{Addr, Message, Sender}; use crate::utils::BoxError; use crate::web::{Session, SessionData}; -use prometheus::Histogram; use std::collections::HashSet; use url::Url; diff --git a/src/handlers/pages.rs b/src/handlers/pages.rs index d36ae02b..8ac955dc 100644 --- a/src/handlers/pages.rs +++ b/src/handlers/pages.rs @@ -4,7 +4,6 @@ use crate::web::{data_response, empty_response, Context, HandlerResult, Response use headers::{ContentType, Header}; use http::StatusCode; use hyper_staticfile::{AcceptEncoding, ResponseBuilder}; -use prometheus::{Encoder, TextEncoder}; use std::env; /// Handler for the root path, redirects to the Portier homepage. @@ -33,13 +32,11 @@ pub async fn version(_ctx: &mut Context) -> HandlerResult { /// Metrics route. (Prometheus-compatible) pub async fn metrics(_ctx: &mut Context) -> HandlerResult { - let mut buffer = vec![]; - let metric_families = prometheus::gather(); - let encoder = TextEncoder::new(); - encoder.encode(&metric_families, &mut buffer).unwrap(); + let mut buffer = String::new(); + crate::metrics::write_metrics(&mut buffer).unwrap(); let mut res = data_response(buffer); - res.header(ContentType::name(), encoder.format_type()); + res.header(ContentType::name(), "text/plain; version=0.0.4"); Ok(res) } diff --git a/src/metrics.rs b/src/metrics.rs index cdd08d85..c3bc14a4 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -1,108 +1,327 @@ -use prometheus::{ - register_histogram, register_int_counter, register_int_counter_vec, Histogram, IntCounter, - IntCounterVec, +use std::{ + fmt, + sync::{ + atomic::{AtomicU64, Ordering}, + RwLock, + }, + time::{Duration, Instant}, }; -lazy_static::lazy_static! { - pub static ref HTTP_CONNECTIONS: IntCounter = register_int_counter!( - "portier_http_connections", - "Number of HTTP connections accepted" - ).unwrap(); +pub static HTTP_CONNECTIONS: Counter = Counter::new(); +pub static HTTP_REQUESTS: Counter = Counter::new(); - pub static ref HTTP_REQUESTS: IntCounter = register_int_counter!( - "portier_http_requests", - "Number of HTTP requests processed" - ).unwrap(); +pub static HTTP_RESPONSE_STATUS_1XX: Counter = Counter::new(); +pub static HTTP_RESPONSE_STATUS_2XX: Counter = Counter::new(); +pub static HTTP_RESPONSE_STATUS_3XX: Counter = Counter::new(); +pub static HTTP_RESPONSE_STATUS_4XX: Counter = Counter::new(); +pub static HTTP_RESPONSE_STATUS_5XX: Counter = Counter::new(); + +pub static AUTH_LIMITED: Counter = Counter::new(); +pub static AUTH_REQUESTS: Counter = Counter::new(); + +pub static AUTH_WEBFINGER_DURATION: Histogram = Histogram::new(); + +pub static AUTH_EMAIL_REQUESTS: Counter = Counter::new(); +pub static AUTH_EMAIL_SEND_DURATION: Histogram = Histogram::new(); + +pub static AUTH_EMAIL_COMPLETED: Counter = Counter::new(); +pub static AUTH_EMAIL_CODE_INCORRECT: Counter = Counter::new(); + +pub static AUTH_OIDC_REQUESTS_PORTIER: Counter = Counter::new(); +pub static AUTH_OIDC_REQUESTS_GOOGLE: Counter = Counter::new(); +pub static AUTH_OIDC_FETCH_CONFIG_DURATION: Histogram = Histogram::new(); +pub static AUTH_OIDC_FETCH_JWKS_DURATION: Histogram = Histogram::new(); +pub static AUTH_OIDC_COMPLETED: Counter = Counter::new(); + +pub static DOMAIN_VALIDATION_INVALID_NAME: Counter = Counter::new(); +pub static DOMAIN_VALIDATION_BLOCKED: Counter = Counter::new(); +pub static DOMAIN_VALIDATION_NULL_MX: Counter = Counter::new(); +pub static DOMAIN_VALIDATION_NO_SERVERS: Counter = Counter::new(); +pub static DOMAIN_VALIDATION_NO_PUBLIC_IPS: Counter = Counter::new(); - pub static ref HTTP_RESPONSE_STATUS: IntCounterVec = register_int_counter_vec!( +pub fn write_metrics(w: &mut impl fmt::Write) -> Result<(), fmt::Error> { + fn header( + w: &mut impl fmt::Write, + name: &str, + kind: &str, + help: &str, + ) -> Result<(), fmt::Error> { + writeln!(w, "# HELP {name} {help}")?; + writeln!(w, "# TYPE {name} {kind}")?; + Ok(()) + } + + fn header_and_metric( + w: &mut impl fmt::Write, + name: &str, + kind: &str, + help: &str, + metric: &impl Metric, + ) -> Result<(), fmt::Error> { + header(w, name, kind, help)?; + metric.format(w, name) + } + + header_and_metric( + w, + "portier_http_connections", + "counter", + "Number of HTTP connections accepted.", + &HTTP_CONNECTIONS, + )?; + header_and_metric( + w, + "portier_http_requests", + "counter", + "Number of HTTP requests processed.", + &HTTP_REQUESTS, + )?; + header( + w, "portier_http_response_status", - "Number of HTTP responses by status category", - &["code"] - ).unwrap(); - pub static ref HTTP_RESPONSE_STATUS_1XX: IntCounter = - HTTP_RESPONSE_STATUS.with_label_values(&["1xx"]); - pub static ref HTTP_RESPONSE_STATUS_2XX: IntCounter = - HTTP_RESPONSE_STATUS.with_label_values(&["2xx"]); - pub static ref HTTP_RESPONSE_STATUS_3XX: IntCounter = - HTTP_RESPONSE_STATUS.with_label_values(&["3xx"]); - pub static ref HTTP_RESPONSE_STATUS_4XX: IntCounter = - HTTP_RESPONSE_STATUS.with_label_values(&["4xx"]); - pub static ref HTTP_RESPONSE_STATUS_5XX: IntCounter = - HTTP_RESPONSE_STATUS.with_label_values(&["5xx"]); - - pub static ref AUTH_LIMITED: IntCounter = register_int_counter!( - "portier_auth_limited", - "Number of rate-limited authentication requests" - ).unwrap(); + "counter", + "Number of HTTP responses by status category.", + )?; + HTTP_RESPONSE_STATUS_1XX.format(w, "portier_http_response_status{code=\"1xx\"}")?; + HTTP_RESPONSE_STATUS_2XX.format(w, "portier_http_response_status{code=\"2xx\"}")?; + HTTP_RESPONSE_STATUS_3XX.format(w, "portier_http_response_status{code=\"3xx\"}")?; + HTTP_RESPONSE_STATUS_4XX.format(w, "portier_http_response_status{code=\"4xx\"}")?; + HTTP_RESPONSE_STATUS_5XX.format(w, "portier_http_response_status{code=\"5xx\"}")?; - pub static ref AUTH_REQUESTS: IntCounter = register_int_counter!( + header_and_metric( + w, + "portier_auth_limited", + "counter", + "Number of rate-limited authentication requests.", + &AUTH_LIMITED, + )?; + header_and_metric( + w, "portier_auth_requests", - "Number of authentication requests" - ).unwrap(); + "counter", + "Number of authentication requests.", + &AUTH_REQUESTS, + )?; - pub static ref AUTH_WEBFINGER_DURATION: Histogram = register_histogram!( + header_and_metric( + w, "portier_auth_webfinger_duration", - "Latency of outgoing Webfinger requests." - ).unwrap(); + "histogram", + "Latency of outgoing Webfinger requests.", + &AUTH_WEBFINGER_DURATION, + )?; - pub static ref AUTH_EMAIL_REQUESTS: IntCounter = register_int_counter!( + header_and_metric( + w, "portier_auth_email_requests", - "Number of authentication requests that used email" - ).unwrap(); - - pub static ref AUTH_EMAIL_SEND_DURATION: Histogram = register_histogram!( + "counter", + "Number of authentication requests that used email.", + &AUTH_EMAIL_REQUESTS, + )?; + header_and_metric( + w, "portier_auth_email_send_duration", - "Latency of sending email" - ).unwrap(); - - pub static ref AUTH_EMAIL_COMPLETED: IntCounter = register_int_counter!( + "histogram", + "Latency of sending email.", + &AUTH_EMAIL_SEND_DURATION, + )?; + header_and_metric( + w, "portier_auth_email_completed", - "Number of successful email authentications" - ).unwrap(); - - pub static ref AUTH_EMAIL_CODE_INCORRECT: IntCounter = register_int_counter!( + "counter", + "Number of successful email authentications.", + &AUTH_EMAIL_COMPLETED, + )?; + header_and_metric( + w, "portier_auth_email_code_incorrect", - "Number of email confirmation attempts with an invalid code" - ).unwrap(); + "counter", + "Number of email confirmation attempts with an invalid code.", + &AUTH_EMAIL_CODE_INCORRECT, + )?; - pub static ref AUTH_OIDC_REQUESTS: IntCounterVec = register_int_counter_vec!( + header( + w, "portier_auth_oidc_requests", - "Number of authentication requests that used OpenID Connect", - &["rel"] - ).unwrap(); - pub static ref AUTH_OIDC_REQUESTS_PORTIER: IntCounter = - AUTH_OIDC_REQUESTS.with_label_values(&["portier"]); - pub static ref AUTH_OIDC_REQUESTS_GOOGLE: IntCounter = - AUTH_OIDC_REQUESTS.with_label_values(&["google"]); - - pub static ref AUTH_OIDC_FETCH_CONFIG_DURATION: Histogram = register_histogram!( + "counter", + "Number of authentication requests that used OpenID Connect.", + )?; + AUTH_OIDC_REQUESTS_PORTIER.format(w, "portier_auth_oidc_requests{rel=\"portier\"}")?; + AUTH_OIDC_REQUESTS_GOOGLE.format(w, "portier_auth_oidc_requests{rel=\"google\"}")?; + header_and_metric( + w, "portier_auth_oidc_fetch_config_duration", - "Latency of outgoing requests for OpenID Connect configuration documents" - ).unwrap(); - - pub static ref AUTH_OIDC_FETCH_JWKS_DURATION: Histogram = register_histogram!( + "histogram", + "Latency of outgoing requests for OpenID Connect configuration documents.", + &AUTH_OIDC_FETCH_CONFIG_DURATION, + )?; + header_and_metric( + w, "portier_auth_oidc_fetch_jwks_duration", - "Latency of outgoing requests for OpenID Connect JWKs" - ).unwrap(); - - pub static ref AUTH_OIDC_COMPLETED: IntCounter = register_int_counter!( + "histogram", + "Latency of outgoing requests for OpenID Connect JWKs.", + &AUTH_OIDC_FETCH_JWKS_DURATION, + )?; + header_and_metric( + w, "portier_auth_oidc_completed", - "Number of successful OpenID Connect authentications" - ).unwrap(); + "counter", + "Number of successful OpenID Connect authentications.", + &AUTH_OIDC_COMPLETED, + )?; - pub static ref DOMAIN_VALIDATION_ERROR: IntCounterVec = register_int_counter_vec!( + header( + w, "portier_domain_validation_error", - "Number of authentication requests for invalid domains", - &["reason"] - ).unwrap(); - pub static ref DOMAIN_VALIDATION_INVALID_NAME: IntCounter = - DOMAIN_VALIDATION_ERROR.with_label_values(&["invalid_name"]); - pub static ref DOMAIN_VALIDATION_BLOCKED: IntCounter = - DOMAIN_VALIDATION_ERROR.with_label_values(&["blocked"]); - pub static ref DOMAIN_VALIDATION_NULL_MX: IntCounter = - DOMAIN_VALIDATION_ERROR.with_label_values(&["null_mx"]); - pub static ref DOMAIN_VALIDATION_NO_SERVERS: IntCounter = - DOMAIN_VALIDATION_ERROR.with_label_values(&["no_servers"]); - pub static ref DOMAIN_VALIDATION_NO_PUBLIC_IPS: IntCounter = - DOMAIN_VALIDATION_ERROR.with_label_values(&["no_public_ips"]); + "counter", + "Number of authentication requests for invalid domains.", + )?; + DOMAIN_VALIDATION_INVALID_NAME.format( + w, + "portier_domain_validation_error{reason=\"invalid_name\"}", + )?; + DOMAIN_VALIDATION_BLOCKED.format(w, "portier_domain_validation_error{reason=\"blocked\"}")?; + DOMAIN_VALIDATION_NULL_MX.format(w, "portier_domain_validation_error{reason=\"null_mx\"}")?; + DOMAIN_VALIDATION_NO_SERVERS + .format(w, "portier_domain_validation_error{reason=\"no_servers\"}")?; + DOMAIN_VALIDATION_NO_PUBLIC_IPS.format( + w, + "portier_domain_validation_error{reason=\"no_public_ips\"}", + )?; + + Ok(()) +} + +trait Metric { + fn format(&self, w: &mut impl fmt::Write, name: &str) -> Result<(), fmt::Error>; +} + +pub struct Counter(AtomicU64); +impl Counter { + pub const fn new() -> Counter { + Counter(AtomicU64::new(0)) + } + pub fn inc(&self) { + self.0.fetch_add(1, Ordering::AcqRel); + } +} +impl Metric for Counter { + fn format(&self, w: &mut impl fmt::Write, name: &str) -> Result<(), fmt::Error> { + writeln!(w, "{name} {}", self.0.load(Ordering::Relaxed)) + } +} + +pub struct Histogram(RwLock); +struct HistogramInner { + le_5: u64, + le_10: u64, + le_25: u64, + le_50: u64, + le_100: u64, + le_250: u64, + le_500: u64, + le_1000: u64, + le_2500: u64, + le_5000: u64, + le_10000: u64, + sum: Duration, + count: u64, +} +impl Histogram { + pub const fn new() -> Histogram { + Histogram(RwLock::new(HistogramInner { + le_5: 0, + le_10: 0, + le_25: 0, + le_50: 0, + le_100: 0, + le_250: 0, + le_500: 0, + le_1000: 0, + le_2500: 0, + le_5000: 0, + le_10000: 0, + sum: Duration::ZERO, + count: 0, + })) + } + pub fn record(&self, time: Duration) { + let mut inner = self.0.write().unwrap(); + if time <= Duration::from_millis(5) { + inner.le_5 += 1; + } + if time <= Duration::from_millis(10) { + inner.le_10 += 1; + } + if time <= Duration::from_millis(25) { + inner.le_25 += 1; + } + if time <= Duration::from_millis(50) { + inner.le_50 += 1; + } + if time <= Duration::from_millis(100) { + inner.le_100 += 1; + } + if time <= Duration::from_millis(250) { + inner.le_250 += 1; + } + if time <= Duration::from_millis(500) { + inner.le_500 += 1; + } + if time <= Duration::from_millis(1000) { + inner.le_1000 += 1; + } + if time <= Duration::from_millis(2500) { + inner.le_2500 += 1; + } + if time <= Duration::from_millis(5000) { + inner.le_5000 += 1; + } + if time <= Duration::from_millis(10000) { + inner.le_10000 += 1; + } + inner.sum += time; + inner.count += 1; + } + pub fn start_timer(&self) -> HistogramTimer { + HistogramTimer { + inner: self, + start: Instant::now(), + } + } +} +impl Metric for Histogram { + fn format(&self, w: &mut impl fmt::Write, name: &str) -> Result<(), fmt::Error> { + let inner = self.0.read().unwrap(); + writeln!(w, "{name}_bucket{{le=\"0.005\"}} {}", inner.le_5)?; + writeln!(w, "{name}_bucket{{le=\"0.01\"}} {}", inner.le_10)?; + writeln!(w, "{name}_bucket{{le=\"0.025\"}} {}", inner.le_25)?; + writeln!(w, "{name}_bucket{{le=\"0.05\"}} {}", inner.le_50)?; + writeln!(w, "{name}_bucket{{le=\"0.1\"}} {}", inner.le_100)?; + writeln!(w, "{name}_bucket{{le=\"0.25\"}} {}", inner.le_250)?; + writeln!(w, "{name}_bucket{{le=\"0.5\"}} {}", inner.le_500)?; + writeln!(w, "{name}_bucket{{le=\"1\"}} {}", inner.le_1000)?; + writeln!(w, "{name}_bucket{{le=\"2.5\"}} {}", inner.le_2500)?; + writeln!(w, "{name}_bucket{{le=\"5\"}} {}", inner.le_5000)?; + writeln!(w, "{name}_bucket{{le=\"10\"}} {}", inner.le_10000)?; + writeln!(w, "{name}_bucket{{le=\"+Inf\"}} {}", inner.count)?; + writeln!( + w, + "{name}_sum {}.{:09}", + inner.sum.as_secs(), + inner.sum.subsec_nanos() + )?; + writeln!(w, "{name}_count {}", inner.count)?; + Ok(()) + } +} + +pub struct HistogramTimer<'a> { + inner: &'a Histogram, + start: Instant, +} +impl<'a> HistogramTimer<'a> { + pub fn observe_duration(self) { + self.inner.record(Instant::now().duration_since(self.start)); + } }