-
Notifications
You must be signed in to change notification settings - Fork 90
feat(prometheus-client-derive): initial implemenation #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ecbe6f6
a194c89
ce460c0
cc680cd
fe49848
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
[package] | ||
name = "prometheus-client-derive" | ||
version = "0.24.0" | ||
authors = ["Max Inden <[email protected]>"] | ||
edition = "2021" | ||
description = "Macros to derive auxiliary traits for the prometheus-client library." | ||
license = "Apache-2.0 OR MIT" | ||
keywords = ["derive", "prometheus", "metrics", "instrumentation", "monitoring"] | ||
repository = "https://github.com/prometheus/client_rust" | ||
homepage = "https://github.com/prometheus/client_rust" | ||
documentation = "https://docs.rs/prometheus-client" | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
proc-macro2 = { workspace = true } | ||
quote = { workspace = true } | ||
syn = { workspace = true } | ||
|
||
[dev-dependencies] | ||
prometheus-client = { path = "../" } | ||
trybuild = { workspace = true } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
#![deny(dead_code)] | ||
#![deny(missing_docs)] | ||
#![deny(unused)] | ||
#![forbid(unsafe_code)] | ||
#![cfg_attr(docsrs, feature(doc_cfg))] | ||
|
||
//! This crate provides a procedural macro to derive | ||
//! auxiliary traits for the | ||
//! [`prometheus_client`](https://docs.rs/prometheus-client/latest/prometheus_client/) | ||
mod registrant; | ||
|
||
use proc_macro::TokenStream as TokenStream1; | ||
use proc_macro2::TokenStream as TokenStream2; | ||
use syn::Error; | ||
|
||
type Result<T> = std::result::Result<T, Error>; | ||
|
||
#[proc_macro_derive(Registrant, attributes(registrant))] | ||
/// Derives the `prometheus_client::registry::Registrant` trait implementation for a struct. | ||
/// ```rust | ||
/// use prometheus_client::metrics::counter::Counter; | ||
/// use prometheus_client::metrics::gauge::Gauge; | ||
/// use prometheus_client::registry::{Registry, Registrant as _}; | ||
/// use prometheus_client_derive::Registrant; | ||
/// | ||
/// #[derive(Registrant)] | ||
/// struct Server { | ||
/// /// Number of HTTP requests received | ||
/// /// from the client | ||
/// requests: Counter, | ||
/// /// Memory usage in bytes | ||
/// /// of the server | ||
/// #[registrant(unit = "bytes")] | ||
/// memory_usage: Gauge, | ||
/// } | ||
/// | ||
/// let mut registry = Registry::default(); | ||
/// let server = Server { | ||
/// requests: Counter::default(), | ||
/// memory_usage: Gauge::default(), | ||
/// }; | ||
/// server.register(&mut registry); | ||
/// ``` | ||
/// | ||
/// There are several field attributes: | ||
/// - `#[registrant(rename = "...")]`: Renames the metric. | ||
/// - `#[registrant(unit = "...")]`: Sets the unit of the metric. | ||
/// - `#[registrant(skip)]`: Skips the field and does not register it. | ||
pub fn registrant_derive(input: TokenStream1) -> TokenStream1 { | ||
match registrant::registrant_impl(input.into()) { | ||
Ok(tokens) => tokens.into(), | ||
Err(err) => err.to_compile_error().into(), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
use proc_macro2::Span; | ||
use quote::ToTokens; | ||
use syn::spanned::Spanned; | ||
|
||
// do not derive debug since this needs "extra-traits" | ||
// feature for crate `syn`, which slows compile time | ||
// too much, and is not needed as this struct is not | ||
// public. | ||
#[derive(Default)] | ||
pub struct Attribute { | ||
pub help: Option<syn::LitStr>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shall we make the |
||
pub unit: Option<syn::LitStr>, | ||
pub rename: Option<syn::LitStr>, | ||
pub skip: bool, | ||
} | ||
|
||
impl Attribute { | ||
fn with_help(mut self, doc: syn::LitStr) -> Self { | ||
self.help = Some(doc); | ||
self | ||
} | ||
|
||
pub(super) fn merge(self, other: Self) -> syn::Result<Self> { | ||
let mut merged = self; | ||
|
||
if let Some(help) = other.help { | ||
// trim leading and trailing whitespace | ||
// and add a space between the two doc strings | ||
let mut acc = merged | ||
.help | ||
.unwrap_or_else(|| syn::LitStr::new("", help.span())) | ||
.value() | ||
.trim() | ||
.to_string(); | ||
acc.push(' '); | ||
acc.push_str(help.value().trim()); | ||
merged.help = Some(syn::LitStr::new(&acc, Span::call_site())); | ||
} | ||
if let Some(unit) = other.unit { | ||
if merged.unit.is_some() { | ||
return Err(syn::Error::new_spanned( | ||
merged.unit, | ||
"Duplicate `unit` attribute", | ||
)); | ||
} | ||
|
||
merged.unit = Some(unit); | ||
} | ||
if let Some(rename) = other.rename { | ||
if merged.rename.is_some() { | ||
return Err(syn::Error::new_spanned( | ||
merged.rename, | ||
"Duplicate `rename` attribute", | ||
)); | ||
} | ||
|
||
merged.rename = Some(rename); | ||
} | ||
if other.skip { | ||
merged.skip = merged.skip || other.skip; | ||
} | ||
|
||
Ok(merged) | ||
} | ||
} | ||
|
||
impl syn::parse::Parse for Attribute { | ||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { | ||
let meta = input.parse::<syn::Meta>()?; | ||
let span = meta.span(); | ||
|
||
match meta { | ||
syn::Meta::NameValue(meta) if meta.path.is_ident("doc") => { | ||
if let syn::Expr::Lit(lit) = meta.value { | ||
let lit_str = syn::parse2::<syn::LitStr>(lit.lit.to_token_stream())?; | ||
Ok(Attribute::default().with_help(lit_str)) | ||
} else { | ||
Err(syn::Error::new_spanned( | ||
meta.value, | ||
"Expected a string literal for doc attribute", | ||
)) | ||
} | ||
} | ||
syn::Meta::List(meta) if meta.path.is_ident("registrant") => { | ||
let mut attr = Attribute::default(); | ||
meta.parse_nested_meta(|meta| { | ||
if meta.path.is_ident("unit") { | ||
let unit = meta.value()?.parse::<syn::LitStr>()?; | ||
|
||
if attr.unit.is_some() { | ||
return Err(syn::Error::new( | ||
meta.path.span(), | ||
"Duplicate `unit` attribute", | ||
)); | ||
} | ||
|
||
// unit should be lowercase | ||
let unit = syn::LitStr::new( | ||
unit.value().as_str().to_ascii_lowercase().as_str(), | ||
unit.span(), | ||
); | ||
Comment on lines
+97
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two options:
Unit is a part of the metric name in Prometheus output, but Prometheus allow uppercase in name which is not the best practice. What do you think? |
||
attr.unit = Some(unit); | ||
} else if meta.path.is_ident("rename") { | ||
let rename = meta.value()?.parse::<syn::LitStr>()?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Shall we emit compilation error if the metric name is not snake_case? |
||
|
||
if attr.rename.is_some() { | ||
return Err(syn::Error::new( | ||
meta.path.span(), | ||
"Duplicate `rename` attribute", | ||
)); | ||
} | ||
|
||
attr.rename = Some(rename); | ||
} else if meta.path.is_ident("skip") { | ||
if attr.skip { | ||
return Err(syn::Error::new( | ||
meta.path.span(), | ||
"Duplicate `skip` attribute", | ||
)); | ||
} | ||
attr.skip = true; | ||
} else { | ||
panic!("Attributes other than `unit` and `rename` should not reach here"); | ||
} | ||
Ok(()) | ||
})?; | ||
Ok(attr) | ||
} | ||
_ => Err(syn::Error::new( | ||
span, | ||
r#"Unknown attribute, expected `#[doc(...)]` or `#[registrant(<key>[=value], ...)]`"#, | ||
)), | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
use super::attribute::Attribute; | ||
use crate::registrant::attribute; | ||
use quote::ToTokens; | ||
|
||
// do not derive debug since this needs "extra-traits" | ||
// feature for crate `syn`, which slows compile time | ||
// too much, and is not needed as this struct is not | ||
// public. | ||
pub struct Field { | ||
ident: syn::Ident, | ||
name: syn::LitStr, | ||
attr: Attribute, | ||
} | ||
|
||
impl Field { | ||
pub(super) fn ident(&self) -> &syn::Ident { | ||
&self.ident | ||
} | ||
|
||
pub(super) fn name(&self) -> &syn::LitStr { | ||
match &self.attr.rename { | ||
Some(rename) => rename, | ||
None => &self.name, | ||
} | ||
} | ||
|
||
pub(super) fn help(&self) -> syn::LitStr { | ||
self.attr | ||
.help | ||
.clone() | ||
.unwrap_or_else(|| syn::LitStr::new("", self.ident.span())) | ||
} | ||
|
||
pub(super) fn unit(&self) -> Option<&syn::LitStr> { | ||
self.attr.unit.as_ref() | ||
} | ||
|
||
pub(super) fn skip(&self) -> bool { | ||
self.attr.skip | ||
} | ||
} | ||
|
||
impl TryFrom<syn::Field> for Field { | ||
type Error = syn::Error; | ||
|
||
fn try_from(field: syn::Field) -> Result<Self, Self::Error> { | ||
let ident = field | ||
.ident | ||
.clone() | ||
.expect("Fields::Named should have an identifier"); | ||
let name = syn::LitStr::new(&ident.to_string(), ident.span()); | ||
let attr = field | ||
.attrs | ||
.into_iter() | ||
// ignore unknown attributes, which might be defined by another derive macros. | ||
.filter(|attr| attr.path().is_ident("doc") || attr.path().is_ident("registrant")) | ||
.try_fold(vec![], |mut acc, attr| { | ||
acc.push(syn::parse2::<Attribute>(attr.meta.into_token_stream())?); | ||
Ok::<Vec<attribute::Attribute>, syn::Error>(acc) | ||
})? | ||
.into_iter() | ||
.try_fold(Attribute::default(), |acc, attr| acc.merge(attr))?; | ||
Ok(Field { ident, name, attr }) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall we gate this dependency using a feature flag?
If so, we should also gate the
prometheus-client-derive-encode
, which should be considered as a breaking change even if the feature flag is default on.Since the latest release is
0.23.1
, the current version is0.24.0
, we can do breaking changes, but this based on the policy of the project. So the project maintainer may able to make a better call.