Skip to content
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

Override derived label value case #136

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion derive-encode/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ syn = "1"
prometheus-client = { path = "../", features = ["protobuf"] }

[lib]
proc-macro = true
proc-macro = true
133 changes: 128 additions & 5 deletions derive-encode/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::DeriveInput;
use syn::{parse::Parse, DeriveInput, Ident, LitStr, Token};

/// Derive `prometheus_client::encoding::EncodeLabelSet`.
#[proc_macro_derive(EncodeLabelSet, attributes(prometheus))]
Expand Down Expand Up @@ -88,22 +88,74 @@ pub fn derive_encode_label_set(input: TokenStream) -> TokenStream {
}

/// Derive `prometheus_client::encoding::EncodeLabelValue`.
#[proc_macro_derive(EncodeLabelValue)]
///
/// This macro only applies to `enum`s and will panic if you attempt to use it on structs.
///
/// At the enum level you can use `#[prometheus(value_case = "lower")]` or `"upper"` to set the
/// default case of the enum variants.
///
/// ```rust
/// # use prometheus_client::encoding::EncodeLabelValue;
/// #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelValue, Debug)]
/// #[prometheus(value_case = "upper")]
/// enum Method {
/// Get,
/// Put,
/// }
/// ```
///
/// Will encode to label values "GET" and "PUT" in prometheus metrics.
///
/// For variants you can use `#[prometheus(lower)]` or `#[prometheus(upper)]` to set the case for
/// only that variant.
Comment on lines +109 to +110
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a concrete use-case where one would upper or lower case a specific variant only?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not

it might be better to remove this and wait for a motivated individual to plumb serde in to label generation which could provide all manner of derive customization

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want me to remove per-variant case changing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please remove. Thank you.

#[proc_macro_derive(EncodeLabelValue, attributes(prometheus))]
pub fn derive_encode_label_value(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;

let config: LabelConfig = ast
.attrs
.iter()
.find_map(|attr| {
if attr.path.is_ident("prometheus") {
match attr.parse_args::<LabelConfig>() {
Ok(config) => Some(config),
Err(e) => panic!("invalid prometheus attribute: {e}"),
}
} else {
None
}
})
.unwrap_or_default();

let body = match ast.clone().data {
syn::Data::Struct(_) => {
panic!("Can not derive EncodeLabel for struct.")
panic!("Can not derive EncodeLabelValue for struct.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏

}
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
let match_arms: TokenStream2 = variants
.into_iter()
.map(|v| {
let ident = v.ident;

let attribute = v
.attrs
.iter()
.find(|a| a.path.is_ident("prometheus"))
.map(|a| a.parse_args::<syn::Ident>().unwrap().to_string());
let case = match attribute.as_deref() {
Some("lower") => ValueCase::Lower,
Some("upper") => ValueCase::Upper,
Some(other) => {
panic!("Provided attribute '{other}', but only 'lower' and 'upper' are supported")
}
None => config.value_case.clone(),
};

let value = case.apply(&ident);

quote! {
#name::#ident => encoder.write_str(stringify!(#ident))?,
#name::#ident => encoder.write_str(stringify!(#value))?,
}
})
.collect();
Expand All @@ -114,7 +166,7 @@ pub fn derive_encode_label_value(input: TokenStream) -> TokenStream {
}
}
}
syn::Data::Union(_) => panic!("Can not derive Encode for union."),
syn::Data::Union(_) => panic!("Can not derive EncodeLabelValue for union."),
};

let gen = quote! {
Expand All @@ -132,6 +184,77 @@ pub fn derive_encode_label_value(input: TokenStream) -> TokenStream {
gen.into()
}

#[derive(Clone)]
enum ValueCase {
Lower,
Upper,
NoChange,
}

impl ValueCase {
fn apply(&self, ident: &Ident) -> Ident {
match self {
ValueCase::Lower => Ident::new(&ident.to_string().to_lowercase(), ident.span()),
ValueCase::Upper => Ident::new(&ident.to_string().to_uppercase(), ident.span()),
ValueCase::NoChange => ident.clone(),
}
}
}

struct LabelConfig {
value_case: ValueCase,
}

impl Default for LabelConfig {
fn default() -> Self {
Self {
value_case: ValueCase::NoChange,
}
}
}

impl Parse for LabelConfig {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut config = LabelConfig::default();

while input.peek(Ident) {
let ident: Ident = input.parse()?;

match ident.to_string().as_str() {
"value_case" => {
let _: Token![=] = input.parse()?;
let case: LitStr = input.parse()?;

match case.value().as_str() {
"lower" => config.value_case = ValueCase::Lower,
"upper" => config.value_case = ValueCase::Upper,
invalid => {
return Err(syn::Error::new(
case.span(),
format!(
"value case may only be \"lower\" or \"upper\", not \"{invalid}\""
),
))
Comment on lines +229 to +237
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To de-duplicate this code with the one above in line 146, how about implementing FromStr for ValueCase?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will take me some time, but I think I can do this

}
}
}
invalid => {
return Err(syn::Error::new(
ident.span(),
format!("invalid prometheus attribute \"{invalid}\""),
))
}
}

if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
}
}

Ok(config)
}
}

// Copied from https://github.com/djc/askama (MIT and APACHE licensed) and
// modified.
static KEYWORD_IDENTIFIERS: [(&str, &str); 48] = [
Expand Down
101 changes: 101 additions & 0 deletions derive-encode/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,104 @@ fn flatten() {
+ "# EOF\n";
assert_eq!(expected, buffer);
}

#[test]
fn case_per_label() {
#[derive(EncodeLabelSet, Hash, Clone, Eq, PartialEq, Debug)]
struct Labels {
lower: EnumLabel,
upper: EnumLabel,
no_change: EnumLabel,
}

#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
enum EnumLabel {
#[prometheus(lower)]
One,
#[prometheus(upper)]
Two,
Three,
}

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.
family
.get_or_create(&Labels {
lower: EnumLabel::One,
upper: EnumLabel::Two,
no_change: EnumLabel::Three,
})
.inc();

// Encode all metrics in the registry in the text format.
let mut buffer = String::new();
encode(&mut buffer, &registry).unwrap();

let expected = "# HELP my_counter This is my counter.\n".to_owned()
+ "# TYPE my_counter counter\n"
+ "my_counter_total{lower=\"one\",upper=\"TWO\",no_change=\"Three\"} 1\n"
+ "# EOF\n";
assert_eq!(expected, buffer);
}

#[test]
fn case_whole_enum() {
#[derive(EncodeLabelSet, Hash, Clone, Eq, PartialEq, Debug)]
struct Labels {
lower: EnumLowerLabel,
upper: EnumUpperLabel,
no_change: EnumNoChangeLabel,
override_case: EnumOverrideLabel,
}

#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
#[prometheus(value_case = "lower")]
enum EnumLowerLabel {
One,
}

#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
#[prometheus(value_case = "upper")]
enum EnumUpperLabel {
Two,
}

#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
enum EnumNoChangeLabel {
Three,
}

#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
#[prometheus(value_case = "upper")]
enum EnumOverrideLabel {
#[prometheus(lower)]
Four,
}

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.
family
.get_or_create(&Labels {
lower: EnumLowerLabel::One,
upper: EnumUpperLabel::Two,
no_change: EnumNoChangeLabel::Three,
override_case: EnumOverrideLabel::Four,
})
.inc();

// Encode all metrics in the registry in the text format.
let mut buffer = String::new();
encode(&mut buffer, &registry).unwrap();

let expected = "# HELP my_counter This is my counter.\n".to_owned()
+ "# TYPE my_counter counter\n"
+ "my_counter_total{lower=\"one\",upper=\"TWO\",no_change=\"Three\",override_case=\"four\"} 1\n"
+ "# EOF\n";
assert_eq!(expected, buffer);
}