From de2723402f9f7604e1d4a7419ae11c3f14e9803d Mon Sep 17 00:00:00 2001 From: Qinxuan Chen <koushiro.cqx@gmail.com> Date: Thu, 7 Mar 2024 17:45:38 +0800 Subject: [PATCH 01/27] feat(metrics): support `Gauge<u32, AtomicU32>` type (#191) Signed-off-by: koushiro <koushiro.cqx@gmail.com> --- CHANGELOG.md | 9 +++++++ Cargo.toml | 2 +- src/encoding.rs | 14 ++++++++-- src/encoding/protobuf.rs | 4 +++ src/encoding/text.rs | 13 +++++++-- src/metrics/gauge.rs | 58 +++++++++++++++++++++++++++++----------- 6 files changed, 79 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c83e85b..7b1533d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.22.2] + +### Added + +- Added `Gauge<u32, AtomicU32>` implementation. + See [PR 191]. + +[PR 191]: https://github.com/prometheus/client_rust/pull/191 + ## [0.22.1] ### Added diff --git a/Cargo.toml b/Cargo.toml index ce4d0ce3..7e919c71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus-client" -version = "0.22.1" +version = "0.22.2" authors = ["Max Inden <mail@max-inden.de>"] edition = "2021" description = "Open Metrics client library allowing users to natively instrument applications." diff --git a/src/encoding.rs b/src/encoding.rs index 24d0d729..23c60328 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -544,6 +544,12 @@ pub trait EncodeGaugeValue { fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error>; } +impl EncodeGaugeValue for u32 { + fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_u32(*self) + } +} + impl EncodeGaugeValue for i64 { fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error> { encoder.encode_i64(*self) @@ -568,13 +574,17 @@ enum GaugeValueEncoderInner<'a> { } impl<'a> GaugeValueEncoder<'a> { - fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { - for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_f64(v)) + fn encode_u32(&mut self, v: u32) -> Result<(), std::fmt::Error> { + for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_u32(v)) } fn encode_i64(&mut self, v: i64) -> Result<(), std::fmt::Error> { for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_i64(v)) } + + fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_f64(v)) + } } impl<'a> From<text::GaugeValueEncoder<'a>> for GaugeValueEncoder<'a> { diff --git a/src/encoding/protobuf.rs b/src/encoding/protobuf.rs index 53b30c3a..eef2731a 100644 --- a/src/encoding/protobuf.rs +++ b/src/encoding/protobuf.rs @@ -323,6 +323,10 @@ pub(crate) struct GaugeValueEncoder<'a> { } impl<'a> GaugeValueEncoder<'a> { + pub fn encode_u32(&mut self, v: u32) -> Result<(), std::fmt::Error> { + self.encode_i64(v as i64) + } + pub fn encode_i64(&mut self, v: i64) -> Result<(), std::fmt::Error> { *self.value = openmetrics_data_model::gauge_value::Value::IntValue(v); Ok(()) diff --git a/src/encoding/text.rs b/src/encoding/text.rs index adf42415..4acf1d51 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -423,9 +423,9 @@ impl<'a> std::fmt::Debug for GaugeValueEncoder<'a> { } impl<'a> GaugeValueEncoder<'a> { - pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + pub fn encode_u32(&mut self, v: u32) -> Result<(), std::fmt::Error> { self.writer.write_str(" ")?; - self.writer.write_str(dtoa::Buffer::new().format(v))?; + self.writer.write_str(itoa::Buffer::new().format(v))?; Ok(()) } @@ -434,6 +434,12 @@ impl<'a> GaugeValueEncoder<'a> { self.writer.write_str(itoa::Buffer::new().format(v))?; Ok(()) } + + pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + self.writer.write_str(" ")?; + self.writer.write_str(dtoa::Buffer::new().format(v))?; + Ok(()) + } } pub(crate) struct ExemplarValueEncoder<'a> { @@ -565,6 +571,7 @@ mod tests { use crate::metrics::{counter::Counter, exemplar::CounterWithExemplar}; use pyo3::{prelude::*, types::PyModule}; use std::borrow::Cow; + use std::sync::atomic::AtomicU32; #[test] fn encode_counter() { @@ -632,6 +639,8 @@ mod tests { let mut registry = Registry::default(); let gauge: Gauge = Gauge::default(); registry.register("my_gauge", "My gauge", gauge); + let gauge = Gauge::<u32, AtomicU32>::default(); + registry.register("u32_gauge", "Gauge::<u32, AtomicU32>", gauge); let mut encoded = String::new(); diff --git a/src/metrics/gauge.rs b/src/metrics/gauge.rs index 98671280..7b268427 100644 --- a/src/metrics/gauge.rs +++ b/src/metrics/gauge.rs @@ -6,7 +6,7 @@ use crate::encoding::{EncodeGaugeValue, EncodeMetric, MetricEncoder}; use super::{MetricType, TypedMetric}; use std::marker::PhantomData; -use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::atomic::{AtomicI32, AtomicU32, Ordering}; #[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] use std::sync::atomic::{AtomicI64, AtomicU64}; use std::sync::Arc; @@ -134,55 +134,81 @@ pub trait Atomic<N> { fn get(&self) -> N; } -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] -impl Atomic<i64> for AtomicI64 { - fn inc(&self) -> i64 { +impl Atomic<i32> for AtomicI32 { + fn inc(&self) -> i32 { self.inc_by(1) } - fn inc_by(&self, v: i64) -> i64 { + fn inc_by(&self, v: i32) -> i32 { self.fetch_add(v, Ordering::Relaxed) } - fn dec(&self) -> i64 { + fn dec(&self) -> i32 { self.dec_by(1) } - fn dec_by(&self, v: i64) -> i64 { + fn dec_by(&self, v: i32) -> i32 { self.fetch_sub(v, Ordering::Relaxed) } - fn set(&self, v: i64) -> i64 { + fn set(&self, v: i32) -> i32 { self.swap(v, Ordering::Relaxed) } - fn get(&self) -> i64 { + fn get(&self) -> i32 { self.load(Ordering::Relaxed) } } -impl Atomic<i32> for AtomicI32 { - fn inc(&self) -> i32 { +impl Atomic<u32> for AtomicU32 { + fn inc(&self) -> u32 { self.inc_by(1) } - fn inc_by(&self, v: i32) -> i32 { + fn inc_by(&self, v: u32) -> u32 { self.fetch_add(v, Ordering::Relaxed) } - fn dec(&self) -> i32 { + fn dec(&self) -> u32 { self.dec_by(1) } - fn dec_by(&self, v: i32) -> i32 { + fn dec_by(&self, v: u32) -> u32 { self.fetch_sub(v, Ordering::Relaxed) } - fn set(&self, v: i32) -> i32 { + fn set(&self, v: u32) -> u32 { self.swap(v, Ordering::Relaxed) } - fn get(&self) -> i32 { + fn get(&self) -> u32 { + self.load(Ordering::Relaxed) + } +} + +#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +impl Atomic<i64> for AtomicI64 { + fn inc(&self) -> i64 { + self.inc_by(1) + } + + fn inc_by(&self, v: i64) -> i64 { + self.fetch_add(v, Ordering::Relaxed) + } + + fn dec(&self) -> i64 { + self.dec_by(1) + } + + fn dec_by(&self, v: i64) -> i64 { + self.fetch_sub(v, Ordering::Relaxed) + } + + fn set(&self, v: i64) -> i64 { + self.swap(v, Ordering::Relaxed) + } + + fn get(&self) -> i64 { self.load(Ordering::Relaxed) } } From b462e610498510cc1a4de42c7e920ba67032a493 Mon Sep 17 00:00:00 2001 From: Jonathan Davies <jd+github@upthedownstair.com> Date: Sun, 31 Mar 2024 13:41:59 +0000 Subject: [PATCH 02/27] examples/actix-web.rs: default enable compress middleware. (#192) Signed-off-by: Jonathan Davies <jpds@protonmail.com> --- examples/actix-web.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/actix-web.rs b/examples/actix-web.rs index d01a1027..a1ce8e79 100644 --- a/examples/actix-web.rs +++ b/examples/actix-web.rs @@ -1,5 +1,6 @@ use std::sync::Mutex; +use actix_web::middleware::Compress; use actix_web::{web, App, HttpResponse, HttpServer, Responder, Result}; use prometheus_client::encoding::text::encode; use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue}; @@ -61,6 +62,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() + .wrap(Compress::default()) .app_data(metrics.clone()) .app_data(state.clone()) .service(web::resource("/metrics").route(web::get().to(metrics_handler))) From bf196d77824294cce6bf996f79cef25f7aa82c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20L=C3=A9vai?= <levaitamas@users.noreply.github.com> Date: Mon, 13 May 2024 21:17:19 +0200 Subject: [PATCH 03/27] chore(examples/hyper): port to hyper 1.x (#195) Based on https://github.com/prometheus/client_rust/pull/184 Co-authored-by: Jun Kurihara <junkurihara@users.noreply.github.com> Signed-off-by: Tamas Levai <levait@tmit.bme.hu> --- Cargo.toml | 4 ++- examples/hyper.rs | 62 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7e919c71..06385a2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,9 @@ 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"] } +hyper = { version = "1.3.1", features = ["server", "http1"] } +hyper-util = { version = "0.1.3", features = ["tokio"] } +http-body-util = "0.1.1" [build-dependencies] prost-build = { version = "0.11.0", optional = true } diff --git a/examples/hyper.rs b/examples/hyper.rs index f5a4009d..82ee121b 100644 --- a/examples/hyper.rs +++ b/examples/hyper.rs @@ -1,7 +1,11 @@ +use http_body_util::{combinators, BodyExt, Full}; use hyper::{ - service::{make_service_fn, service_fn}, - Body, Request, Response, Server, + body::{Bytes, Incoming}, + server::conn::http1, + service::service_fn, + Request, Response, }; +use hyper_util::rt::TokioIo; use prometheus_client::{encoding::text::encode, metrics::counter::Counter, registry::Registry}; use std::{ future::Future, @@ -10,7 +14,11 @@ use std::{ pin::Pin, sync::Arc, }; -use tokio::signal::unix::{signal, SignalKind}; +use tokio::{ + net::TcpListener, + pin, + signal::unix::{signal, SignalKind}, +}; #[tokio::main] async fn main() { @@ -31,39 +39,48 @@ async fn main() { /// 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)) + + let tcp_listener = TcpListener::bind(metrics_addr).await.unwrap(); + let server = http1::Builder::new(); + while let Ok((stream, _)) = tcp_listener.accept().await { + let mut shutdown_stream = signal(SignalKind::terminate()).unwrap(); + let io = TokioIo::new(stream); + let server_clone = server.clone(); + let registry_clone = registry.clone(); + tokio::task::spawn(async move { + let conn = server_clone.serve_connection(io, service_fn(make_handler(registry_clone))); + pin!(conn); + tokio::select! { + _ = conn.as_mut() => {} + _ = shutdown_stream.recv() => { + conn.as_mut().graceful_shutdown(); + } } - })) - .with_graceful_shutdown(async move { - shutdown_stream.recv().await; - }) - .await - .unwrap(); + }); + } } +/// Boxed HTTP body for responses +type BoxBody = combinators::BoxBody<Bytes, hyper::Error>; + /// This function returns a HTTP handler (i.e. another function) pub fn make_handler( registry: Arc<Registry>, -) -> impl Fn(Request<Body>) -> Pin<Box<dyn Future<Output = io::Result<Response<Body>>> + Send>> { +) -> impl Fn(Request<Incoming>) -> Pin<Box<dyn Future<Output = io::Result<Response<BoxBody>>> + Send>> +{ // This closure accepts a request and responds with the OpenMetrics encoding of our metrics. - move |_req: Request<Body>| { + move |_req: Request<Incoming>| { let reg = registry.clone(); + Box::pin(async move { let mut buf = String::new(); encode(&mut buf, ®.clone()) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) .map(|_| { - let body = Body::from(buf); + let body = full(Bytes::from(buf)); Response::builder() .header( hyper::header::CONTENT_TYPE, @@ -75,3 +92,8 @@ pub fn make_handler( }) } } + +/// helper function to build a full boxed body +pub fn full(body: Bytes) -> BoxBody { + Full::new(body).map_err(|never| match never {}).boxed() +} From 4b78df1ec4d736d74da6998246f3c37e598b5144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Gillot-Lamure?= <leo.gillot@navaati.net> Date: Tue, 2 Jul 2024 10:29:14 +0200 Subject: [PATCH 04/27] chore(portability): support all platforms without 64 bit atomics (#203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notably, RISC-V 32 for ESP32-C3 chips. Signed-off-by: Léo Gillot-Lamure <leo.gillot@navaati.net> Signed-off-by: Max Inden <mail@max-inden.de> Co-authored-by: Max Inden <mail@max-inden.de> --- CHANGELOG.md | 9 +++++++++ src/metrics/counter.rs | 12 ++++++------ src/metrics/exemplar.rs | 8 ++++---- src/metrics/gauge.rs | 10 +++++----- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b1533d4..a52b060d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.22.3] - unreleased + +### Added + +- Support all platforms with 32 bit atomics lacking 64 bit atomics. + See [PR 203]. + +[PR 203]: https://github.com/prometheus/client_rust/pull/203 + ## [0.22.2] ### Added diff --git a/src/metrics/counter.rs b/src/metrics/counter.rs index c1c5e511..63d84ffc 100644 --- a/src/metrics/counter.rs +++ b/src/metrics/counter.rs @@ -6,7 +6,7 @@ use crate::encoding::{EncodeMetric, MetricEncoder}; use super::{MetricType, TypedMetric}; use std::marker::PhantomData; -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] use std::sync::atomic::AtomicU64; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; @@ -40,7 +40,7 @@ use std::sync::Arc; /// counter.inc(); /// let _value: f64 = counter.get(); /// ``` -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] #[derive(Debug)] pub struct Counter<N = u64, A = AtomicU64> { value: Arc<A>, @@ -48,7 +48,7 @@ pub struct Counter<N = u64, A = AtomicU64> { } /// Open Metrics [`Counter`] to measure discrete events. -#[cfg(any(target_arch = "mips", target_arch = "powerpc"))] +#[cfg(not(target_has_atomic = "64"))] #[derive(Debug)] pub struct Counter<N = u32, A = AtomicU32> { value: Arc<A>, @@ -114,7 +114,7 @@ pub trait Atomic<N> { fn get(&self) -> N; } -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] impl Atomic<u64> for AtomicU64 { fn inc(&self) -> u64 { self.inc_by(1) @@ -143,7 +143,7 @@ impl Atomic<u32> for AtomicU32 { } } -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] impl Atomic<f64> for AtomicU64 { fn inc(&self) -> f64 { self.inc_by(1.0) @@ -231,7 +231,7 @@ mod tests { assert_eq!(1, counter.get()); } - #[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] + #[cfg(target_has_atomic = "64")] #[test] fn f64_stored_in_atomic_u64() { fn prop(fs: Vec<f64>) { diff --git a/src/metrics/exemplar.rs b/src/metrics/exemplar.rs index c8478c6a..e61f4d5a 100644 --- a/src/metrics/exemplar.rs +++ b/src/metrics/exemplar.rs @@ -11,9 +11,9 @@ 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"))] +#[cfg(not(target_has_atomic = "64"))] use std::sync::atomic::AtomicU32; -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] use std::sync::atomic::AtomicU64; use std::sync::Arc; @@ -65,7 +65,7 @@ pub struct Exemplar<S, V> { /// }), /// ); /// ``` -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] #[derive(Debug)] pub struct CounterWithExemplar<S, N = u64, A = AtomicU64> { pub(crate) inner: Arc<RwLock<CounterWithExemplarInner<S, N, A>>>, @@ -77,7 +77,7 @@ impl<S> TypedMetric for CounterWithExemplar<S> { /// 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"))] +#[cfg(not(target_has_atomic = "64"))] #[derive(Debug)] pub struct CounterWithExemplar<S, N = u32, A = AtomicU32> { pub(crate) inner: Arc<RwLock<CounterWithExemplarInner<S, N, A>>>, diff --git a/src/metrics/gauge.rs b/src/metrics/gauge.rs index 7b268427..fcf7e6a7 100644 --- a/src/metrics/gauge.rs +++ b/src/metrics/gauge.rs @@ -7,7 +7,7 @@ use crate::encoding::{EncodeGaugeValue, EncodeMetric, MetricEncoder}; use super::{MetricType, TypedMetric}; use std::marker::PhantomData; use std::sync::atomic::{AtomicI32, AtomicU32, Ordering}; -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] use std::sync::atomic::{AtomicI64, AtomicU64}; use std::sync::Arc; @@ -40,7 +40,7 @@ use std::sync::Arc; /// gauge.set(42.0); /// let _value: f64 = gauge.get(); /// ``` -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] #[derive(Debug)] pub struct Gauge<N = i64, A = AtomicI64> { value: Arc<A>, @@ -48,7 +48,7 @@ pub struct Gauge<N = i64, A = AtomicI64> { } /// Open Metrics [`Gauge`] to record current measurements. -#[cfg(any(target_arch = "mips", target_arch = "powerpc"))] +#[cfg(not(target_has_atomic = "64"))] #[derive(Debug)] pub struct Gauge<N = i32, A = AtomicI32> { value: Arc<A>, @@ -186,7 +186,7 @@ impl Atomic<u32> for AtomicU32 { } } -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] impl Atomic<i64> for AtomicI64 { fn inc(&self) -> i64 { self.inc_by(1) @@ -213,7 +213,7 @@ impl Atomic<i64> for AtomicI64 { } } -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] +#[cfg(target_has_atomic = "64")] impl Atomic<f64> for AtomicU64 { fn inc(&self) -> f64 { self.inc_by(1.0) From a914fc96308c65e0a87f44d5e30cd7ebc6a77e70 Mon Sep 17 00:00:00 2001 From: Tyrone Wu <wudevelops@gmail.com> Date: Sun, 7 Jul 2024 09:17:47 -0400 Subject: [PATCH 05/27] feat(encoding): add functions for individually encoding registry & EOF (#205) All credits on the initial idea, implementation, and testing belong to @amunra, who is the original author of this PR #154. From the original PR description: Adds new `encode_registry` and `encode_eof` functions to allow encoding of parts of the response. This is useful when there are multiple registries at play, or when composing metrics for a process that embeds Rust as part of its logic whilst serving metrics to Prometheus outside of Rust. Fixes: #153 Refs: #154, #204 Co-authored-by: amunra <cimarosti@gmail.com> Signed-off-by: tyrone-wu <wudevelops@gmail.com> Signed-off-by: Max Inden <mail@max-inden.de> --- CHANGELOG.md | 5 ++ Cargo.toml | 2 +- src/encoding/text.rs | 204 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 200 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a52b060d..d6eed664 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 `encode_registry` and `encode_eof` functions to `text` module. + See [PR 205]. + + [PR 205]: https://github.com/prometheus/client_rust/pull/205 + - Support all platforms with 32 bit atomics lacking 64 bit atomics. See [PR 203]. diff --git a/Cargo.toml b/Cargo.toml index 06385a2e..fae43aa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus-client" -version = "0.22.2" +version = "0.22.3" authors = ["Max Inden <mail@max-inden.de>"] edition = "2021" description = "Open Metrics client library allowing users to natively instrument applications." diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 4acf1d51..42815269 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -1,7 +1,7 @@ //! Open Metrics text format implementation. //! //! ``` -//! # use prometheus_client::encoding::text::encode; +//! # use prometheus_client::encoding::text::{encode, encode_registry, encode_eof}; //! # use prometheus_client::metrics::counter::Counter; //! # use prometheus_client::registry::Registry; //! # @@ -15,13 +15,26 @@ //! # ); //! # counter.inc(); //! let mut buffer = String::new(); +//! +//! // Encode the complete OpenMetrics exposition into the message buffer //! encode(&mut buffer, ®istry).unwrap(); +//! let expected_msg = "# HELP my_counter This is my counter.\n".to_owned() + +//! "# TYPE my_counter counter\n" + +//! "my_counter_total 1\n" + +//! "# EOF\n"; +//! assert_eq!(expected_msg, buffer); +//! buffer.clear(); +//! +//! // Encode just the registry into the message buffer +//! encode_registry(&mut buffer, ®istry).unwrap(); +//! let expected_reg = "# HELP my_counter This is my counter.\n".to_owned() + +//! "# TYPE my_counter counter\n" + +//! "my_counter_total 1\n"; +//! assert_eq!(expected_reg, buffer); //! -//! let expected = "# HELP my_counter This is my counter.\n".to_owned() + -//! "# TYPE my_counter counter\n" + -//! "my_counter_total 1\n" + -//! "# EOF\n"; -//! assert_eq!(expected, buffer); +//! // Encode EOF marker into message buffer to complete the OpenMetrics exposition +//! encode_eof(&mut buffer).unwrap(); +//! assert_eq!(expected_msg, buffer); //! ``` use crate::encoding::{EncodeExemplarValue, EncodeLabelSet}; @@ -33,15 +46,140 @@ use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Write; +/// Encode both the metrics registered with the provided [`Registry`] and the +/// EOF marker into the provided [`Write`]r using the OpenMetrics text format. +/// +/// Note: This function encodes the **complete** OpenMetrics exposition. +/// +/// Use [`encode_registry`] or [`encode_eof`] if partial encoding is needed. +/// +/// See [OpenMetrics exposition format](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#text-format) +/// for additional details. +/// +/// # Examples +/// +/// ```no_run +/// # use prometheus_client::encoding::text::encode; +/// # use prometheus_client::metrics::counter::Counter; +/// # use prometheus_client::metrics::gauge::Gauge; +/// # use prometheus_client::registry::Registry; +/// # +/// // Initialize registry with metric families +/// let mut registry = Registry::default(); +/// let counter: Counter = Counter::default(); +/// registry.register( +/// "my_counter", +/// "This is my counter", +/// counter.clone(), +/// ); +/// let gauge: Gauge = Gauge::default(); +/// registry.register( +/// "my_gauge", +/// "This is my gauge", +/// gauge.clone(), +/// ); +/// +/// // Encode the complete OpenMetrics exposition into the buffer +/// let mut buffer = String::new(); +/// encode(&mut buffer, ®istry)?; +/// # Ok::<(), std::fmt::Error>(()) +/// ``` +pub fn encode<W>(writer: &mut W, registry: &Registry) -> Result<(), std::fmt::Error> +where + W: Write, +{ + encode_registry(writer, registry)?; + encode_eof(writer) +} + /// Encode the metrics registered with the provided [`Registry`] into the /// provided [`Write`]r using the OpenMetrics text format. -pub fn encode<W>(writer: &mut W, registry: &Registry) -> Result<(), std::fmt::Error> +/// +/// Note: The OpenMetrics exposition requires that a complete message must end +/// with an EOF marker. +/// +/// This function may be called repeatedly for the HTTP scrape response until +/// [`encode_eof`] signals the end of the response. +/// +/// This may also be used to compose a partial message with metrics assembled +/// from multiple registries. +/// +/// # Examples +/// +/// ```no_run +/// # use prometheus_client::encoding::text::encode_registry; +/// # use prometheus_client::metrics::counter::Counter; +/// # use prometheus_client::metrics::gauge::Gauge; +/// # use prometheus_client::registry::Registry; +/// # +/// // Initialize registry with a counter +/// let mut reg_counter = Registry::default(); +/// let counter: Counter = Counter::default(); +/// reg_counter.register( +/// "my_counter", +/// "This is my counter", +/// counter.clone(), +/// ); +/// +/// // Encode the counter registry into the buffer +/// let mut buffer = String::new(); +/// encode_registry(&mut buffer, ®_counter)?; +/// +/// // Initialize another registry but with a gauge +/// let mut reg_gauge = Registry::default(); +/// let gauge: Gauge = Gauge::default(); +/// reg_gauge.register( +/// "my_gauge", +/// "This is my gauge", +/// gauge.clone(), +/// ); +/// +/// // Encode the gauge registry into the buffer +/// encode_registry(&mut buffer, ®_gauge)?; +/// # Ok::<(), std::fmt::Error>(()) +/// ``` +pub fn encode_registry<W>(writer: &mut W, registry: &Registry) -> Result<(), std::fmt::Error> where W: Write, { - registry.encode(&mut DescriptorEncoder::new(writer).into())?; - writer.write_str("# EOF\n")?; - Ok(()) + registry.encode(&mut DescriptorEncoder::new(writer).into()) +} + +/// Encode the EOF marker into the provided [`Write`]r using the OpenMetrics +/// text format. +/// +/// Note: This function is used to mark/signal the end of the exposition. +/// +/// # Examples +/// +/// ```no_run +/// # use prometheus_client::encoding::text::{encode_registry, encode_eof}; +/// # use prometheus_client::metrics::counter::Counter; +/// # use prometheus_client::metrics::gauge::Gauge; +/// # use prometheus_client::registry::Registry; +/// # +/// // Initialize registry with a counter +/// let mut registry = Registry::default(); +/// let counter: Counter = Counter::default(); +/// registry.register( +/// "my_counter", +/// "This is my counter", +/// counter.clone(), +/// ); +/// +/// // Encode registry into the buffer +/// let mut buffer = String::new(); +/// encode_registry(&mut buffer, ®istry)?; +/// +/// // Encode EOF marker to complete the message +/// encode_eof(&mut buffer)?; +/// # Ok::<(), std::fmt::Error>(()) +/// ``` +pub fn encode_eof<W>(writer: &mut W) -> Result<(), std::fmt::Error> +where + W: Write, +{ + writer.write_str("# EOF\n") } pub(crate) struct DescriptorEncoder<'a> { @@ -915,6 +1053,52 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn encode_registry_eof() { + let mut orders_registry = Registry::default(); + + let total_orders: Counter<u64> = Default::default(); + orders_registry.register("orders", "Total orders received", total_orders.clone()); + total_orders.inc(); + + let processing_times = Histogram::new(exponential_buckets(1.0, 2.0, 10)); + orders_registry.register_with_unit( + "processing_times", + "Order times", + Unit::Seconds, + processing_times.clone(), + ); + processing_times.observe(2.4); + + let mut user_auth_registry = Registry::default(); + + let successful_logins: Counter<u64> = Default::default(); + user_auth_registry.register( + "successful_logins", + "Total successful logins", + successful_logins.clone(), + ); + successful_logins.inc(); + + let failed_logins: Counter<u64> = Default::default(); + user_auth_registry.register( + "failed_logins", + "Total failed logins", + failed_logins.clone(), + ); + + let mut response = String::new(); + + encode_registry(&mut response, &orders_registry).unwrap(); + assert_eq!(&response[response.len() - 20..], "bucket{le=\"+Inf\"} 1\n"); + + encode_registry(&mut response, &user_auth_registry).unwrap(); + assert_eq!(&response[response.len() - 20..], "iled_logins_total 0\n"); + + encode_eof(&mut response).unwrap(); + assert_eq!(&response[response.len() - 20..], "ogins_total 0\n# EOF\n"); + } + fn parse_with_python_client(input: String) { pyo3::prepare_freethreaded_python(); From 0f85e8eb84a9faa3c60585f64eb452c4228d0f55 Mon Sep 17 00:00:00 2001 From: Max Inden <mail@max-inden.de> Date: Mon, 15 Jul 2024 16:35:40 +0200 Subject: [PATCH 06/27] docs: enable for protobuf feature (#208) - have docs.rs generate with `protobuf` feature - flag that only enabled with `protobuf` feature Signed-off-by: Max Inden <mail@max-inden.de> --- Cargo.toml | 5 +++++ src/encoding.rs | 1 + src/lib.rs | 1 + 3 files changed, 7 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index fae43aa5..4f78e1ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,3 +61,8 @@ name = "proto" path = "benches/encoding/proto.rs" harness = false required-features = ["protobuf"] + +# Passing arguments to the docsrs builder in order to properly document cfg's. +# More information: https://docs.rs/about/builds#cross-compiling +[package.metadata.docs.rs] +all-features = true diff --git a/src/encoding.rs b/src/encoding.rs index 23c60328..30993999 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -13,6 +13,7 @@ use std::rc::Rc; use std::sync::Arc; #[cfg(feature = "protobuf")] +#[cfg_attr(docsrs, doc(cfg(feature = "protobuf")))] pub mod protobuf; pub mod text; diff --git a/src/lib.rs b/src/lib.rs index cf39dc1a..cfff6238 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ #![deny(unused)] #![forbid(unsafe_code)] #![warn(missing_debug_implementations)] +#![cfg_attr(docsrs, feature(doc_cfg))] //! Client library implementation of the [Open Metrics //! specification](https://github.com/OpenObservability/OpenMetrics). Allows From 7f570f3f6057be5bdab3da4c817ff8b1a962c483 Mon Sep 17 00:00:00 2001 From: Max Inden <mail@max-inden.de> Date: Wed, 17 Jul 2024 08:45:24 +0200 Subject: [PATCH 07/27] chore: prepare v0.22.3 (#209) Signed-off-by: Max Inden <mail@max-inden.de> --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6eed664..00af9688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.22.3] - unreleased +## [0.22.3] ### Added - Added `encode_registry` and `encode_eof` functions to `text` module. See [PR 205]. - + [PR 205]: https://github.com/prometheus/client_rust/pull/205 - Support all platforms with 32 bit atomics lacking 64 bit atomics. From 24a8ada9108ab9d41c725139f6f5b3769bb8a762 Mon Sep 17 00:00:00 2001 From: Max Inden <mail@max-inden.de> Date: Wed, 17 Jul 2024 08:57:15 +0200 Subject: [PATCH 08/27] chore(deps): update prost dependencies to v0.12 (#210) Signed-off-by: koushiro <koushiro.cqx@gmail.com> Signed-off-by: Max Inden <mail@max-inden.de> Co-authored-by: koushiro <koushiro.cqx@gmail.com> --- CHANGELOG.md | 7 +++++++ Cargo.toml | 10 +++++----- src/encoding/text.rs | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00af9688..5fa5ea86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.23.0] - unreleased + +- Update `prost` dependencies to `v0.12`. + See [PR 198]. + +[PR 198]: https://github.com/prometheus/client_rust/pull/198 + ## [0.22.3] ### Added diff --git a/Cargo.toml b/Cargo.toml index 4f78e1ef..2c1f8892 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus-client" -version = "0.22.3" +version = "0.23.0" authors = ["Max Inden <mail@max-inden.de>"] edition = "2021" description = "Open Metrics client library allowing users to natively instrument applications." @@ -22,14 +22,14 @@ dtoa = "1.0" itoa = "1.0" parking_lot = "0.12" prometheus-client-derive-encode = { version = "0.4.1", path = "derive-encode" } -prost = { version = "0.11.0", optional = true } -prost-types = { version = "0.11.0", optional = true } +prost = { version = "0.12.0", optional = true } +prost-types = { version = "0.12.0", optional = true } [dev-dependencies] async-std = { version = "1", features = ["attributes"] } criterion = "0.5" http-types = "2" -pyo3 = "0.20" +pyo3 = "0.21" quickcheck = "1" rand = "0.8.4" tide = "0.16" @@ -40,7 +40,7 @@ hyper-util = { version = "0.1.3", features = ["tokio"] } http-body-util = "0.1.1" [build-dependencies] -prost-build = { version = "0.11.0", optional = true } +prost-build = { version = "0.12.0", optional = true } [[bench]] name = "baseline" diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 42815269..bf65aa06 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -1104,7 +1104,7 @@ mod tests { println!("{:?}", input); Python::with_gil(|py| { - let parser = PyModule::from_code( + let parser = PyModule::from_code_bound( py, r#" from prometheus_client.openmetrics.parser import text_string_to_metric_families From 87442a202dc73cbe730d8808f4f6682ef6dbc4ae Mon Sep 17 00:00:00 2001 From: Orne Brocaar <info@brocaar.com> Date: Wed, 17 Jul 2024 08:27:11 +0100 Subject: [PATCH 09/27] feat(encoding): add impls for `Encode*Value` (#173) Signed-off-by: Orne Brocaar <info@brocaar.com> Signed-off-by: Max Inden <mail@max-inden.de> Co-authored-by: Max Inden <mail@max-inden.de> --- CHANGELOG.md | 5 +++++ src/collector.rs | 2 +- src/encoding.rs | 36 ++++++++++++++++++++++++++++++++++++ src/encoding/text.rs | 2 +- src/registry.rs | 2 +- 5 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa5ea86..448d179f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [PR 198]: https://github.com/prometheus/client_rust/pull/198 +- Add `EncodeGaugeValue` `i32` and `f32`, `EncodeCounterValue` `u32` and `f32` and `EncodeExemplarValue` `f32` and `u32` implementations. + See [PR 173]. + +[PR 173]: https://github.com/prometheus/client_rust/pull/173 + ## [0.22.3] ### Added diff --git a/src/collector.rs b/src/collector.rs index eb5bdeb0..66270c41 100644 --- a/src/collector.rs +++ b/src/collector.rs @@ -23,7 +23,7 @@ use crate::encoding::DescriptorEncoder; /// /// impl Collector for MyCollector { /// fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> { -/// let counter = ConstCounter::new(42); +/// let counter = ConstCounter::new(42u64); /// let metric_encoder = encoder.encode_descriptor( /// "my_counter", /// "some help", diff --git a/src/encoding.rs b/src/encoding.rs index 30993999..c206a1c9 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -563,6 +563,18 @@ impl EncodeGaugeValue for f64 { } } +impl EncodeGaugeValue for i32 { + fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_i64(*self as i64) + } +} + +impl EncodeGaugeValue for f32 { + fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_f64(*self as f64) + } +} + /// Encoder for a gauge value. #[derive(Debug)] pub struct GaugeValueEncoder<'a>(GaugeValueEncoderInner<'a>); @@ -619,6 +631,18 @@ impl EncodeCounterValue for f64 { } } +impl EncodeCounterValue for u32 { + fn encode(&self, encoder: &mut CounterValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_u64(*self as u64) + } +} + +impl EncodeCounterValue for f32 { + fn encode(&self, encoder: &mut CounterValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_f64(*self as f64) + } +} + /// Encoder for a counter value. #[derive(Debug)] pub struct CounterValueEncoder<'a>(CounterValueEncoderInner<'a>); @@ -658,6 +682,18 @@ impl EncodeExemplarValue for u64 { } } +impl EncodeExemplarValue for f32 { + fn encode(&self, mut encoder: ExemplarValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode(*self as f64) + } +} + +impl EncodeExemplarValue for u32 { + fn encode(&self, mut encoder: ExemplarValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode(*self as f64) + } +} + impl<'a> From<text::CounterValueEncoder<'a>> for CounterValueEncoder<'a> { fn from(e: text::CounterValueEncoder<'a>) -> Self { CounterValueEncoder(CounterValueEncoderInner::Text(e)) diff --git a/src/encoding/text.rs b/src/encoding/text.rs index bf65aa06..216ddde0 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -1014,7 +1014,7 @@ mod tests { &self, mut encoder: crate::encoding::DescriptorEncoder, ) -> Result<(), std::fmt::Error> { - let counter = crate::metrics::counter::ConstCounter::new(42); + let counter = crate::metrics::counter::ConstCounter::new(42u64); let metric_encoder = encoder.encode_descriptor( &self.name, "some help", diff --git a/src/registry.rs b/src/registry.rs index d2bb1972..c7dec3ba 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -185,7 +185,7 @@ impl Registry { /// /// impl Collector for MyCollector { /// fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> { - /// let counter = ConstCounter::new(42); + /// let counter = ConstCounter::new(42u64); /// let metric_encoder = encoder.encode_descriptor( /// "my_counter", /// "some help", From 4216fa6f6f040a6ec73c2e391ea855725ee63854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Gillot-Lamure?= <leo.gillot@navaati.net> Date: Wed, 17 Jul 2024 20:44:20 +0200 Subject: [PATCH 10/27] chore: add missing tests for u32 i32 that already work in metrics (#215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Léo Gillot-Lamure <leo.gillot@navaati.net> --- src/encoding/text.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 216ddde0..7cf22e4f 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -709,7 +709,7 @@ mod tests { use crate::metrics::{counter::Counter, exemplar::CounterWithExemplar}; use pyo3::{prelude::*, types::PyModule}; use std::borrow::Cow; - use std::sync::atomic::AtomicU32; + use std::sync::atomic::{AtomicI32, AtomicU32}; #[test] fn encode_counter() { @@ -717,6 +717,9 @@ mod tests { let mut registry = Registry::default(); registry.register("my_counter", "My counter", counter); + let counter_u32 = Counter::<u32, AtomicU32>::default(); + registry.register("u32_counter", "Counter::<u32, AtomicU32>", counter_u32); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); @@ -780,6 +783,9 @@ mod tests { let gauge = Gauge::<u32, AtomicU32>::default(); registry.register("u32_gauge", "Gauge::<u32, AtomicU32>", gauge); + let gauge_i32 = Gauge::<i32, AtomicI32>::default(); + registry.register("i32_gauge", "Gauge::<i32, AtomicU32>", gauge_i32); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); From aeca8d8955e8a6c91e71c46b362b4cfb456cd32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Gillot-Lamure?= <leo.gillot@navaati.net> Date: Wed, 17 Jul 2024 20:58:38 +0200 Subject: [PATCH 11/27] feat(metrics): add support of f32 for Gauge and Counter (#216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Léo Gillot-Lamure <leo.gillot@navaati.net> Signed-off-by: Max Inden <mail@max-inden.de> Co-authored-by: Max Inden <mail@max-inden.de> --- CHANGELOG.md | 12 +++++++++-- src/encoding/text.rs | 6 +++++- src/metrics/counter.rs | 25 ++++++++++++++++++++++ src/metrics/gauge.rs | 48 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 448d179f..3930fccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.23.0] - unreleased +### Changed + +- `ConstCounter::new` now requires specifying the type of literal arguments, like this: `ConstCounter::new(42u64);`. + See [PR 173]. + - Update `prost` dependencies to `v0.12`. See [PR 198]. [PR 198]: https://github.com/prometheus/client_rust/pull/198 -- Add `EncodeGaugeValue` `i32` and `f32`, `EncodeCounterValue` `u32` and `f32` and `EncodeExemplarValue` `f32` and `u32` implementations. - See [PR 173]. +### Added + +- Support `i32`/`f32` for `Gauge` and `u32`/`f32` for `Counter`/`CounterWithExemplar`. + See [PR 173] and [PR 216]. [PR 173]: https://github.com/prometheus/client_rust/pull/173 +[PR 216]: https://github.com/prometheus/client_rust/pull/216 ## [0.22.3] diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 7cf22e4f..955396a6 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -717,9 +717,10 @@ mod tests { let mut registry = Registry::default(); registry.register("my_counter", "My counter", counter); + let counter_f32 = Counter::<f32, AtomicU32>::default(); + registry.register("f32_counter", "Counter::<f32, AtomicU32>", counter_f32); let counter_u32 = Counter::<u32, AtomicU32>::default(); registry.register("u32_counter", "Counter::<u32, AtomicU32>", counter_u32); - let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); @@ -783,6 +784,9 @@ mod tests { let gauge = Gauge::<u32, AtomicU32>::default(); registry.register("u32_gauge", "Gauge::<u32, AtomicU32>", gauge); + let gauge_f32 = Gauge::<f32, AtomicU32>::default(); + registry.register("f32_gauge", "Gauge::<f32, AtomicU32>", gauge_f32); + let gauge_i32 = Gauge::<i32, AtomicI32>::default(); registry.register("i32_gauge", "Gauge::<i32, AtomicU32>", gauge_i32); diff --git a/src/metrics/counter.rs b/src/metrics/counter.rs index 63d84ffc..d1fee30f 100644 --- a/src/metrics/counter.rs +++ b/src/metrics/counter.rs @@ -169,6 +169,31 @@ impl Atomic<f64> for AtomicU64 { } } +impl Atomic<f32> for AtomicU32 { + fn inc(&self) -> f32 { + self.inc_by(1.0) + } + + fn inc_by(&self, v: f32) -> f32 { + let mut old_u32 = self.load(Ordering::Relaxed); + let mut old_f32; + loop { + old_f32 = f32::from_bits(old_u32); + let new = f32::to_bits(old_f32 + v); + match self.compare_exchange_weak(old_u32, new, Ordering::Relaxed, Ordering::Relaxed) { + Ok(_) => break, + Err(x) => old_u32 = x, + } + } + + old_f32 + } + + fn get(&self) -> f32 { + f32::from_bits(self.load(Ordering::Relaxed)) + } +} + impl<N, A> TypedMetric for Counter<N, A> { const TYPE: MetricType = MetricType::Counter; } diff --git a/src/metrics/gauge.rs b/src/metrics/gauge.rs index fcf7e6a7..5c8bef9b 100644 --- a/src/metrics/gauge.rs +++ b/src/metrics/gauge.rs @@ -262,6 +262,54 @@ impl Atomic<f64> for AtomicU64 { } } +impl Atomic<f32> for AtomicU32 { + fn inc(&self) -> f32 { + self.inc_by(1.0) + } + + fn inc_by(&self, v: f32) -> f32 { + let mut old_u32 = self.load(Ordering::Relaxed); + let mut old_f32; + loop { + old_f32 = f32::from_bits(old_u32); + let new = f32::to_bits(old_f32 + v); + match self.compare_exchange_weak(old_u32, new, Ordering::Relaxed, Ordering::Relaxed) { + Ok(_) => break, + Err(x) => old_u32 = x, + } + } + + old_f32 + } + + fn dec(&self) -> f32 { + self.dec_by(1.0) + } + + fn dec_by(&self, v: f32) -> f32 { + let mut old_u32 = self.load(Ordering::Relaxed); + let mut old_f32; + loop { + old_f32 = f32::from_bits(old_u32); + let new = f32::to_bits(old_f32 - v); + match self.compare_exchange_weak(old_u32, new, Ordering::Relaxed, Ordering::Relaxed) { + Ok(_) => break, + Err(x) => old_u32 = x, + } + } + + old_f32 + } + + fn set(&self, v: f32) -> f32 { + f32::from_bits(self.swap(f32::to_bits(v), Ordering::Relaxed)) + } + + fn get(&self) -> f32 { + f32::from_bits(self.load(Ordering::Relaxed)) + } +} + impl<N, A> TypedMetric for Gauge<N, A> { const TYPE: MetricType = MetricType::Gauge; } From 53f15ec2f9e2d5cf7df548bdae362c7c98aaf024 Mon Sep 17 00:00:00 2001 From: flaneur <me.ssword@gmail.com> Date: Mon, 29 Jul 2024 15:00:29 +0800 Subject: [PATCH 12/27] feat: allow Arc<String> on EncodeLabelSet (#217) Signed-off-by: Li Yazhou <me.ssword@gmail.com> --- CHANGELOG.md | 4 ++++ derive-encode/tests/lib.rs | 32 ++++++++++++++++++++++++++++++++ src/encoding.rs | 7 +++++++ 3 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3930fccb..32de9b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support `i32`/`f32` for `Gauge` and `u32`/`f32` for `Counter`/`CounterWithExemplar`. See [PR 173] and [PR 216]. +- Supoort `Arc<String>` for `EncodeLabelValue`. + See [PR 217]. + [PR 173]: https://github.com/prometheus/client_rust/pull/173 [PR 216]: https://github.com/prometheus/client_rust/pull/216 +[PR 217]: https://github.com/prometheus/client_rust/pull/217 ## [0.22.3] diff --git a/derive-encode/tests/lib.rs b/derive-encode/tests/lib.rs index fba8412d..5c7eb54d 100644 --- a/derive-encode/tests/lib.rs +++ b/derive-encode/tests/lib.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use prometheus_client::encoding::text::encode; use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue}; use prometheus_client::metrics::counter::Counter; @@ -137,6 +139,36 @@ fn remap_keyword_identifiers() { assert_eq!(expected, buffer); } +#[test] +fn arc_string() { + #[derive(EncodeLabelSet, Hash, Clone, Eq, PartialEq, Debug)] + struct Labels { + client_id: Arc<String>, + } + + let mut registry = Registry::default(); + let family = Family::<Labels, Counter>::default(); + registry.register("my_counter", "This is my counter", family.clone()); + + // Record a single HTTP GET request. + let client_id = Arc::new("client_id".to_string()); + family + .get_or_create(&Labels { + client_id: client_id.clone(), + }) + .inc(); + + // Encode all metrics in the registry in the text format. + let mut buffer = String::new(); + encode(&mut buffer, ®istry).unwrap(); + + let expected = "# HELP my_counter This is my counter.\n".to_owned() + + "# TYPE my_counter counter\n" + + "my_counter_total{client_id=\"client_id\"} 1\n" + + "# EOF\n"; + assert_eq!(expected, buffer); +} + #[test] fn flatten() { #[derive(EncodeLabelSet, Hash, Clone, Eq, PartialEq, Debug)] diff --git a/src/encoding.rs b/src/encoding.rs index c206a1c9..c9c9a838 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -468,12 +468,19 @@ impl EncodeLabelValue for &str { Ok(()) } } + impl EncodeLabelValue for String { fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { EncodeLabelValue::encode(&self.as_str(), encoder) } } +impl EncodeLabelValue for &String { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + EncodeLabelValue::encode(&self.as_str(), encoder) + } +} + impl<'a> EncodeLabelValue for Cow<'a, str> { fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { EncodeLabelValue::encode(&self.as_ref(), encoder) From 3e6579ba36c37c108869df27bf6fd29355a7cbef Mon Sep 17 00:00:00 2001 From: Jerry Wang <j3rry.wan9@gmail.com> Date: Sun, 4 Aug 2024 16:40:23 +0800 Subject: [PATCH 13/27] chore(examples): add `axum` example (#219) Signed-off-by: Jerry Wang <j3rry.wan9@gmail.com> --- Cargo.toml | 1 + examples/axum.rs | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 examples/axum.rs diff --git a/Cargo.toml b/Cargo.toml index 2c1f8892..b9e93380 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ prost-types = { version = "0.12.0", optional = true } [dev-dependencies] async-std = { version = "1", features = ["attributes"] } +axum = "0.7" criterion = "0.5" http-types = "2" pyo3 = "0.21" diff --git a/examples/axum.rs b/examples/axum.rs new file mode 100644 index 00000000..bf852559 --- /dev/null +++ b/examples/axum.rs @@ -0,0 +1,88 @@ +use axum::body::Body; +use axum::extract::State; +use axum::http::header::CONTENT_TYPE; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use axum::Router; +use prometheus_client::encoding::text::encode; +use prometheus_client::metrics::counter::Counter; +use prometheus_client::metrics::family::Family; +use prometheus_client::registry::Registry; +use prometheus_client_derive_encode::{EncodeLabelSet, EncodeLabelValue}; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] +pub enum Method { + Get, + Post, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] +pub struct MethodLabels { + pub method: Method, +} + +#[derive(Debug)] +pub struct Metrics { + requests: Family<MethodLabels, Counter>, +} + +impl Metrics { + pub fn inc_requests(&self, method: Method) { + self.requests.get_or_create(&MethodLabels { method }).inc(); + } +} + +#[derive(Debug)] +pub struct AppState { + pub registry: Registry, +} + +pub async fn metrics_handler(State(state): State<Arc<Mutex<AppState>>>) -> impl IntoResponse { + let state = state.lock().await; + let mut buffer = String::new(); + encode(&mut buffer, &state.registry).unwrap(); + + Response::builder() + .status(StatusCode::OK) + .header( + CONTENT_TYPE, + "application/openmetrics-text; version=1.0.0; charset=utf-8", + ) + .body(Body::from(buffer)) + .unwrap() +} + +pub async fn some_handler(State(metrics): State<Arc<Mutex<Metrics>>>) -> impl IntoResponse { + metrics.lock().await.inc_requests(Method::Get); + "okay".to_string() +} + +#[tokio::main] +async fn main() { + let metrics = Metrics { + requests: Family::default(), + }; + let mut state = AppState { + registry: Registry::default(), + }; + state + .registry + .register("requests", "Count of requests", metrics.requests.clone()); + let metrics = Arc::new(Mutex::new(metrics)); + let state = Arc::new(Mutex::new(state)); + + let router = Router::new() + .route("/metrics", get(metrics_handler)) + .with_state(state) + .route("/handler", get(some_handler)) + .with_state(metrics); + let port = 8080; + let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) + .await + .unwrap(); + + axum::serve(listener, router).await.unwrap(); +} From 0c3e89ea1ea4cf07ca92dc0200721709a194d53d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:38:39 +0200 Subject: [PATCH 14/27] build(deps): update pyo3 requirement from 0.21 to 0.22 (#213) Updates the requirements on [pyo3](https://github.com/pyo3/pyo3) to permit the latest version. - [Release notes](https://github.com/pyo3/pyo3/releases) - [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md) - [Commits](https://github.com/pyo3/pyo3/compare/v0.21.0...v0.22.1) --- updated-dependencies: - dependency-name: pyo3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b9e93380..5514ab68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ async-std = { version = "1", features = ["attributes"] } axum = "0.7" criterion = "0.5" http-types = "2" -pyo3 = "0.21" +pyo3 = "0.22" quickcheck = "1" rand = "0.8.4" tide = "0.16" From ad05f0ff2ea6b2802a306dc9686389f29eb3b25a Mon Sep 17 00:00:00 2001 From: Max Inden <mail@max-inden.de> Date: Fri, 6 Sep 2024 12:35:57 +0200 Subject: [PATCH 15/27] fix(encoding): correctly handle empty family labels (#224) Signed-off-by: Tyler Levine <tyler.levine@falconx.io> Signed-off-by: Max Inden <mail@max-inden.de> --- CHANGELOG.md | 7 ++++ examples/custom-metric.rs | 4 +-- src/encoding.rs | 6 +++- src/encoding/text.rs | 69 ++++++++++++++++++++++++++++++++++----- src/metrics/counter.rs | 6 ++-- src/metrics/histogram.rs | 4 +-- 6 files changed, 79 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32de9b95..66eb579d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [PR 216]: https://github.com/prometheus/client_rust/pull/216 [PR 217]: https://github.com/prometheus/client_rust/pull/217 +### Fixed + +- Don't prepend `,` when encoding empty family label set. + See [PR 175]. + +[PR 175]: https://github.com/prometheus/client_rust/pull/175 + ## [0.22.3] ### Added diff --git a/examples/custom-metric.rs b/examples/custom-metric.rs index 9ad17a5a..a61ff8a7 100644 --- a/examples/custom-metric.rs +++ b/examples/custom-metric.rs @@ -1,4 +1,4 @@ -use prometheus_client::encoding::{text::encode, EncodeMetric, MetricEncoder}; +use prometheus_client::encoding::{text::encode, EncodeMetric, MetricEncoder, NoLabelSet}; use prometheus_client::metrics::MetricType; use prometheus_client::registry::Registry; @@ -20,7 +20,7 @@ impl EncodeMetric for MyCustomMetric { // E.g. every CPU cycle spend in this method delays the response send to // the Prometheus server. - encoder.encode_counter::<(), _, u64>(&rand::random::<u64>(), None) + encoder.encode_counter::<NoLabelSet, _, u64>(&rand::random::<u64>(), None) } fn metric_type(&self) -> prometheus_client::metrics::MetricType { diff --git a/src/encoding.rs b/src/encoding.rs index c9c9a838..c644f82b 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -247,6 +247,10 @@ pub trait EncodeLabel { #[derive(Debug)] pub struct LabelEncoder<'a>(LabelEncoderInner<'a>); +/// Uninhabited type to represent the lack of a label set for a metric +#[derive(Debug)] +pub enum NoLabelSet {} + #[derive(Debug)] enum LabelEncoderInner<'a> { Text(text::LabelEncoder<'a>), @@ -352,7 +356,7 @@ impl<T: EncodeLabel> EncodeLabelSet for Vec<T> { } } -impl EncodeLabelSet for () { +impl EncodeLabelSet for NoLabelSet { fn encode(&self, _encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { Ok(()) } diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 955396a6..4946fe35 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -37,7 +37,7 @@ //! assert_eq!(expected_msg, buffer); //! ``` -use crate::encoding::{EncodeExemplarValue, EncodeLabelSet}; +use crate::encoding::{EncodeExemplarValue, EncodeLabelSet, NoLabelSet}; use crate::metrics::exemplar::Exemplar; use crate::metrics::MetricType; use crate::registry::{Prefix, Registry, Unit}; @@ -324,7 +324,7 @@ impl<'a> MetricEncoder<'a> { self.write_suffix("total")?; - self.encode_labels::<()>(None)?; + self.encode_labels::<NoLabelSet>(None)?; v.encode( &mut CounterValueEncoder { @@ -348,7 +348,7 @@ impl<'a> MetricEncoder<'a> { ) -> Result<(), std::fmt::Error> { self.write_prefix_name_unit()?; - self.encode_labels::<()>(None)?; + self.encode_labels::<NoLabelSet>(None)?; v.encode( &mut GaugeValueEncoder { @@ -404,14 +404,14 @@ impl<'a> MetricEncoder<'a> { ) -> Result<(), std::fmt::Error> { self.write_prefix_name_unit()?; self.write_suffix("sum")?; - self.encode_labels::<()>(None)?; + self.encode_labels::<NoLabelSet>(None)?; self.writer.write_str(" ")?; self.writer.write_str(dtoa::Buffer::new().format(sum))?; self.newline()?; self.write_prefix_name_unit()?; self.write_suffix("count")?; - self.encode_labels::<()>(None)?; + self.encode_labels::<NoLabelSet>(None)?; self.writer.write_str(" ")?; self.writer.write_str(itoa::Buffer::new().format(count))?; self.newline()?; @@ -512,12 +512,37 @@ impl<'a> MetricEncoder<'a> { additional_labels.encode(LabelSetEncoder::new(self.writer).into())?; } - if let Some(labels) = &self.family_labels { - if !self.const_labels.is_empty() || additional_labels.is_some() { - self.writer.write_str(",")?; + /// Writer impl which prepends a comma on the first call to write output to the wrapped writer + struct CommaPrependingWriter<'a> { + writer: &'a mut dyn Write, + should_prepend: bool, + } + + impl Write for CommaPrependingWriter<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + if self.should_prepend { + self.writer.write_char(',')?; + self.should_prepend = false; + } + self.writer.write_str(s) } + } - labels.encode(LabelSetEncoder::new(self.writer).into())?; + if let Some(labels) = self.family_labels { + // if const labels or additional labels have been written, a comma must be prepended before writing the family labels. + // However, it could be the case that the family labels are `Some` and yet empty, so the comma should _only_ + // be prepended if one of the `Write` methods are actually called when attempting to write the family labels. + // Therefore, wrap the writer on `Self` with a CommaPrependingWriter if other labels have been written and + // there may be a need to prepend an extra comma before writing additional labels. + if !self.const_labels.is_empty() || additional_labels.is_some() { + let mut writer = CommaPrependingWriter { + writer: self.writer, + should_prepend: true, + }; + labels.encode(LabelSetEncoder::new(&mut writer).into())?; + } else { + labels.encode(LabelSetEncoder::new(self.writer).into())?; + }; } self.writer.write_str("}")?; @@ -709,6 +734,7 @@ mod tests { use crate::metrics::{counter::Counter, exemplar::CounterWithExemplar}; use pyo3::{prelude::*, types::PyModule}; use std::borrow::Cow; + use std::fmt::Error; use std::sync::atomic::{AtomicI32, AtomicU32}; #[test] @@ -899,6 +925,31 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn encode_histogram_family_with_empty_struct_family_labels() { + let mut registry = Registry::default(); + let family = + Family::new_with_constructor(|| Histogram::new(exponential_buckets(1.0, 2.0, 10))); + registry.register("my_histogram", "My histogram", family.clone()); + + #[derive(Eq, PartialEq, Hash, Debug, Clone)] + struct EmptyLabels {} + + impl EncodeLabelSet for EmptyLabels { + fn encode(&self, _encoder: crate::encoding::LabelSetEncoder) -> Result<(), Error> { + Ok(()) + } + } + + family.get_or_create(&EmptyLabels {}).observe(1.0); + + let mut encoded = String::new(); + + encode(&mut encoded, ®istry).unwrap(); + + parse_with_python_client(encoded); + } + #[test] fn encode_histogram_with_exemplars() { let mut registry = Registry::default(); diff --git a/src/metrics/counter.rs b/src/metrics/counter.rs index d1fee30f..7016c361 100644 --- a/src/metrics/counter.rs +++ b/src/metrics/counter.rs @@ -2,7 +2,7 @@ //! //! See [`Counter`] for details. -use crate::encoding::{EncodeMetric, MetricEncoder}; +use crate::encoding::{EncodeMetric, MetricEncoder, NoLabelSet}; use super::{MetricType, TypedMetric}; use std::marker::PhantomData; @@ -204,7 +204,7 @@ where A: Atomic<N>, { fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { - encoder.encode_counter::<(), _, u64>(&self.get(), None) + encoder.encode_counter::<NoLabelSet, _, u64>(&self.get(), None) } fn metric_type(&self) -> MetricType { @@ -236,7 +236,7 @@ where N: crate::encoding::EncodeCounterValue, { fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { - encoder.encode_counter::<(), _, u64>(&self.value, None) + encoder.encode_counter::<NoLabelSet, _, u64>(&self.value, None) } fn metric_type(&self) -> MetricType { diff --git a/src/metrics/histogram.rs b/src/metrics/histogram.rs index 66f4496d..1e418ad9 100644 --- a/src/metrics/histogram.rs +++ b/src/metrics/histogram.rs @@ -2,7 +2,7 @@ //! //! See [`Histogram`] for details. -use crate::encoding::{EncodeMetric, MetricEncoder}; +use crate::encoding::{EncodeMetric, MetricEncoder, NoLabelSet}; use super::{MetricType, TypedMetric}; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; @@ -133,7 +133,7 @@ pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator<Item impl EncodeMetric for Histogram { fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { let (sum, count, buckets) = self.get(); - encoder.encode_histogram::<()>(sum, count, &buckets, None) + encoder.encode_histogram::<NoLabelSet>(sum, count, &buckets, None) } fn metric_type(&self) -> MetricType { From 12923ca39c6392dcc7d0e81af1acc3788c4235d4 Mon Sep 17 00:00:00 2001 From: Ivan Babrou <github@ivan.computer> Date: Wed, 16 Oct 2024 03:40:16 -0700 Subject: [PATCH 16/27] fix(metrics/gauge): implement Atomic<u64> for AtomicU64 (#226) Between forcing end users to do endless `as i64` for things that are clearly `u64` and having one error case for rarely used protobuf when a gauge is set to `u64::MAX`, the latter seems like the right choice. Signed-off-by: Ivan Babrou <github@ivan.computer> --- CHANGELOG.md | 5 +++++ src/encoding.rs | 13 +++++++++++++ src/encoding/protobuf.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/metrics/gauge.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66eb579d..c398e073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.23.0] - unreleased + ### Changed - `ConstCounter::new` now requires specifying the type of literal arguments, like this: `ConstCounter::new(42u64);`. @@ -14,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update `prost` dependencies to `v0.12`. See [PR 198]. +- Implement `Atomic<u64>` for `AtomicU64` for gauges. + See [PR 226]. + +[PR 226]: https://github.com/prometheus/client_rust/pull/198 [PR 198]: https://github.com/prometheus/client_rust/pull/198 ### Added diff --git a/src/encoding.rs b/src/encoding.rs index c644f82b..7f77efa0 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -568,6 +568,19 @@ impl EncodeGaugeValue for i64 { } } +impl EncodeGaugeValue for u64 { + fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error> { + // Between forcing end users to do endless as i64 for things that are + // clearly u64 and having one error case for rarely used protobuf when + // a gauge is set to u64::MAX, the latter seems like the right choice. + if *self == u64::MAX { + return Err(std::fmt::Error); + } + + encoder.encode_i64(*self as i64) + } +} + impl EncodeGaugeValue for f64 { fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error> { encoder.encode_f64(*self) diff --git a/src/encoding/protobuf.rs b/src/encoding/protobuf.rs index eef2731a..095a3dba 100644 --- a/src/encoding/protobuf.rs +++ b/src/encoding/protobuf.rs @@ -453,6 +453,7 @@ mod tests { use std::borrow::Cow; use std::collections::HashSet; use std::sync::atomic::AtomicI64; + use std::sync::atomic::AtomicU64; #[test] fn encode_counter_int() { @@ -600,6 +601,43 @@ mod tests { } } + #[test] + fn encode_gauge_u64_normal() { + let mut registry = Registry::default(); + let gauge = Gauge::<u64, AtomicU64>::default(); + registry.register("my_gauge", "My gauge", gauge.clone()); + gauge.set(12345); + + let metric_set = encode(®istry).unwrap(); + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_gauge", family.name); + assert_eq!("My gauge.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Gauge as i32, + extract_metric_type(&metric_set) + ); + + match extract_metric_point_value(&metric_set) { + openmetrics_data_model::metric_point::Value::GaugeValue(value) => { + let expected = openmetrics_data_model::gauge_value::Value::IntValue(12345); + assert_eq!(Some(expected), value.value); + } + _ => panic!("wrong value type"), + } + } + + #[test] + fn encode_gauge_u64_max() { + let mut registry = Registry::default(); + let gauge = Gauge::<u64, AtomicU64>::default(); + registry.register("my_gauge", "My gauge", gauge.clone()); + gauge.set(u64::MAX); + + // This expected to fail as protobuf uses i64 and u64::MAX does not fit into it. + assert!(encode(®istry).is_err()); + } + #[test] fn encode_counter_family() { let mut registry = Registry::default(); diff --git a/src/metrics/gauge.rs b/src/metrics/gauge.rs index 5c8bef9b..abbc8cee 100644 --- a/src/metrics/gauge.rs +++ b/src/metrics/gauge.rs @@ -213,6 +213,33 @@ impl Atomic<i64> for AtomicI64 { } } +#[cfg(target_has_atomic = "64")] +impl Atomic<u64> for AtomicU64 { + fn inc(&self) -> u64 { + self.inc_by(1) + } + + fn inc_by(&self, v: u64) -> u64 { + self.fetch_add(v, Ordering::Relaxed) + } + + fn dec(&self) -> u64 { + self.dec_by(1) + } + + fn dec_by(&self, v: u64) -> u64 { + self.fetch_sub(v, Ordering::Relaxed) + } + + fn set(&self, v: u64) -> u64 { + self.swap(v, Ordering::Relaxed) + } + + fn get(&self) -> u64 { + self.load(Ordering::Relaxed) + } +} + #[cfg(target_has_atomic = "64")] impl Atomic<f64> for AtomicU64 { fn inc(&self) -> f64 { From 3e6b9e2e00c376706e5e4c2ed7cb7bcb5c9fb318 Mon Sep 17 00:00:00 2001 From: katelyn martin <me+cratelyn@katelyn.world> Date: Thu, 28 Nov 2024 15:13:27 -0500 Subject: [PATCH 17/27] chore: add `CONTRIBUTING.md` instructions (#247) the test suite in this repository has some implicit dependencies upon `protoc` and the Python client library. in order to help newcomers get situated and effectively contributing to this project, a `CONTRIBUTING.md` file is added to help provide some guidance on how to install the protocol buffer compiler, and how to create and activate a virtual environment with the Python library installed. Signed-off-by: katelyn martin <me+cratelyn@katelyn.world> --- CONTRIBUTING.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..e20a178b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing + +## Protocol Buffers + +The `build.rs` script in this library depends upon the +[Protocol Buffers compiler][protoc]. Be sure that `protoc` is installed and +available within your `PATH`. + +[protoc]: https://docs.rs/prost-build/latest/prost_build/#sourcing-protoc + +## Python Dependencies + +This repository uses the [`prometheus-client`][client-python] Python client +library in its test suite. + +You may create and activate a virtual environment with this dependency +installed by running the following shell commands from the root of this +repository: + +```shell +python -m venv ./venv +source venv/bin/activate +pip install prometheus-client +``` + +[client-python]: https://github.com/prometheus/client_python From d3e363728a40c1f9fefb6ed3003c7cec5786aef4 Mon Sep 17 00:00:00 2001 From: Qinxuan Chen <koushiro.cqx@gmail.com> Date: Fri, 29 Nov 2024 18:14:51 +0800 Subject: [PATCH 18/27] chore(ci): fix clippy warnings when using rust 1.83 (#250) Signed-off-by: koushiro <koushiro.cqx@gmail.com> --- Cargo.toml | 1 + examples/hyper.rs | 6 ++---- src/encoding.rs | 20 ++++++++++---------- src/encoding/protobuf.rs | 16 ++++++++-------- src/encoding/text.rs | 34 +++++++++++++++++----------------- 5 files changed, 38 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5514ab68..18dd395f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ prost-types = { version = "0.12.0", optional = true } async-std = { version = "1", features = ["attributes"] } axum = "0.7" criterion = "0.5" +futures = "0.3" http-types = "2" pyo3 = "0.22" quickcheck = "1" diff --git a/examples/hyper.rs b/examples/hyper.rs index 82ee121b..bbab0058 100644 --- a/examples/hyper.rs +++ b/examples/hyper.rs @@ -1,3 +1,4 @@ +use futures::future::BoxFuture; use http_body_util::{combinators, BodyExt, Full}; use hyper::{ body::{Bytes, Incoming}, @@ -8,10 +9,8 @@ use hyper::{ use hyper_util::rt::TokioIo; 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::{ @@ -69,8 +68,7 @@ type BoxBody = combinators::BoxBody<Bytes, hyper::Error>; /// This function returns a HTTP handler (i.e. another function) pub fn make_handler( registry: Arc<Registry>, -) -> impl Fn(Request<Incoming>) -> Pin<Box<dyn Future<Output = io::Result<Response<BoxBody>>> + Send>> -{ +) -> impl Fn(Request<Incoming>) -> BoxFuture<'static, io::Result<Response<BoxBody>>> { // This closure accepts a request and responds with the OpenMetrics encoding of our metrics. move |_req: Request<Incoming>| { let reg = registry.clone(); diff --git a/src/encoding.rs b/src/encoding.rs index 7f77efa0..9b2e497c 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -230,7 +230,7 @@ impl<'a> From<protobuf::LabelSetEncoder<'a>> for LabelSetEncoder<'a> { } } -impl<'a> LabelSetEncoder<'a> { +impl LabelSetEncoder<'_> { /// Encode the given label. pub fn encode_label(&mut self) -> LabelEncoder { for_both_mut!(self, LabelSetEncoderInner, e, e.encode_label().into()) @@ -271,7 +271,7 @@ impl<'a> From<protobuf::LabelEncoder<'a>> for LabelEncoder<'a> { } } -impl<'a> LabelEncoder<'a> { +impl LabelEncoder<'_> { /// Encode a label. pub fn encode_label_key(&mut self) -> Result<LabelKeyEncoder, std::fmt::Error> { for_both_mut!( @@ -313,7 +313,7 @@ impl<'a> From<protobuf::LabelKeyEncoder<'a>> for LabelKeyEncoder<'a> { } } -impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { +impl std::fmt::Write for LabelKeyEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { for_both_mut!(self, LabelKeyEncoderInner, e, e.write_str(s)) } @@ -390,7 +390,7 @@ impl EncodeLabelKey for String { } } -impl<'a> EncodeLabelKey for Cow<'a, str> { +impl EncodeLabelKey for Cow<'_, str> { fn encode(&self, encoder: &mut LabelKeyEncoder) -> Result<(), std::fmt::Error> { EncodeLabelKey::encode(&self.as_ref(), encoder) } @@ -453,14 +453,14 @@ enum LabelValueEncoderInner<'a> { Protobuf(protobuf::LabelValueEncoder<'a>), } -impl<'a> LabelValueEncoder<'a> { +impl LabelValueEncoder<'_> { /// Finish encoding the label value. pub fn finish(self) -> Result<(), std::fmt::Error> { for_both!(self, LabelValueEncoderInner, e, e.finish()) } } -impl<'a> std::fmt::Write for LabelValueEncoder<'a> { +impl std::fmt::Write for LabelValueEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { for_both_mut!(self, LabelValueEncoderInner, e, e.write_str(s)) } @@ -485,7 +485,7 @@ impl EncodeLabelValue for &String { } } -impl<'a> EncodeLabelValue for Cow<'a, str> { +impl EncodeLabelValue for Cow<'_, str> { fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { EncodeLabelValue::encode(&self.as_ref(), encoder) } @@ -610,7 +610,7 @@ enum GaugeValueEncoderInner<'a> { Protobuf(protobuf::GaugeValueEncoder<'a>), } -impl<'a> GaugeValueEncoder<'a> { +impl GaugeValueEncoder<'_> { fn encode_u32(&mut self, v: u32) -> Result<(), std::fmt::Error> { for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_u32(v)) } @@ -678,7 +678,7 @@ enum CounterValueEncoderInner<'a> { Protobuf(protobuf::CounterValueEncoder<'a>), } -impl<'a> CounterValueEncoder<'a> { +impl CounterValueEncoder<'_> { fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { for_both_mut!(self, CounterValueEncoderInner, e, e.encode_f64(v)) } @@ -742,7 +742,7 @@ enum ExemplarValueEncoderInner<'a> { Protobuf(protobuf::ExemplarValueEncoder<'a>), } -impl<'a> ExemplarValueEncoder<'a> { +impl ExemplarValueEncoder<'_> { fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { for_both_mut!(self, ExemplarValueEncoderInner, e, e.encode(v)) } diff --git a/src/encoding/protobuf.rs b/src/encoding/protobuf.rs index 095a3dba..5ec54c91 100644 --- a/src/encoding/protobuf.rs +++ b/src/encoding/protobuf.rs @@ -322,7 +322,7 @@ pub(crate) struct GaugeValueEncoder<'a> { value: &'a mut openmetrics_data_model::gauge_value::Value, } -impl<'a> GaugeValueEncoder<'a> { +impl GaugeValueEncoder<'_> { pub fn encode_u32(&mut self, v: u32) -> Result<(), std::fmt::Error> { self.encode_i64(v as i64) } @@ -343,7 +343,7 @@ pub(crate) struct ExemplarValueEncoder<'a> { value: &'a mut f64, } -impl<'a> ExemplarValueEncoder<'a> { +impl ExemplarValueEncoder<'_> { pub fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { *self.value = v; Ok(()) @@ -364,7 +364,7 @@ pub(crate) struct CounterValueEncoder<'a> { value: &'a mut openmetrics_data_model::counter_value::Total, } -impl<'a> CounterValueEncoder<'a> { +impl CounterValueEncoder<'_> { pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { *self.value = openmetrics_data_model::counter_value::Total::DoubleValue(v); Ok(()) @@ -381,7 +381,7 @@ pub(crate) struct LabelSetEncoder<'a> { labels: &'a mut Vec<openmetrics_data_model::Label>, } -impl<'a> LabelSetEncoder<'a> { +impl LabelSetEncoder<'_> { pub fn encode_label(&mut self) -> LabelEncoder { LabelEncoder { labels: self.labels, @@ -394,7 +394,7 @@ pub(crate) struct LabelEncoder<'a> { labels: &'a mut Vec<openmetrics_data_model::Label>, } -impl<'a> LabelEncoder<'a> { +impl LabelEncoder<'_> { pub fn encode_label_key(&mut self) -> Result<LabelKeyEncoder, std::fmt::Error> { self.labels.push(openmetrics_data_model::Label::default()); @@ -409,7 +409,7 @@ pub(crate) struct LabelKeyEncoder<'a> { label: &'a mut openmetrics_data_model::Label, } -impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { +impl std::fmt::Write for LabelKeyEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { self.label.name.write_str(s) } @@ -428,13 +428,13 @@ pub(crate) struct LabelValueEncoder<'a> { label_value: &'a mut String, } -impl<'a> LabelValueEncoder<'a> { +impl LabelValueEncoder<'_> { pub fn finish(self) -> Result<(), std::fmt::Error> { Ok(()) } } -impl<'a> std::fmt::Write for LabelValueEncoder<'a> { +impl std::fmt::Write for LabelValueEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { self.label_value.write_str(s) } diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 4946fe35..8f3c63df 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -188,7 +188,7 @@ pub(crate) struct DescriptorEncoder<'a> { labels: &'a [(Cow<'static, str>, Cow<'static, str>)], } -impl<'a> std::fmt::Debug for DescriptorEncoder<'a> { +impl std::fmt::Debug for DescriptorEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DescriptorEncoder").finish() } @@ -293,7 +293,7 @@ pub(crate) struct MetricEncoder<'a> { family_labels: Option<&'a dyn super::EncodeLabelSet>, } -impl<'a> std::fmt::Debug for MetricEncoder<'a> { +impl std::fmt::Debug for MetricEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut labels = String::new(); if let Some(l) = self.family_labels { @@ -310,7 +310,7 @@ impl<'a> std::fmt::Debug for MetricEncoder<'a> { } } -impl<'a> MetricEncoder<'a> { +impl MetricEncoder<'_> { pub fn encode_counter< S: EncodeLabelSet, CounterValue: super::EncodeCounterValue, @@ -555,13 +555,13 @@ pub(crate) struct CounterValueEncoder<'a> { writer: &'a mut dyn Write, } -impl<'a> std::fmt::Debug for CounterValueEncoder<'a> { +impl std::fmt::Debug for CounterValueEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CounterValueEncoder").finish() } } -impl<'a> CounterValueEncoder<'a> { +impl CounterValueEncoder<'_> { pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { self.writer.write_str(" ")?; self.writer.write_str(dtoa::Buffer::new().format(v))?; @@ -579,13 +579,13 @@ pub(crate) struct GaugeValueEncoder<'a> { writer: &'a mut dyn Write, } -impl<'a> std::fmt::Debug for GaugeValueEncoder<'a> { +impl std::fmt::Debug for GaugeValueEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GaugeValueEncoder").finish() } } -impl<'a> GaugeValueEncoder<'a> { +impl GaugeValueEncoder<'_> { pub fn encode_u32(&mut self, v: u32) -> Result<(), std::fmt::Error> { self.writer.write_str(" ")?; self.writer.write_str(itoa::Buffer::new().format(v))?; @@ -609,13 +609,13 @@ pub(crate) struct ExemplarValueEncoder<'a> { writer: &'a mut dyn Write, } -impl<'a> std::fmt::Debug for ExemplarValueEncoder<'a> { +impl std::fmt::Debug for ExemplarValueEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ExemplarValueEncoder").finish() } } -impl<'a> ExemplarValueEncoder<'a> { +impl ExemplarValueEncoder<'_> { pub fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { self.writer.write_str(dtoa::Buffer::new().format(v)) } @@ -626,7 +626,7 @@ pub(crate) struct LabelSetEncoder<'a> { first: bool, } -impl<'a> std::fmt::Debug for LabelSetEncoder<'a> { +impl std::fmt::Debug for LabelSetEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LabelSetEncoder") .field("first", &self.first) @@ -657,7 +657,7 @@ pub(crate) struct LabelEncoder<'a> { first: bool, } -impl<'a> std::fmt::Debug for LabelEncoder<'a> { +impl std::fmt::Debug for LabelEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LabelEncoder") .field("first", &self.first) @@ -665,7 +665,7 @@ impl<'a> std::fmt::Debug for LabelEncoder<'a> { } } -impl<'a> LabelEncoder<'a> { +impl LabelEncoder<'_> { pub fn encode_label_key(&mut self) -> Result<LabelKeyEncoder, std::fmt::Error> { if !self.first { self.writer.write_str(",")?; @@ -680,7 +680,7 @@ pub(crate) struct LabelKeyEncoder<'a> { writer: &'a mut dyn Write, } -impl<'a> std::fmt::Debug for LabelKeyEncoder<'a> { +impl std::fmt::Debug for LabelKeyEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LabelKeyEncoder").finish() } @@ -695,7 +695,7 @@ impl<'a> LabelKeyEncoder<'a> { } } -impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { +impl std::fmt::Write for LabelKeyEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { self.writer.write_str(s) } @@ -705,19 +705,19 @@ pub(crate) struct LabelValueEncoder<'a> { writer: &'a mut dyn Write, } -impl<'a> std::fmt::Debug for LabelValueEncoder<'a> { +impl std::fmt::Debug for LabelValueEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LabelValueEncoder").finish() } } -impl<'a> LabelValueEncoder<'a> { +impl LabelValueEncoder<'_> { pub fn finish(self) -> Result<(), std::fmt::Error> { self.writer.write_str("\"") } } -impl<'a> std::fmt::Write for LabelValueEncoder<'a> { +impl std::fmt::Write for LabelValueEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { self.writer.write_str(s) } From 7413b61b12830b4952bcab43d0a2e6a58b1382a6 Mon Sep 17 00:00:00 2001 From: Qinxuan Chen <koushiro.cqx@gmail.com> Date: Fri, 29 Nov 2024 22:44:48 +0800 Subject: [PATCH 19/27] feat(encoding): impl `EncodeLabelValue` for `bool` (#237) Signed-off-by: koushiro <koushiro.cqx@gmail.com> --- CHANGELOG.md | 7 +- src/encoding.rs | 179 +++++++++++++++++++++++++----------------------- 2 files changed, 99 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c398e073..945d128f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement `Atomic<u64>` for `AtomicU64` for gauges. See [PR 226]. -[PR 226]: https://github.com/prometheus/client_rust/pull/198 +- Implement `EnableLabelValue` for `bool`. + See [PR 237] + +[PR 173]: https://github.com/prometheus/client_rust/pull/173 [PR 198]: https://github.com/prometheus/client_rust/pull/198 +[PR 226]: https://github.com/prometheus/client_rust/pull/226 +[PR 237]: https://github.com/prometheus/client_rust/pull/237 ### Added diff --git a/src/encoding.rs b/src/encoding.rs index 9b2e497c..9e2acac0 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -206,12 +206,6 @@ pub trait EncodeLabelSet { fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error>; } -impl<'a> From<text::LabelSetEncoder<'a>> for LabelSetEncoder<'a> { - fn from(e: text::LabelSetEncoder<'a>) -> Self { - Self(LabelSetEncoderInner::Text(e)) - } -} - /// Encoder for a label set. #[derive(Debug)] pub struct LabelSetEncoder<'a>(LabelSetEncoderInner<'a>); @@ -223,6 +217,12 @@ enum LabelSetEncoderInner<'a> { Protobuf(protobuf::LabelSetEncoder<'a>), } +impl<'a> From<text::LabelSetEncoder<'a>> for LabelSetEncoder<'a> { + fn from(e: text::LabelSetEncoder<'a>) -> Self { + Self(LabelSetEncoderInner::Text(e)) + } +} + #[cfg(feature = "protobuf")] impl<'a> From<protobuf::LabelSetEncoder<'a>> for LabelSetEncoder<'a> { fn from(e: protobuf::LabelSetEncoder<'a>) -> Self { @@ -237,6 +237,42 @@ impl LabelSetEncoder<'_> { } } +impl<T: EncodeLabel, const N: usize> EncodeLabelSet for [T; N] { + fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + self.as_ref().encode(encoder) + } +} + +impl<T: EncodeLabel> EncodeLabelSet for &[T] { + fn encode(&self, mut encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + if self.is_empty() { + return Ok(()); + } + + for label in self.iter() { + label.encode(encoder.encode_label())? + } + + Ok(()) + } +} + +impl<T: EncodeLabel> EncodeLabelSet for Vec<T> { + fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + self.as_slice().encode(encoder) + } +} + +/// Uninhabited type to represent the lack of a label set for a metric +#[derive(Debug)] +pub enum NoLabelSet {} + +impl EncodeLabelSet for NoLabelSet { + fn encode(&self, _encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + Ok(()) + } +} + /// An encodable label. pub trait EncodeLabel { /// Encode oneself into the given encoder. @@ -247,10 +283,6 @@ pub trait EncodeLabel { #[derive(Debug)] pub struct LabelEncoder<'a>(LabelEncoderInner<'a>); -/// Uninhabited type to represent the lack of a label set for a metric -#[derive(Debug)] -pub enum NoLabelSet {} - #[derive(Debug)] enum LabelEncoderInner<'a> { Text(text::LabelEncoder<'a>), @@ -283,6 +315,21 @@ impl LabelEncoder<'_> { } } +impl<K: EncodeLabelKey, V: EncodeLabelValue> EncodeLabel for (K, V) { + fn encode(&self, mut encoder: LabelEncoder) -> Result<(), std::fmt::Error> { + let (key, value) = self; + + let mut label_key_encoder = encoder.encode_label_key()?; + key.encode(&mut label_key_encoder)?; + + let mut label_value_encoder = label_key_encoder.encode_label_value()?; + value.encode(&mut label_value_encoder)?; + label_value_encoder.finish()?; + + Ok(()) + } +} + /// An encodable label key. pub trait EncodeLabelKey { /// Encode oneself into the given encoder. @@ -330,52 +377,6 @@ impl<'a> LabelKeyEncoder<'a> { ) } } -impl<T: EncodeLabel, const N: usize> EncodeLabelSet for [T; N] { - fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { - self.as_ref().encode(encoder) - } -} - -impl<T: EncodeLabel> EncodeLabelSet for &[T] { - fn encode(&self, mut encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { - if self.is_empty() { - return Ok(()); - } - - for label in self.iter() { - label.encode(encoder.encode_label())? - } - - Ok(()) - } -} - -impl<T: EncodeLabel> EncodeLabelSet for Vec<T> { - fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { - self.as_slice().encode(encoder) - } -} - -impl EncodeLabelSet for NoLabelSet { - fn encode(&self, _encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { - Ok(()) - } -} - -impl<K: EncodeLabelKey, V: EncodeLabelValue> EncodeLabel for (K, V) { - fn encode(&self, mut encoder: LabelEncoder) -> Result<(), std::fmt::Error> { - let (key, value) = self; - - let mut label_key_encoder = encoder.encode_label_key()?; - key.encode(&mut label_key_encoder)?; - - let mut label_value_encoder = label_key_encoder.encode_label_value()?; - value.encode(&mut label_value_encoder)?; - label_value_encoder.finish()?; - - Ok(()) - } -} impl EncodeLabelKey for &str { fn encode(&self, encoder: &mut LabelKeyEncoder) -> Result<(), std::fmt::Error> { @@ -433,6 +434,13 @@ pub trait EncodeLabelValue { #[derive(Debug)] pub struct LabelValueEncoder<'a>(LabelValueEncoderInner<'a>); +#[derive(Debug)] +enum LabelValueEncoderInner<'a> { + Text(text::LabelValueEncoder<'a>), + #[cfg(feature = "protobuf")] + Protobuf(protobuf::LabelValueEncoder<'a>), +} + impl<'a> From<text::LabelValueEncoder<'a>> for LabelValueEncoder<'a> { fn from(e: text::LabelValueEncoder<'a>) -> Self { LabelValueEncoder(LabelValueEncoderInner::Text(e)) @@ -446,11 +454,10 @@ impl<'a> From<protobuf::LabelValueEncoder<'a>> for LabelValueEncoder<'a> { } } -#[derive(Debug)] -enum LabelValueEncoderInner<'a> { - Text(text::LabelValueEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::LabelValueEncoder<'a>), +impl std::fmt::Write for LabelValueEncoder<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + for_both_mut!(self, LabelValueEncoderInner, e, e.write_str(s)) + } } impl LabelValueEncoder<'_> { @@ -460,12 +467,6 @@ impl LabelValueEncoder<'_> { } } -impl std::fmt::Write for LabelValueEncoder<'_> { - fn write_str(&mut self, s: &str) -> std::fmt::Result { - for_both_mut!(self, LabelValueEncoderInner, e, e.write_str(s)) - } -} - impl EncodeLabelValue for &str { fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { encoder.write_str(self)?; @@ -536,6 +537,12 @@ where } } +impl EncodeLabelValue for bool { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + encoder.write_str(if *self { "true" } else { "false" }) + } +} + macro_rules! impl_encode_label_value_for_integer { ($($t:ident),*) => {$( impl EncodeLabelValue for $t { @@ -678,6 +685,19 @@ enum CounterValueEncoderInner<'a> { Protobuf(protobuf::CounterValueEncoder<'a>), } +impl<'a> From<text::CounterValueEncoder<'a>> for CounterValueEncoder<'a> { + fn from(e: text::CounterValueEncoder<'a>) -> Self { + CounterValueEncoder(CounterValueEncoderInner::Text(e)) + } +} + +#[cfg(feature = "protobuf")] +impl<'a> From<protobuf::CounterValueEncoder<'a>> for CounterValueEncoder<'a> { + fn from(e: protobuf::CounterValueEncoder<'a>) -> Self { + CounterValueEncoder(CounterValueEncoderInner::Protobuf(e)) + } +} + impl CounterValueEncoder<'_> { fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { for_both_mut!(self, CounterValueEncoderInner, e, e.encode_f64(v)) @@ -718,19 +738,6 @@ impl EncodeExemplarValue for u32 { } } -impl<'a> From<text::CounterValueEncoder<'a>> for CounterValueEncoder<'a> { - fn from(e: text::CounterValueEncoder<'a>) -> Self { - CounterValueEncoder(CounterValueEncoderInner::Text(e)) - } -} - -#[cfg(feature = "protobuf")] -impl<'a> From<protobuf::CounterValueEncoder<'a>> for CounterValueEncoder<'a> { - fn from(e: protobuf::CounterValueEncoder<'a>) -> Self { - CounterValueEncoder(CounterValueEncoderInner::Protobuf(e)) - } -} - /// Encoder for an exemplar value. #[derive(Debug)] pub struct ExemplarValueEncoder<'a>(ExemplarValueEncoderInner<'a>); @@ -742,12 +749,6 @@ enum ExemplarValueEncoderInner<'a> { Protobuf(protobuf::ExemplarValueEncoder<'a>), } -impl ExemplarValueEncoder<'_> { - fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { - for_both_mut!(self, ExemplarValueEncoderInner, e, e.encode(v)) - } -} - impl<'a> From<text::ExemplarValueEncoder<'a>> for ExemplarValueEncoder<'a> { fn from(e: text::ExemplarValueEncoder<'a>) -> Self { ExemplarValueEncoder(ExemplarValueEncoderInner::Text(e)) @@ -760,3 +761,9 @@ impl<'a> From<protobuf::ExemplarValueEncoder<'a>> for ExemplarValueEncoder<'a> { ExemplarValueEncoder(ExemplarValueEncoderInner::Protobuf(e)) } } + +impl ExemplarValueEncoder<'_> { + fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { + for_both_mut!(self, ExemplarValueEncoderInner, e, e.encode(v)) + } +} From 93c63dbcdecda2aaca331a1df9746f9774483800 Mon Sep 17 00:00:00 2001 From: Wenbo Zhang <ethercflow@gmail.com> Date: Tue, 3 Dec 2024 16:49:36 +0800 Subject: [PATCH 20/27] feat(metrics/family): add `get` method to `Family` (#234) Signed-off-by: Wenbo Zhang <wenbo.zhang@iomesh.com> --- CHANGELOG.md | 4 +++ src/metrics/family.rs | 64 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 945d128f..685c8b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,9 +34,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Supoort `Arc<String>` for `EncodeLabelValue`. See [PR 217]. +- Added `get` method to `Family`. + See [PR 234]. + [PR 173]: https://github.com/prometheus/client_rust/pull/173 [PR 216]: https://github.com/prometheus/client_rust/pull/216 [PR 217]: https://github.com/prometheus/client_rust/pull/217 +[PR 234]: https://github.com/prometheus/client_rust/pull/234 ### Fixed diff --git a/src/metrics/family.rs b/src/metrics/family.rs index 2f23b198..1a76cf8f 100644 --- a/src/metrics/family.rs +++ b/src/metrics/family.rs @@ -226,9 +226,7 @@ impl<S: Clone + std::hash::Hash + Eq, M, C: MetricConstructor<M>> Family<S, M, C /// family.get_or_create(&vec![("method".to_owned(), "GET".to_owned())]).inc(); /// ``` pub fn get_or_create(&self, label_set: &S) -> MappedRwLockReadGuard<M> { - if let Ok(metric) = - RwLockReadGuard::try_map(self.metrics.read(), |metrics| metrics.get(label_set)) - { + if let Some(metric) = self.get(label_set) { return metric; } @@ -247,6 +245,23 @@ impl<S: Clone + std::hash::Hash + Eq, M, C: MetricConstructor<M>> Family<S, M, C }) } + /// Access a metric with the given label set, returning None if one + /// does not yet exist. + /// + /// ``` + /// # use prometheus_client::metrics::counter::{Atomic, Counter}; + /// # use prometheus_client::metrics::family::Family; + /// # + /// let family = Family::<Vec<(String, String)>, Counter>::default(); + /// + /// if let Some(metric) = family.get(&vec![("method".to_owned(), "GET".to_owned())]) { + /// metric.inc(); + /// }; + /// ``` + pub fn get(&self, label_set: &S) -> Option<MappedRwLockReadGuard<M>> { + RwLockReadGuard::try_map(self.metrics.read(), |metrics| metrics.get(label_set)).ok() + } + /// Remove a label set from the metric family. /// /// Returns a bool indicating if a label set was removed or not. @@ -452,4 +467,47 @@ mod tests { .get() ); } + + #[test] + fn test_get() { + let family = Family::<Vec<(String, String)>, Counter>::default(); + + // Test getting a non-existent metric. + let non_existent = family.get(&vec![("method".to_string(), "GET".to_string())]); + assert!(non_existent.is_none()); + + // Create a metric. + family + .get_or_create(&vec![("method".to_string(), "GET".to_string())]) + .inc(); + + // Test getting an existing metric. + let existing = family.get(&vec![("method".to_string(), "GET".to_string())]); + assert!(existing.is_some()); + assert_eq!(existing.unwrap().get(), 1); + + // Test getting a different non-existent metric. + let another_non_existent = family.get(&vec![("method".to_string(), "POST".to_string())]); + assert!(another_non_existent.is_none()); + + // Test modifying the metric through the returned reference. + if let Some(metric) = family.get(&vec![("method".to_string(), "GET".to_string())]) { + metric.inc(); + } + + // Verify the modification. + let modified = family.get(&vec![("method".to_string(), "GET".to_string())]); + assert_eq!(modified.unwrap().get(), 2); + + // Test with a different label set type. + let string_family = Family::<String, Counter>::default(); + string_family.get_or_create(&"test".to_string()).inc(); + + let string_metric = string_family.get(&"test".to_string()); + assert!(string_metric.is_some()); + assert_eq!(string_metric.unwrap().get(), 1); + + let non_existent_string = string_family.get(&"non_existent".to_string()); + assert!(non_existent_string.is_none()); + } } From 1adc99464d10bfd21e202d50a3fcfadfc3a886cf Mon Sep 17 00:00:00 2001 From: David Ashpole <dashpole@google.com> Date: Mon, 6 Jan 2025 12:12:15 -0500 Subject: [PATCH 21/27] docs: update openmetrics links to v1.0.0 release (#253) Signed-off-by: David Ashpole <dashpole@google.com> --- src/encoding/protobuf.rs | 2 +- src/encoding/text.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encoding/protobuf.rs b/src/encoding/protobuf.rs index 5ec54c91..3c598144 100644 --- a/src/encoding/protobuf.rs +++ b/src/encoding/protobuf.rs @@ -14,7 +14,7 @@ //! # counter.clone(), //! # ); //! # counter.inc(); -//! // Returns `MetricSet`, the top-level container type. Please refer to [openmetrics_data_model.proto](https://github.com/OpenObservability/OpenMetrics/blob/main/proto/openmetrics_data_model.proto) for details. +//! // Returns `MetricSet`, the top-level container type. Please refer to [openmetrics_data_model.proto](https://github.com/prometheus/OpenMetrics/blob/v1.0.0/proto/openmetrics_data_model.proto) for details. //! let metric_set = encode(®istry).unwrap(); //! //! let family = metric_set.metric_families.first().unwrap(); diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 8f3c63df..f10cf1d3 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -53,7 +53,7 @@ use std::fmt::Write; /// /// Use [`encode_registry`] or [`encode_eof`] if partial encoding is needed. /// -/// See [OpenMetrics exposition format](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#text-format) +/// See [OpenMetrics exposition format](https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#text-format) /// for additional details. /// /// # Examples From 8f134607a5204728a1e5b44efe74b3459f18a603 Mon Sep 17 00:00:00 2001 From: Sidhant Kohli <sidhant.kohli@gmail.com> Date: Tue, 7 Jan 2025 14:08:17 +0530 Subject: [PATCH 22/27] feat: add range based exponential buckets in histogram (#233) Signed-off-by: Sidhant Kohli <sidhant_kohli@intuit.com> --- CHANGELOG.md | 4 ++++ src/metrics/histogram.rs | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 685c8b66..839d97e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,12 +34,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Supoort `Arc<String>` for `EncodeLabelValue`. See [PR 217]. +- Add `histogram::exponential_buckets_range`. + See [PR 233]. + - Added `get` method to `Family`. See [PR 234]. [PR 173]: https://github.com/prometheus/client_rust/pull/173 [PR 216]: https://github.com/prometheus/client_rust/pull/216 [PR 217]: https://github.com/prometheus/client_rust/pull/217 +[PR 233]: https://github.com/prometheus/client_rust/pull/233 [PR 234]: https://github.com/prometheus/client_rust/pull/234 ### Fixed diff --git a/src/metrics/histogram.rs b/src/metrics/histogram.rs index 1e418ad9..f95ca8d5 100644 --- a/src/metrics/histogram.rs +++ b/src/metrics/histogram.rs @@ -122,6 +122,30 @@ pub fn exponential_buckets(start: f64, factor: f64, length: u16) -> impl Iterato .take(length.into()) } +/// Exponential bucket distribution within a range +/// +/// Creates `length` buckets, where the lowest bucket is `min` and the highest bucket is `max`. +/// +/// If `length` is less than 1, or `min` is less than or equal to 0, an empty iterator is returned. +pub fn exponential_buckets_range(min: f64, max: f64, length: u16) -> impl Iterator<Item = f64> { + let mut len_observed = length; + let mut min_bucket = min; + // length needs a positive length and min needs to be greater than 0 + // set len_observed to 0 and min_bucket to 1.0 + // this will return an empty iterator in the result + if length < 1 || min <= 0.0 { + len_observed = 0; + min_bucket = 1.0; + } + // We know max/min and highest bucket. Solve for growth_factor. + let growth_factor = (max / min_bucket).powf(1.0 / (len_observed as f64 - 1.0)); + + iter::repeat(()) + .enumerate() + .map(move |(i, _)| min_bucket * growth_factor.powf(i as f64)) + .take(len_observed.into()) +} + /// Linear bucket distribution. pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator<Item = f64> { iter::repeat(()) @@ -166,4 +190,21 @@ mod tests { linear_buckets(0.0, 1.0, 10).collect::<Vec<_>>() ); } + + #[test] + fn exponential_range() { + assert_eq!( + vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0], + exponential_buckets_range(1.0, 32.0, 6).collect::<Vec<_>>() + ); + } + + #[test] + fn exponential_range_incorrect() { + let res = exponential_buckets_range(1.0, 32.0, 0).collect::<Vec<_>>(); + assert!(res.is_empty()); + + let res = exponential_buckets_range(0.0, 32.0, 6).collect::<Vec<_>>(); + assert!(res.is_empty()); + } } From cbde2cb6796629d35857761c612fcbe768bfe18b Mon Sep 17 00:00:00 2001 From: Max Inden <mail@max-inden.de> Date: Tue, 7 Jan 2025 09:49:54 +0100 Subject: [PATCH 23/27] chore: prepare v0.23.0 (#255) Signed-off-by: Max Inden <mail@max-inden.de> --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839d97e4..db4f6062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.23.0] - unreleased - +## [0.23.0] ### Changed From 9a74e9900e9a21eb94d34226149343ab7ff1733f Mon Sep 17 00:00:00 2001 From: katelyn martin <me+cratelyn@katelyn.world> Date: Wed, 15 Jan 2025 13:02:05 -0500 Subject: [PATCH 24/27] refactor(metrics/histogram): constructor accepts `IntoIterator` (#243) this is a very small, **non-breaking**, alteration to the signature of `Histogram`'s constructor. rather than accepting an `impl Iterator`, this commit changes the parameter of `Histogram::new()` to be an `impl IntoIterator` instead. this does not affect the existing contract because of the blanket `impl<I: Iterator> IntoIterator for I` implementation provided by the standard library. by accepting `IntoIterator` however, callers providing a collection such as a `[f64; 5]` array or a `Vec<f64>` vector do not need to invoke `into_iter()` themselves at the call-site. ```rust // now, constructing a histogram needn't involve `into_iter()`... use prometheus_client::metrics::histogram::Histogram; let histogram = Histogram::new([10.0, 100.0, 1_000.0]); ``` this leans on the same sugar used by `for {}` loops, see the relevant section of the `std::iter` documentation here: <https://doc.rust-lang.org/stable/std/iter/index.html#for-loops-and-intoiterator> no changes are needed within `Histogram::new()` because we already call `into_iter()` on the provider iterator when appending `f64::MAX` and collecting the buckets into a `Vec<_>`. Signed-off-by: katelyn martin <me+cratelyn@katelyn.world> --- CHANGELOG.md | 9 +++++++++ Cargo.toml | 2 +- src/metrics/histogram.rs | 9 +++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db4f6062..3b9d4f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.23.1] - unreleased + +### Changed + +- `Histogram::new` now accepts an `IntoIterator` argument, rather than an `Iterator`. + See [PR 243]. + +[PR 243]: https://github.com/prometheus/client_rust/pull/243 + ## [0.23.0] ### Changed diff --git a/Cargo.toml b/Cargo.toml index 18dd395f..d04c8999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus-client" -version = "0.23.0" +version = "0.23.1" authors = ["Max Inden <mail@max-inden.de>"] edition = "2021" description = "Open Metrics client library allowing users to natively instrument applications." diff --git a/src/metrics/histogram.rs b/src/metrics/histogram.rs index f95ca8d5..5a50278c 100644 --- a/src/metrics/histogram.rs +++ b/src/metrics/histogram.rs @@ -28,7 +28,7 @@ use std::sync::Arc; /// let custom_buckets = [ /// 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, /// ]; -/// let histogram = Histogram::new(custom_buckets.into_iter()); +/// let histogram = Histogram::new(custom_buckets); /// histogram.observe(4.2); /// ``` // TODO: Consider using atomics. See @@ -57,7 +57,12 @@ pub(crate) struct Inner { impl Histogram { /// Create a new [`Histogram`]. - pub fn new(buckets: impl Iterator<Item = f64>) -> Self { + /// + /// ```rust + /// # use prometheus_client::metrics::histogram::Histogram; + /// let histogram = Histogram::new([10.0, 100.0, 1_000.0]); + /// ``` + pub fn new(buckets: impl IntoIterator<Item = f64>) -> Self { Self { inner: Arc::new(RwLock::new(Inner { sum: Default::default(), From 84e2cc6a917341b31cfbf2f809193344fb7fb99d Mon Sep 17 00:00:00 2001 From: katelyn martin <me+cratelyn@katelyn.world> Date: Sun, 9 Feb 2025 08:48:30 -0500 Subject: [PATCH 25/27] feat(encoding)!: `EncodeLabelSet::encode()` uses reference (#257) this commit alters the signature of the `EncodeLabelSet::encode()` trait method, such that it now accepts a mutable reference to its encoder. this is related to #135, and is a second proposal following previous work in #240. this change permits distinct label sets to be composed together, now that the label set encoder is not consumed. a new implementation for tuples `(A, B)` is provided. this commit includes a test case showing that a metric family can compose two label sets together, and that such a family can successfully be digested by the python client library. `derive-encode` is altered to generate code matching this new trait signature, and has been bumped to version 0.5.0 as a result of this breaking change in the `prometheus-client` library. Signed-off-by: katelyn martin <me+cratelyn@katelyn.world> --- CHANGELOG.md | 12 ++++++- Cargo.toml | 4 +-- derive-encode/Cargo.toml | 2 +- derive-encode/src/lib.rs | 2 +- src/encoding.rs | 28 ++++++++++++---- src/encoding/protobuf.rs | 8 ++--- src/encoding/text.rs | 69 ++++++++++++++++++++++++++++++++++++---- 7 files changed, 103 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b9d4f94..a0e32055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.23.1] - unreleased +## [0.24.0] - unreleased + +### Added + +- `EncodeLabelSet` is now implemented for tuples `(A: EncodeLabelSet, B: EncodeLabelSet)`. + +### Changed + +- `EncodeLabelSet::encode()` now accepts a mutable reference to its encoder parameter. + +## [0.23.1] ### Changed diff --git a/Cargo.toml b/Cargo.toml index d04c8999..a0f5e2a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus-client" -version = "0.23.1" +version = "0.24.0" authors = ["Max Inden <mail@max-inden.de>"] edition = "2021" description = "Open Metrics client library allowing users to natively instrument applications." @@ -21,7 +21,7 @@ members = ["derive-encode"] dtoa = "1.0" itoa = "1.0" parking_lot = "0.12" -prometheus-client-derive-encode = { version = "0.4.1", path = "derive-encode" } +prometheus-client-derive-encode = { version = "0.5.0", path = "derive-encode" } prost = { version = "0.12.0", optional = true } prost-types = { version = "0.12.0", optional = true } diff --git a/derive-encode/Cargo.toml b/derive-encode/Cargo.toml index f365b077..145c0159 100644 --- a/derive-encode/Cargo.toml +++ b/derive-encode/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus-client-derive-encode" -version = "0.4.2" +version = "0.5.0" authors = ["Max Inden <mail@max-inden.de>"] edition = "2021" description = "Auxiliary crate to derive Encode trait from prometheus-client." diff --git a/derive-encode/src/lib.rs b/derive-encode/src/lib.rs index 1858cf8d..5b3cbe32 100644 --- a/derive-encode/src/lib.rs +++ b/derive-encode/src/lib.rs @@ -72,7 +72,7 @@ pub fn derive_encode_label_set(input: TokenStream) -> TokenStream { let gen = quote! { impl prometheus_client::encoding::EncodeLabelSet for #name { - fn encode(&self, mut encoder: prometheus_client::encoding::LabelSetEncoder) -> std::result::Result<(), std::fmt::Error> { + fn encode(&self, encoder: &mut prometheus_client::encoding::LabelSetEncoder) -> std::result::Result<(), std::fmt::Error> { use prometheus_client::encoding::EncodeLabel; use prometheus_client::encoding::EncodeLabelKey; use prometheus_client::encoding::EncodeLabelValue; diff --git a/src/encoding.rs b/src/encoding.rs index 9e2acac0..ae8420f6 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -203,7 +203,7 @@ impl MetricEncoder<'_> { /// An encodable label set. pub trait EncodeLabelSet { /// Encode oneself into the given encoder. - fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error>; + fn encode(&self, encoder: &mut LabelSetEncoder) -> Result<(), std::fmt::Error>; } /// Encoder for a label set. @@ -238,19 +238,20 @@ impl LabelSetEncoder<'_> { } impl<T: EncodeLabel, const N: usize> EncodeLabelSet for [T; N] { - fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + fn encode(&self, encoder: &mut LabelSetEncoder) -> Result<(), std::fmt::Error> { self.as_ref().encode(encoder) } } impl<T: EncodeLabel> EncodeLabelSet for &[T] { - fn encode(&self, mut encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + fn encode(&self, encoder: &mut LabelSetEncoder) -> Result<(), std::fmt::Error> { if self.is_empty() { return Ok(()); } for label in self.iter() { - label.encode(encoder.encode_label())? + let encoder = encoder.encode_label(); + label.encode(encoder)? } Ok(()) @@ -258,17 +259,32 @@ impl<T: EncodeLabel> EncodeLabelSet for &[T] { } impl<T: EncodeLabel> EncodeLabelSet for Vec<T> { - fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + fn encode(&self, encoder: &mut LabelSetEncoder) -> Result<(), std::fmt::Error> { self.as_slice().encode(encoder) } } +impl<A, B> EncodeLabelSet for (A, B) +where + A: EncodeLabelSet, + B: EncodeLabelSet, +{ + fn encode(&self, encoder: &mut LabelSetEncoder) -> Result<(), std::fmt::Error> { + let (a, b) = self; + + a.encode(encoder)?; + b.encode(encoder)?; + + Ok(()) + } +} + /// Uninhabited type to represent the lack of a label set for a metric #[derive(Debug)] pub enum NoLabelSet {} impl EncodeLabelSet for NoLabelSet { - fn encode(&self, _encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + fn encode(&self, _encoder: &mut LabelSetEncoder) -> Result<(), std::fmt::Error> { Ok(()) } } diff --git a/src/encoding/protobuf.rs b/src/encoding/protobuf.rs index 3c598144..b22ea5ae 100644 --- a/src/encoding/protobuf.rs +++ b/src/encoding/protobuf.rs @@ -120,7 +120,7 @@ impl DescriptorEncoder<'_> { }; let mut labels = vec![]; self.labels.encode( - LabelSetEncoder { + &mut LabelSetEncoder { labels: &mut labels, } .into(), @@ -210,7 +210,7 @@ impl MetricEncoder<'_> { ) -> Result<(), std::fmt::Error> { let mut info_labels = vec![]; label_set.encode( - LabelSetEncoder { + &mut LabelSetEncoder { labels: &mut info_labels, } .into(), @@ -235,7 +235,7 @@ impl MetricEncoder<'_> { ) -> Result<MetricEncoder, std::fmt::Error> { let mut labels = self.labels.clone(); label_set.encode( - LabelSetEncoder { + &mut LabelSetEncoder { labels: &mut labels, } .into(), @@ -303,7 +303,7 @@ impl<S: EncodeLabelSet, V: EncodeExemplarValue> TryFrom<&Exemplar<S, V>> let mut labels = vec![]; exemplar.label_set.encode( - LabelSetEncoder { + &mut LabelSetEncoder { labels: &mut labels, } .into(), diff --git a/src/encoding/text.rs b/src/encoding/text.rs index f10cf1d3..80b91076 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -296,8 +296,9 @@ pub(crate) struct MetricEncoder<'a> { impl std::fmt::Debug for MetricEncoder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut labels = String::new(); + let mut encoder = LabelSetEncoder::new(&mut labels).into(); if let Some(l) = self.family_labels { - l.encode(LabelSetEncoder::new(&mut labels).into())?; + l.encode(&mut encoder)?; } f.debug_struct("Encoder") @@ -451,7 +452,7 @@ impl MetricEncoder<'_> { self.writer.write_str(" # {")?; exemplar .label_set - .encode(LabelSetEncoder::new(self.writer).into())?; + .encode(&mut LabelSetEncoder::new(self.writer).into())?; self.writer.write_str("} ")?; exemplar.value.encode( ExemplarValueEncoder { @@ -502,14 +503,14 @@ impl MetricEncoder<'_> { self.writer.write_str("{")?; self.const_labels - .encode(LabelSetEncoder::new(self.writer).into())?; + .encode(&mut LabelSetEncoder::new(self.writer).into())?; if let Some(additional_labels) = additional_labels { if !self.const_labels.is_empty() { self.writer.write_str(",")?; } - additional_labels.encode(LabelSetEncoder::new(self.writer).into())?; + additional_labels.encode(&mut LabelSetEncoder::new(self.writer).into())?; } /// Writer impl which prepends a comma on the first call to write output to the wrapped writer @@ -539,9 +540,9 @@ impl MetricEncoder<'_> { writer: self.writer, should_prepend: true, }; - labels.encode(LabelSetEncoder::new(&mut writer).into())?; + labels.encode(&mut LabelSetEncoder::new(&mut writer).into())?; } else { - labels.encode(LabelSetEncoder::new(self.writer).into())?; + labels.encode(&mut LabelSetEncoder::new(self.writer).into())?; }; } @@ -936,7 +937,7 @@ mod tests { struct EmptyLabels {} impl EncodeLabelSet for EmptyLabels { - fn encode(&self, _encoder: crate::encoding::LabelSetEncoder) -> Result<(), Error> { + fn encode(&self, _encoder: &mut crate::encoding::LabelSetEncoder) -> Result<(), Error> { Ok(()) } } @@ -1114,6 +1115,60 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn label_sets_can_be_composed() { + #[derive(Clone, Debug, Eq, Hash, PartialEq)] + struct Color(&'static str); + impl EncodeLabelSet for Color { + fn encode( + &self, + encoder: &mut crate::encoding::LabelSetEncoder, + ) -> Result<(), std::fmt::Error> { + use crate::encoding::EncodeLabel; + let Self(color) = *self; + let labels = ("color", color); + let encoder = encoder.encode_label(); + labels.encode(encoder) + } + } + + #[derive(Clone, Debug, Eq, Hash, PartialEq)] + struct Size(&'static str); + impl EncodeLabelSet for Size { + fn encode( + &self, + encoder: &mut crate::encoding::LabelSetEncoder, + ) -> Result<(), std::fmt::Error> { + use crate::encoding::EncodeLabel; + let Self(size) = *self; + let labels = ("size", size); + let encoder = encoder.encode_label(); + labels.encode(encoder) + } + } + + type Labels = (Color, Size); + + let mut registry = Registry::default(); + let family = Family::<Labels, Counter>::default(); + registry.register("items", "Example metric", family.clone()); + + let labels = (Color("red"), Size("large")); + let counter = family.get_or_create(&labels); + counter.inc(); + + let mut encoded = String::new(); + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP items Example metric.\n\ + # TYPE items counter\n\ + items_total{color=\"red\",size=\"large\"} 1\n\ + # EOF\n"; + assert_eq!(expected, encoded); + + parse_with_python_client(encoded); + } + #[test] fn encode_registry_eof() { let mut orders_registry = Registry::default(); From 377ca2d86308d868576e961c778bcbad8b62ec0e Mon Sep 17 00:00:00 2001 From: katelyn martin <me+cratelyn@katelyn.world> Date: Sun, 9 Feb 2025 15:51:38 -0500 Subject: [PATCH 26/27] =?UTF-8?q?feat(metrics/family):=20=F0=9F=8D=AB=20ad?= =?UTF-8?q?d=20`Family::get=5For=5Fcreate=5Fowned()`=20(#244)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #231. this introduces a new method to `Family<S, M, C>` to help alleviate the risk of deadlocks when accessing multiple series within a given metrics family. this returns a plain `M`, rather than the `MappedRwLockReadGuard<M>` RAII guard returned by `get_or_create()`. a test case is introduced in this commit to demonstrate that structures accessing multiple series within a single expression will not accidentally create a deadlock. Signed-off-by: katelyn martin <me+cratelyn@katelyn.world> --- CHANGELOG.md | 8 ++++++ src/metrics/family.rs | 57 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e32055..c798c594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `EncodeLabelSet` is now implemented for tuples `(A: EncodeLabelSet, B: EncodeLabelSet)`. + See [PR 257]. + +- `Family::get_or_create_owned` can access a metric in a labeled family. This + method avoids the risk of runtime deadlocks at the expense of creating an + owned type. See [PR 244]. + +[PR 244]: https://github.com/prometheus/client_rust/pull/244 +[PR 257]: https://github.com/prometheus/client_rust/pull/257 ### Changed diff --git a/src/metrics/family.rs b/src/metrics/family.rs index 1a76cf8f..d3d55c6f 100644 --- a/src/metrics/family.rs +++ b/src/metrics/family.rs @@ -207,6 +207,40 @@ impl<S: Clone + std::hash::Hash + Eq, M, C> Family<S, M, C> { } } +impl<S: Clone + std::hash::Hash + Eq, M: Clone, C: MetricConstructor<M>> Family<S, M, C> +where + S: Clone + std::hash::Hash + Eq, + M: Clone, + C: MetricConstructor<M>, +{ + /// Access a metric with the given label set, creating it if one does not yet exist. + /// + /// ``` + /// # use prometheus_client::metrics::counter::{Atomic, Counter}; + /// # use prometheus_client::metrics::family::Family; + /// # + /// let family = Family::<Vec<(String, String)>, Counter>::default(); + /// + /// // Will create and return the metric with label `method="GET"` when first called. + /// family.get_or_create_owned(&vec![("method".to_owned(), "GET".to_owned())]).inc(); + /// + /// // Will return a clone of the existing metric on all subsequent calls. + /// family.get_or_create_owned(&vec![("method".to_owned(), "GET".to_owned())]).inc(); + /// ``` + /// + /// Callers wishing to avoid a clone of the metric `M` can call [`Family::get_or_create()`] to + /// return a reference to the metric instead. + pub fn get_or_create_owned(&self, label_set: &S) -> M { + use std::ops::Deref; + + let guard = self.get_or_create(label_set); + let metric = guard.deref().to_owned(); + drop(guard); + + metric + } +} + impl<S: Clone + std::hash::Hash + Eq, M, C: MetricConstructor<M>> Family<S, M, C> { /// Access a metric with the given label set, creating it if one does not /// yet exist. @@ -225,6 +259,10 @@ impl<S: Clone + std::hash::Hash + Eq, M, C: MetricConstructor<M>> Family<S, M, C /// // calls. /// family.get_or_create(&vec![("method".to_owned(), "GET".to_owned())]).inc(); /// ``` + /// + /// NB: This method can cause deadlocks if multiple metrics within this family are read at + /// once. Use [`Family::get_or_create_owned()`] if you would like to avoid this by cloning the + /// metric `M`. pub fn get_or_create(&self, label_set: &S) -> MappedRwLockReadGuard<M> { if let Some(metric) = self.get(label_set) { return metric; @@ -510,4 +548,23 @@ mod tests { let non_existent_string = string_family.get(&"non_existent".to_string()); assert!(non_existent_string.is_none()); } + + /// Tests that [`Family::get_or_create_owned()`] does not cause deadlocks. + #[test] + fn counter_family_does_not_deadlock() { + /// A structure we'll place two counters into, within a single expression. + struct S { + apples: Counter, + oranges: Counter, + } + + let family = Family::<(&str, &str), Counter>::default(); + let s = S { + apples: family.get_or_create_owned(&("kind", "apple")), + oranges: family.get_or_create_owned(&("kind", "orange")), + }; + + s.apples.inc(); + s.oranges.inc_by(2); + } } From ab152ee915698f83ae5b7ac5e4bee104678b70b1 Mon Sep 17 00:00:00 2001 From: Edwin Amsler <EdwinGuy@GMail.com> Date: Thu, 27 Feb 2025 03:57:26 -0500 Subject: [PATCH 27/27] docs: add example using std::io::Write (#261) Signed-off-by: Edwin Amsler <edwinguy@gmail.com> --- examples/io_write.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 examples/io_write.rs diff --git a/examples/io_write.rs b/examples/io_write.rs new file mode 100644 index 00000000..47d1056b --- /dev/null +++ b/examples/io_write.rs @@ -0,0 +1,39 @@ +//! Example showing how one could write to a file or socket instead of a string. +//! For large metrics registries this will be more memory efficient. + +use prometheus_client::{encoding::text::encode, metrics::counter::Counter, registry::Registry}; +use std::io::Write; + +fn main() { + let mut registry = <Registry>::with_prefix("stream"); + let request_counter: Counter<u64> = Default::default(); + + registry.register( + "requests", + "How many requests the application has received", + request_counter.clone(), + ); + + let mut buf = String::new(); + encode(&mut buf, ®istry).unwrap(); + + let mut file = Vec::new(); + let mut writer = IoWriterWrapper(&mut file); + encode(&mut writer, ®istry).unwrap(); + + assert!(buf.as_bytes() == file); +} + +pub struct IoWriterWrapper<W>(W); + +impl<W> std::fmt::Write for IoWriterWrapper<W> +where + W: Write, +{ + fn write_str(&mut self, input: &str) -> std::fmt::Result { + self.0 + .write_all(input.as_bytes()) + .map(|_| ()) + .map_err(|_| std::fmt::Error) + } +}