Skip to content

Commit 688ca66

Browse files
committed
Allow setting derived label value case
When deriving label values for an enum the default case of the label matches the identifier case. This forced derived label values to use rust's case rules which may not match up with metrics exported from other programs, or metrics from other crates that export prometheus metrics. This change adds the ability to set the case of a derived value label to all-lowercase or all-uppercase for the entire struct in addition to for an individual label from the prior commit. Signed-off-by: Eric Hodel <[email protected]>
1 parent b6654ce commit 688ca66

File tree

3 files changed

+151
-21
lines changed

3 files changed

+151
-21
lines changed

derive-encode/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ syn = "1"
2020
prometheus-client = { path = "../", features = ["protobuf"] }
2121

2222
[lib]
23-
proc-macro = true
23+
proc-macro = true

derive-encode/src/lib.rs

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use proc_macro::TokenStream;
1010
use proc_macro2::TokenStream as TokenStream2;
1111
use quote::quote;
12-
use syn::{DeriveInput, Ident};
12+
use syn::{parse::Parse, DeriveInput, Ident, LitStr, Token};
1313

1414
/// Derive `prometheus_client::encoding::EncodeLabelSet`.
1515
#[proc_macro_derive(EncodeLabelSet, attributes(prometheus))]
@@ -87,18 +87,27 @@ pub fn derive_encode_label_set(input: TokenStream) -> TokenStream {
8787
gen.into()
8888
}
8989

90-
enum ValueCase {
91-
Lower,
92-
Upper,
93-
NoChange,
94-
}
95-
9690
/// Derive `prometheus_client::encoding::EncodeLabelValue`.
9791
#[proc_macro_derive(EncodeLabelValue, attributes(prometheus))]
9892
pub fn derive_encode_label_value(input: TokenStream) -> TokenStream {
9993
let ast: DeriveInput = syn::parse(input).unwrap();
10094
let name = &ast.ident;
10195

96+
let config: LabelConfig = ast
97+
.attrs
98+
.iter()
99+
.find_map(|attr| {
100+
if attr.path.is_ident("prometheus") {
101+
match attr.parse_args::<LabelConfig>() {
102+
Ok(config) => Some(config),
103+
Err(e) => panic!("invalid prometheus attribute: {e}"),
104+
}
105+
} else {
106+
None
107+
}
108+
})
109+
.unwrap_or_default();
110+
102111
let body = match ast.clone().data {
103112
syn::Data::Struct(_) => {
104113
panic!("Can not derive EncodeLabelValue for struct.")
@@ -120,20 +129,10 @@ pub fn derive_encode_label_value(input: TokenStream) -> TokenStream {
120129
Some(other) => {
121130
panic!("Provided attribute '{other}', but only 'lower' and 'upper' are supported")
122131
}
123-
None => ValueCase::NoChange,
132+
None => config.value_case.clone(),
124133
};
125134

126-
let value = match case {
127-
ValueCase::Lower => {
128-
Ident::new(&ident.to_string().to_lowercase(), ident.span())
129-
},
130-
ValueCase::Upper => {
131-
Ident::new(&ident.to_string().to_uppercase(), ident.span())
132-
},
133-
ValueCase::NoChange => {
134-
ident.clone()
135-
}
136-
};
135+
let value = case.apply(&ident);
137136

138137
quote! {
139138
#name::#ident => encoder.write_str(stringify!(#value))?,
@@ -165,6 +164,77 @@ pub fn derive_encode_label_value(input: TokenStream) -> TokenStream {
165164
gen.into()
166165
}
167166

167+
#[derive(Clone)]
168+
enum ValueCase {
169+
Lower,
170+
Upper,
171+
NoChange,
172+
}
173+
174+
impl ValueCase {
175+
fn apply(&self, ident: &Ident) -> Ident {
176+
match self {
177+
ValueCase::Lower => Ident::new(&ident.to_string().to_lowercase(), ident.span()),
178+
ValueCase::Upper => Ident::new(&ident.to_string().to_uppercase(), ident.span()),
179+
ValueCase::NoChange => ident.clone(),
180+
}
181+
}
182+
}
183+
184+
struct LabelConfig {
185+
value_case: ValueCase,
186+
}
187+
188+
impl Default for LabelConfig {
189+
fn default() -> Self {
190+
Self {
191+
value_case: ValueCase::NoChange,
192+
}
193+
}
194+
}
195+
196+
impl Parse for LabelConfig {
197+
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
198+
let mut config = LabelConfig::default();
199+
200+
while input.peek(Ident) {
201+
let ident: Ident = input.parse()?;
202+
203+
match ident.to_string().as_str() {
204+
"value_case" => {
205+
let _: Token![=] = input.parse()?;
206+
let case: LitStr = input.parse()?;
207+
208+
match case.value().as_str() {
209+
"lower" => config.value_case = ValueCase::Lower,
210+
"upper" => config.value_case = ValueCase::Upper,
211+
invalid => {
212+
return Err(syn::Error::new(
213+
case.span(),
214+
format!(
215+
"value case may only be \"lower\" or \"upper\", not \"{invalid}\""
216+
),
217+
))
218+
}
219+
}
220+
}
221+
invalid => {
222+
return Err(syn::Error::new(
223+
ident.span(),
224+
format!("invalid prometheus attribute \"{invalid}\""),
225+
))
226+
}
227+
}
228+
229+
if input.peek(Token![,]) {
230+
let _: Token![,] = input.parse()?;
231+
}
232+
}
233+
234+
Ok(config)
235+
}
236+
}
237+
168238
// Copied from https://github.com/djc/askama (MIT and APACHE licensed) and
169239
// modified.
170240
static KEYWORD_IDENTIFIERS: [(&str, &str); 48] = [

derive-encode/tests/lib.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,14 @@ fn flatten() {
175175
}
176176

177177
#[test]
178-
fn case() {
178+
fn case_per_label() {
179179
#[derive(EncodeLabelSet, Hash, Clone, Eq, PartialEq, Debug)]
180180
struct Labels {
181181
lower: EnumLabel,
182182
upper: EnumLabel,
183183
no_change: EnumLabel,
184184
}
185+
185186
#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
186187
enum EnumLabel {
187188
#[prometheus(lower)]
@@ -214,3 +215,62 @@ fn case() {
214215
+ "# EOF\n";
215216
assert_eq!(expected, buffer);
216217
}
218+
219+
#[test]
220+
fn case_whole_enum() {
221+
#[derive(EncodeLabelSet, Hash, Clone, Eq, PartialEq, Debug)]
222+
struct Labels {
223+
lower: EnumLowerLabel,
224+
upper: EnumUpperLabel,
225+
no_change: EnumNoChangeLabel,
226+
override_case: EnumOverrideLabel,
227+
}
228+
229+
#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
230+
#[prometheus(value_case = "lower")]
231+
enum EnumLowerLabel {
232+
One,
233+
}
234+
235+
#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
236+
#[prometheus(value_case = "upper")]
237+
enum EnumUpperLabel {
238+
Two,
239+
}
240+
241+
#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
242+
enum EnumNoChangeLabel {
243+
Three,
244+
}
245+
246+
#[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)]
247+
#[prometheus(value_case = "upper")]
248+
enum EnumOverrideLabel {
249+
#[prometheus(lower)]
250+
Four,
251+
}
252+
253+
let mut registry = Registry::default();
254+
let family = Family::<Labels, Counter>::default();
255+
registry.register("my_counter", "This is my counter", family.clone());
256+
257+
// Record a single HTTP GET request.
258+
family
259+
.get_or_create(&Labels {
260+
lower: EnumLowerLabel::One,
261+
upper: EnumUpperLabel::Two,
262+
no_change: EnumNoChangeLabel::Three,
263+
override_case: EnumOverrideLabel::Four,
264+
})
265+
.inc();
266+
267+
// Encode all metrics in the registry in the text format.
268+
let mut buffer = String::new();
269+
encode(&mut buffer, &registry).unwrap();
270+
271+
let expected = "# HELP my_counter This is my counter.\n".to_owned()
272+
+ "# TYPE my_counter counter\n"
273+
+ "my_counter_total{lower=\"one\",upper=\"TWO\",no_change=\"Three\",override_case=\"four\"} 1\n"
274+
+ "# EOF\n";
275+
assert_eq!(expected, buffer);
276+
}

0 commit comments

Comments
 (0)