Skip to content

Commit ecbe6f6

Browse files
committed
feat(prometheus-client-derive): initial implemenation
Signed-off-by: ADD-SP <[email protected]>
1 parent adc7c4e commit ecbe6f6

File tree

19 files changed

+622
-6
lines changed

19 files changed

+622
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- `Family::get_or_create_owned` can access a metric in a labeled family. This
1515
method avoids the risk of runtime deadlocks at the expense of creating an
1616
owned type. See [PR 244].
17-
17+
18+
- Supported derive macro `Registrant` to register a metric set with a
19+
`Registry`. See [PR 270].
20+
1821
[PR 244]: https://github.com/prometheus/client_rust/pull/244
1922
[PR 257]: https://github.com/prometheus/client_rust/pull/257
23+
[PR 270]: https://github.com/prometheus/client_rust/pull/270
2024

2125
### Changed
2226

Cargo.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ default = []
1515
protobuf = ["dep:prost", "dep:prost-types", "dep:prost-build"]
1616

1717
[workspace]
18-
members = ["derive-encode"]
18+
members = ["derive-encode", "prometheus-client-derive"]
19+
20+
[workspace.dependencies]
21+
proc-macro2 = "1"
22+
quote = "1"
23+
syn = "2"
24+
25+
# dev-dependencies
26+
trybuild = "1"
1927

2028
[dependencies]
2129
dtoa = "1.0"
@@ -24,6 +32,7 @@ parking_lot = "0.12"
2432
prometheus-client-derive-encode = { version = "0.5.0", path = "derive-encode" }
2533
prost = { version = "0.12.0", optional = true }
2634
prost-types = { version = "0.12.0", optional = true }
35+
prometheus-client-derive = { version = "0.24.0", path = "./prometheus-client-derive" }
2736

2837
[dev-dependencies]
2938
async-std = { version = "1", features = ["attributes"] }
@@ -40,6 +49,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "net", "macros", "signal
4049
hyper = { version = "1.3.1", features = ["server", "http1"] }
4150
hyper-util = { version = "0.1.3", features = ["tokio"] }
4251
http-body-util = "0.1.1"
52+
prometheus-client-derive = { path = "./prometheus-client-derive" }
4353

4454
[build-dependencies]
4555
prost-build = { version = "0.12.0", optional = true }

derive-encode/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ documentation = "https://docs.rs/prometheus-client-derive-text-encode"
1212
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1313

1414
[dependencies]
15-
proc-macro2 = "1"
16-
quote = "1"
17-
syn = "2"
15+
proc-macro2 = { workspace = true }
16+
quote = { workspace = true }
17+
syn = { workspace = true }
1818

1919
[dev-dependencies]
2020
prometheus-client = { path = "../", features = ["protobuf"] }
21-
trybuild = "1"
21+
trybuild = { workspace = true }
2222

2323
[lib]
2424
proc-macro = true

prometheus-client-derive/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "prometheus-client-derive"
3+
version = "0.24.0"
4+
authors = ["Max Inden <[email protected]>"]
5+
edition = "2021"
6+
description = "Macros to derive auxiliary traits for the prometheus-client library."
7+
license = "Apache-2.0 OR MIT"
8+
keywords = ["derive", "prometheus", "metrics", "instrumentation", "monitoring"]
9+
repository = "https://github.com/prometheus/client_rust"
10+
homepage = "https://github.com/prometheus/client_rust"
11+
documentation = "https://docs.rs/prometheus-client"
12+
13+
[lib]
14+
proc-macro = true
15+
16+
[dependencies]
17+
proc-macro2 = { workspace = true }
18+
quote = { workspace = true }
19+
syn = { workspace = true }
20+
21+
[dev-dependencies]
22+
prometheus-client = { path = "../" }
23+
trybuild = { workspace = true }

prometheus-client-derive/src/lib.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#![deny(dead_code)]
2+
#![deny(missing_docs)]
3+
#![deny(unused)]
4+
#![forbid(unsafe_code)]
5+
#![cfg_attr(docsrs, feature(doc_cfg))]
6+
7+
//! This crate provides a procedural macro to derive
8+
//! auxiliary traits for the
9+
//! [`prometheus_client`](https://docs.rs/prometheus-client/latest/prometheus_client/)
10+
mod registrant;
11+
12+
use proc_macro::TokenStream as TokenStream1;
13+
use proc_macro2::TokenStream as TokenStream2;
14+
use syn::Error;
15+
16+
type Result<T> = std::result::Result<T, Error>;
17+
18+
#[proc_macro_derive(Registrant, attributes(registrant))]
19+
/// Derives the `prometheus_client::registry::Registrant` trait implementation for a struct.
20+
/// ```rust
21+
/// use prometheus_client::metrics::counter::Counter;
22+
/// use prometheus_client::metrics::gauge::Gauge;
23+
/// use prometheus_client::registry::{Registry, Registrant as _};
24+
/// use prometheus_client_derive::Registrant;
25+
///
26+
/// #[derive(Registrant)]
27+
/// struct Server {
28+
/// /// Number of HTTP requests received
29+
/// /// from the client
30+
/// requests: Counter,
31+
/// /// Memory usage in bytes
32+
/// /// of the server
33+
/// #[registrant(unit = "bytes")]
34+
/// memory_usage: Gauge,
35+
/// }
36+
///
37+
/// let mut registry = Registry::default();
38+
/// let server = Server {
39+
/// requests: Counter::default(),
40+
/// memory_usage: Gauge::default(),
41+
/// };
42+
/// server.register(&mut registry);
43+
/// ```
44+
///
45+
/// There are several field attributes:
46+
/// - `#[registrant(rename = "...")]`: Renames the metric.
47+
/// - `#[registrant(unit = "...")]`: Sets the unit of the metric.
48+
/// - `#[registrant(skip)]`: Skips the field and does not register it.
49+
pub fn registrant_derive(input: TokenStream1) -> TokenStream1 {
50+
match registrant::registrant_impl(input.into()) {
51+
Ok(tokens) => tokens.into(),
52+
Err(err) => err.to_compile_error().into(),
53+
}
54+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
use quote::ToTokens;
2+
use syn::spanned::Spanned;
3+
use proc_macro2::Span;
4+
5+
// do not derive debug since this needs "extra-traits"
6+
// feature for crate `syn`, which slows compile time
7+
// too much, and is not needed as this struct is not
8+
// public.
9+
pub struct Attribute {
10+
pub help: Option<syn::LitStr>,
11+
pub unit: Option<syn::LitStr>,
12+
pub rename: Option<syn::LitStr>,
13+
pub skip: bool,
14+
}
15+
16+
impl Default for Attribute {
17+
fn default() -> Self {
18+
Attribute {
19+
help: None,
20+
unit: None,
21+
rename: None,
22+
skip: false,
23+
}
24+
}
25+
}
26+
27+
impl Attribute {
28+
fn with_help(mut self, doc: syn::LitStr) -> Self {
29+
self.help = Some(doc);
30+
self
31+
}
32+
33+
pub(super) fn merge(self, other: Self) -> syn::Result<Self> {
34+
let mut merged = self;
35+
36+
if let Some(doc) = other.help {
37+
// trim leading and trailing whitespace
38+
// and add a space between the two doc strings
39+
let mut acc = merged
40+
.help
41+
.unwrap_or_else(|| syn::LitStr::new("", doc.span()))
42+
.value().trim()
43+
.to_string();
44+
acc.push(' ');
45+
acc.push_str(&doc.value().trim());
46+
merged.help = Some(syn::LitStr::new(&acc, Span::call_site()));
47+
}
48+
if let Some(unit) = other.unit {
49+
if merged.unit.is_some() {
50+
return Err(syn::Error::new_spanned(
51+
merged.unit,
52+
"Duplicate `unit` attribute",
53+
));
54+
}
55+
56+
merged.unit = Some(unit);
57+
}
58+
if let Some(rename) = other.rename {
59+
if merged.rename.is_some() {
60+
return Err(syn::Error::new_spanned(
61+
merged.rename,
62+
"Duplicate `rename` attribute",
63+
));
64+
}
65+
66+
merged.rename = Some(rename);
67+
}
68+
if other.skip {
69+
merged.skip = merged.skip || other.skip;
70+
}
71+
72+
Ok(merged)
73+
}
74+
}
75+
76+
impl syn::parse::Parse for Attribute {
77+
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
78+
let meta = input.parse::<syn::Meta>()?;
79+
let span = meta.span();
80+
81+
match meta {
82+
syn::Meta::NameValue(meta) if meta.path.is_ident("doc") => {
83+
if let syn::Expr::Lit(lit) = meta.value {
84+
let lit_str = syn::parse2::<syn::LitStr>(lit.lit.to_token_stream())?;
85+
return Ok(Attribute::default().with_help(lit_str));
86+
} else {
87+
return Err(syn::Error::new_spanned(
88+
meta.value,
89+
"Expected a string literal for doc attribute",
90+
));
91+
}
92+
}
93+
syn::Meta::List(meta) if meta.path.is_ident("registrant") => {
94+
let mut attr = Attribute::default();
95+
meta.parse_nested_meta(|meta| {
96+
if meta.path.is_ident("unit") {
97+
let unit = meta.value()?.parse::<syn::LitStr>()?;
98+
99+
if attr.unit.is_some() {
100+
return Err(syn::Error::new(
101+
meta.path.span(),
102+
"Duplicate `unit` attribute",
103+
));
104+
}
105+
106+
// unit should be lowercase
107+
let unit = syn::LitStr::new(
108+
unit.value().as_str().to_ascii_lowercase().as_str(),
109+
unit.span(),
110+
);
111+
attr.unit = Some(unit);
112+
} else if meta.path.is_ident("rename") {
113+
let rename = meta.value()?.parse::<syn::LitStr>()?;
114+
115+
if attr.rename.is_some() {
116+
return Err(syn::Error::new(
117+
meta.path.span(),
118+
"Duplicate `rename` attribute",
119+
));
120+
}
121+
122+
attr.rename = Some(rename);
123+
} else if meta.path.is_ident("skip") {
124+
if attr.skip {
125+
return Err(syn::Error::new(
126+
meta.path.span(),
127+
"Duplicate `skip` attribute",
128+
));
129+
}
130+
attr.skip = true;
131+
} else {
132+
panic!("Attributes other than `unit` and `rename` should not reach here");
133+
}
134+
Ok(())
135+
})?;
136+
Ok(attr)
137+
}
138+
_ => {
139+
return Err(syn::Error::new(
140+
span,
141+
r#"Unknown attribute, expected `#[doc(...)]` or `#[registrant(<key>[=value], ...)]`"#,
142+
))
143+
}
144+
}
145+
}
146+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use quote::ToTokens;
2+
use crate::registrant::attribute;
3+
use super::attribute::Attribute;
4+
5+
// do not derive debug since this needs "extra-traits"
6+
// feature for crate `syn`, which slows compile time
7+
// too much, and is not needed as this struct is not
8+
// public.
9+
pub struct Field {
10+
ident: syn::Ident,
11+
name: syn::LitStr,
12+
attr: Attribute,
13+
}
14+
15+
impl Field {
16+
pub(super) fn ident(&self) -> &syn::Ident {
17+
&self.ident
18+
}
19+
20+
pub(super) fn name(&self) -> &syn::LitStr {
21+
match &self.attr.rename {
22+
Some(rename) => rename,
23+
None => &self.name,
24+
}
25+
}
26+
27+
pub(super) fn help(&self) -> syn::LitStr {
28+
self.attr.help.clone()
29+
.unwrap_or_else(|| {
30+
syn::LitStr::new(
31+
"",
32+
self.ident.span(),
33+
)
34+
})
35+
}
36+
37+
pub(super) fn unit(&self) -> Option<&syn::LitStr> {
38+
self.attr.unit.as_ref()
39+
}
40+
41+
pub(super) fn skip(&self) -> bool {
42+
self.attr.skip
43+
}
44+
}
45+
46+
impl TryFrom<syn::Field> for Field {
47+
type Error = syn::Error;
48+
49+
fn try_from(field: syn::Field) -> Result<Self, Self::Error> {
50+
let ident = field.ident.clone().expect("Fields::Named should have an identifier");
51+
let name = syn::LitStr::new(
52+
&ident.to_string(),
53+
ident.span(),
54+
);
55+
let attr = field
56+
.attrs
57+
.into_iter()
58+
// ignore unknown attributes, which might be defined by another derive macros.
59+
.filter(|attr| attr.path().is_ident("doc") || attr.path().is_ident("registrant") )
60+
.try_fold(vec![], |mut acc, attr| {
61+
acc.push(syn::parse2::<Attribute>(attr.meta.into_token_stream())?);
62+
Ok::<Vec<attribute::Attribute>, syn::Error>(acc)
63+
})?
64+
.into_iter()
65+
.try_fold(Attribute::default(), |acc, attr| {
66+
acc.merge(attr)
67+
})?;
68+
Ok(Field{
69+
ident,
70+
name,
71+
attr,
72+
})
73+
}
74+
}

0 commit comments

Comments
 (0)