diff --git a/CHANGELOG.md b/CHANGELOG.md index 0283893e..c1a4445d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for the OpenMetrics protobuf format. See [PR 83]. +- Added a `remove` method to `Family` to allow the removal of a specified label + set from a family. See [PR 85]. +- Added a `clear` method to `Family` to allow the removal of all label sets + from a family. See [PR 85]. +- Impl `TypedMetric` for `CounterWithExemplar` and `HistogramWithExemplar`, so that they can be used with `Family`. See [PR 96]. ### Changed @@ -16,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Move`Encode` trait from `prometheus_client::encoding::text` to `prometheus_client::encoding`. See [PR 83]. [PR 83]: https://github.com/prometheus/client_rust/pull/83 +[PR 85]: https://github.com/prometheus/client_rust/pull/85 +[PR 96]: https://github.com/prometheus/client_rust/pull/96 ## [0.18.0] diff --git a/Cargo.toml b/Cargo.toml index eef8be6e..88bc0569 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,13 +27,15 @@ void = { version = "1.0", optional = true } [dev-dependencies] async-std = { version = "1", features = ["attributes"] } -criterion = "0.3" +criterion = "0.4" http-types = "2" pyo3 = "0.17" quickcheck = "1" rand = "0.8.4" tide = "0.16" actix-web = "4" +tokio = { version = "1", features = ["rt-multi-thread", "net", "macros", "signal"] } +hyper = { version = "0.14.16", features = ["server", "http1", "tcp"] } [build-dependencies] prost-build = { version = "0.9.0", optional = true } diff --git a/README.md b/README.md index fb599048..d046c1ca 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,6 @@ be fixed in the future. Contributions in all forms are most welcome. - Enforce "Exposers SHOULD avoid names that could be confused with the suffixes that text format sample metric names use". -- Protobuf wire format. (Follow [spec - issue](https://github.com/OpenObservability/OpenMetrics/issues/183).) - - Gauge histogram metric. - Allow "A MetricPoint in a Metric with the type [Counter, Histogram] SHOULD have a Timestamp diff --git a/derive-encode/src/lib.rs b/derive-encode/src/lib.rs index 4612ecc9..d6f6eba1 100644 --- a/derive-encode/src/lib.rs +++ b/derive-encode/src/lib.rs @@ -104,7 +104,7 @@ fn derive_protobuf_encode(ast: DeriveInput) -> TokenStream2 { quote! { let mut label = { let mut labels = vec![]; - self.#ident.encode(&mut labels); + prometheus_client::encoding::proto::EncodeLabels::encode(&self.#ident, &mut labels); debug_assert_eq!(1, labels.len(), "Labels encoded from {} should have only one label.", #ident_string); labels.pop().expect("should have an element") }; diff --git a/examples/hyper.rs b/examples/hyper.rs new file mode 100644 index 00000000..93884ca0 --- /dev/null +++ b/examples/hyper.rs @@ -0,0 +1,75 @@ +use hyper::{ + service::{make_service_fn, service_fn}, + Body, Request, Response, Server, +}; +use prometheus_client::{encoding::text::encode, metrics::counter::Counter, registry::Registry}; +use std::{ + future::Future, + io, + net::{IpAddr, Ipv4Addr, SocketAddr}, + pin::Pin, + sync::Arc, +}; +use tokio::signal::unix::{signal, SignalKind}; + +#[tokio::main] +async fn main() { + let request_counter: Counter = Default::default(); + + let mut registry = ::with_prefix("tokio_hyper_example"); + + registry.register( + "requests", + "How many requests the application has received", + request_counter.clone(), + ); + + // Spawn a server to serve the OpenMetrics endpoint. + let metrics_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8001); + start_metrics_server(metrics_addr, registry).await +} + +/// Start a HTTP server to report metrics. +pub async fn start_metrics_server(metrics_addr: SocketAddr, registry: Registry) { + let mut shutdown_stream = signal(SignalKind::terminate()).unwrap(); + + eprintln!("Starting metrics server on {metrics_addr}"); + + let registry = Arc::new(registry); + Server::bind(&metrics_addr) + .serve(make_service_fn(move |_conn| { + let registry = registry.clone(); + async move { + let handler = make_handler(registry); + Ok::<_, io::Error>(service_fn(handler)) + } + })) + .with_graceful_shutdown(async move { + shutdown_stream.recv().await; + }) + .await + .unwrap(); +} + +/// This function returns a HTTP handler (i.e. another function) +pub fn make_handler( + registry: Arc, +) -> impl Fn(Request) -> Pin>> + Send>> { + // This closure accepts a request and responds with the OpenMetrics encoding of our metrics. + move |_req: Request| { + let reg = registry.clone(); + Box::pin(async move { + let mut buf = Vec::new(); + encode(&mut buf, ®.clone()).map(|_| { + let body = Body::from(buf); + Response::builder() + .header( + hyper::header::CONTENT_TYPE, + "application/openmetrics-text; version=1.0.0; charset=utf-8", + ) + .body(body) + .unwrap() + }) + }) + } +} diff --git a/src/metrics/exemplar.rs b/src/metrics/exemplar.rs index 84462003..a2cffc03 100644 --- a/src/metrics/exemplar.rs +++ b/src/metrics/exemplar.rs @@ -4,6 +4,7 @@ use super::counter::{self, Counter}; use super::histogram::Histogram; +use super::{MetricType, TypedMetric}; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use std::collections::HashMap; #[cfg(any(target_arch = "mips", target_arch = "powerpc"))] @@ -31,12 +32,45 @@ pub struct Exemplar { /// counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), "42".to_string())])); /// let _value: (u64, _) = counter_with_exemplar.get(); /// ``` +/// You can also use exemplars with families. Just wrap the exemplar in a Family. +/// ``` +/// # use prometheus_client::metrics::exemplar::CounterWithExemplar; +/// # use prometheus_client::metrics::histogram::exponential_buckets; +/// # use prometheus_client::metrics::family::Family; +/// # use prometheus_client_derive_encode::Encode; +/// #[derive(Clone, Hash, PartialEq, Eq, Encode, Debug, Default)] +/// pub struct ResultLabel { +/// pub result: String, +/// } +/// +/// #[derive(Clone, Hash, PartialEq, Eq, Encode, Debug, Default)] +/// pub struct TraceLabel { +/// pub trace_id: String, +/// } +/// +/// let latency: Family> = Family::default(); +/// +/// latency +/// .get_or_create(&ResultLabel { +/// result: "success".to_owned(), +/// }) +/// .inc_by( +/// 1, +/// Some(TraceLabel { +/// trace_id: "3a2f90c9f80b894f".to_owned(), +/// }), +/// ); +/// ``` #[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] #[derive(Debug)] pub struct CounterWithExemplar { pub(crate) inner: Arc>>, } +impl TypedMetric for CounterWithExemplar { + const TYPE: MetricType = MetricType::Counter; +} + /// Open Metrics [`Counter`] with an [`Exemplar`] to both measure discrete /// events and track references to data outside of the metric set. #[cfg(any(target_arch = "mips", target_arch = "powerpc"))] @@ -122,12 +156,48 @@ impl> CounterWithExemplar { /// let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); /// histogram.observe(4.2, Some(vec![("user_id".to_string(), "42".to_string())])); /// ``` +/// You can also use exemplars with families. Just wrap the exemplar in a Family. +/// ``` +/// # use prometheus_client::metrics::exemplar::HistogramWithExemplars; +/// # use prometheus_client::metrics::histogram::exponential_buckets; +/// # use prometheus_client::metrics::family::Family; +/// # use prometheus_client_derive_encode::Encode; +/// #[derive(Clone, Hash, PartialEq, Eq, Encode, Debug, Default)] +/// pub struct ResultLabel { +/// pub result: String, +/// } +/// +/// #[derive(Clone, Hash, PartialEq, Eq, Encode, Debug, Default)] +/// pub struct TraceLabel { +/// pub trace_id: String, +/// } +/// +/// let latency: Family> = +/// Family::new_with_constructor(|| { +/// HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)) +/// }); +/// +/// latency +/// .get_or_create(&ResultLabel { +/// result: "success".to_owned(), +/// }) +/// .observe( +/// 0.001345422, +/// Some(TraceLabel { +/// trace_id: "3a2f90c9f80b894f".to_owned(), +/// }), +/// ); +/// ``` #[derive(Debug)] pub struct HistogramWithExemplars { // TODO: Not ideal, as Histogram has a Mutex as well. pub(crate) inner: Arc>>, } +impl TypedMetric for HistogramWithExemplars { + const TYPE: MetricType = MetricType::Histogram; +} + impl Clone for HistogramWithExemplars { fn clone(&self) -> Self { Self { diff --git a/src/metrics/family.rs b/src/metrics/family.rs index 9b170d59..0dbf51ee 100644 --- a/src/metrics/family.rs +++ b/src/metrics/family.rs @@ -241,6 +241,47 @@ impl> Family, Counter>::default(); + /// + /// // Will create the metric with label `method="GET"` on first call and + /// // return a reference. + /// family.get_or_create(&vec![("method".to_owned(), "GET".to_owned())]).inc(); + /// + /// // Will return `true`, indicating that the `method="GET"` label set was + /// // removed. + /// assert!(family.remove(&vec![("method".to_owned(), "GET".to_owned())])); + /// ``` + pub fn remove(&self, label_set: &S) -> bool { + self.metrics.write().remove(label_set).is_some() + } + + /// Clear all label sets from the metric family. + /// + /// ``` + /// # use prometheus_client::metrics::counter::{Atomic, Counter}; + /// # use prometheus_client::metrics::family::Family; + /// # + /// let family = Family::, Counter>::default(); + /// + /// // Will create the metric with label `method="GET"` on first call and + /// // return a reference. + /// family.get_or_create(&vec![("method".to_owned(), "GET".to_owned())]).inc(); + /// + /// // Clear the family of all label sets. + /// family.clear(); + /// ``` + pub fn clear(&self) { + self.metrics.write().clear() + } + pub(crate) fn read(&self) -> RwLockReadGuard> { self.metrics.read() } @@ -302,4 +343,87 @@ mod tests { let custom_builder = CustomBuilder { custom_start: 1.0 }; Family::<(), Histogram, CustomBuilder>::new_with_constructor(custom_builder); } + + #[test] + fn counter_family_remove() { + let family = Family::, Counter>::default(); + + family + .get_or_create(&vec![("method".to_string(), "GET".to_string())]) + .inc(); + + assert_eq!( + 1, + family + .get_or_create(&vec![("method".to_string(), "GET".to_string())]) + .get() + ); + + family + .get_or_create(&vec![("method".to_string(), "POST".to_string())]) + .inc_by(2); + + assert_eq!( + 2, + family + .get_or_create(&vec![("method".to_string(), "POST".to_string())]) + .get() + ); + + // Attempt to remove it twice, showing it really was removed on the + // first attempt. + assert!(family.remove(&vec![("method".to_string(), "POST".to_string())])); + assert!(!family.remove(&vec![("method".to_string(), "POST".to_string())])); + + // This should make a new POST label. + family + .get_or_create(&vec![("method".to_string(), "POST".to_string())]) + .inc(); + + assert_eq!( + 1, + family + .get_or_create(&vec![("method".to_string(), "POST".to_string())]) + .get() + ); + + // GET label should have be untouched. + assert_eq!( + 1, + family + .get_or_create(&vec![("method".to_string(), "GET".to_string())]) + .get() + ); + } + + #[test] + fn counter_family_clear() { + let family = Family::, Counter>::default(); + + // Create a label and check it. + family + .get_or_create(&vec![("method".to_string(), "GET".to_string())]) + .inc(); + + assert_eq!( + 1, + family + .get_or_create(&vec![("method".to_string(), "GET".to_string())]) + .get() + ); + + // Clear it, then try recreating and checking it again. + family.clear(); + + family + .get_or_create(&vec![("method".to_string(), "GET".to_string())]) + .inc(); + + assert_eq!( + 1, + family + .get_or_create(&vec![("method".to_string(), "GET".to_string())]) + .get() + ); + } }