Skip to content

Commit 08f285f

Browse files
committed
Add #[with_removal] attribute to #[metrics] macro
`#[with_removal]` generates two additional functions that allow to remove individual label sets from a metric family as well as clear the family completely. This is useful when metrics are used for dynamic data, such as peer RTT in a P2P system. The feature is hidden behind `cfg(foundations_unstable)`, which allows us to iterate on the API in the future. Both proc-macro unit tests as well as an integration test are included in the change.
1 parent 9c7033d commit 08f285f

File tree

4 files changed

+256
-60
lines changed

4 files changed

+256
-60
lines changed

foundations-macros/src/metrics/mod.rs

Lines changed: 166 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ struct FnAttrs {
5858
doc: String,
5959
ctor: Option<ExprStruct>,
6060
optional: bool,
61+
with_removal: bool,
6162
}
6263

6364
struct FnArg {
@@ -300,6 +301,7 @@ fn metric_init(foundations: &Path, fn_: &ItemFn) -> proc_macro2::TokenStream {
300301
doc,
301302
optional,
302303
ctor,
304+
..
303305
},
304306
ident: field_name,
305307
args,
@@ -357,7 +359,13 @@ fn metric_init(foundations: &Path, fn_: &ItemFn) -> proc_macro2::TokenStream {
357359

358360
fn metric_fn(foundations: &Path, metrics_struct: &Ident, fn_: &ItemFn) -> proc_macro2::TokenStream {
359361
let ItemFn {
360-
attrs: FnAttrs { cfg, doc, .. },
362+
attrs:
363+
FnAttrs {
364+
cfg,
365+
doc,
366+
with_removal,
367+
..
368+
},
361369
fn_token,
362370
vis: fn_vis,
363371
ident: metric_name,
@@ -366,35 +374,77 @@ fn metric_fn(foundations: &Path, metrics_struct: &Ident, fn_: &ItemFn) -> proc_m
366374
ty: metric_type,
367375
} = fn_;
368376

369-
let fn_args = args.iter().map(|arg| arg.to_arg());
377+
let fn_args: Vec<_> = args.iter().map(|arg| arg.to_arg()).collect();
370378

371-
let fn_body = if args.is_empty() {
372-
quote! {
379+
let (convert_args, access_metric) = if args.is_empty() {
380+
let accessor = quote! {
373381
::std::clone::Clone::clone(&#metrics_struct.#metric_name)
374-
}
382+
};
383+
(quote! {}, accessor)
375384
} else {
376385
let label_inits = args.iter().map(|arg| arg.to_initializer());
386+
let convert = quote! {
387+
let __args = #metric_name {
388+
#(#label_inits,)*
389+
};
390+
};
377391

378-
379-
quote! {
392+
let accessor = quote! {
380393
::std::clone::Clone::clone(
381394
&#foundations::reexports_for_macros::prometools::serde::Family::get_or_create(
382395
&#metrics_struct.#metric_name,
383-
&#metric_name {
384-
#(#label_inits,)*
385-
},
396+
&__args,
386397
)
387398
)
399+
};
400+
(convert, accessor)
401+
};
402+
403+
let removal_fns = if cfg!(foundations_unstable) && *with_removal {
404+
let remove_ident = format_ident!("{metric_name}_remove");
405+
let remove_doc = LitStr::new(
406+
&format!("Removes one label set from the `{metric_name}` family."),
407+
Span::call_site(),
408+
);
409+
410+
let clear_ident = format_ident!("{metric_name}_clear");
411+
let clear_doc = LitStr::new(
412+
&format!("Removes all label sets from the `{metric_name}` family."),
413+
Span::call_site(),
414+
);
415+
416+
quote! {
417+
#[doc = #remove_doc]
418+
#(#cfg)*
419+
#fn_vis #fn_token #remove_ident(#(#fn_args,)*) #arrow_token bool {
420+
#convert_args
421+
#foundations::reexports_for_macros::prometools::serde::Family::remove(
422+
&#metrics_struct.#metric_name,
423+
&__args,
424+
)
425+
}
426+
427+
#[doc = #clear_doc]
428+
#(#cfg)*
429+
#fn_vis #fn_token #clear_ident() {
430+
#foundations::reexports_for_macros::prometools::serde::Family::clear(
431+
&#metrics_struct.#metric_name,
432+
)
433+
}
388434
}
435+
} else {
436+
quote! {}
389437
};
390438

391439
quote! {
392440
#[doc = #doc]
393441
#(#cfg)*
394442
#[must_use]
395443
#fn_vis #fn_token #metric_name(#(#fn_args,)*) #arrow_token #metric_type {
396-
#fn_body
444+
#convert_args
445+
#access_metric
397446
}
447+
#removal_fns
398448
}
399449
}
400450

@@ -713,15 +763,16 @@ mod tests {
713763
message: &'static str,
714764
error: impl Into<String>,
715765
) -> Counter {
766+
let __args = connections_errors_total {
767+
endpoint: ::std::clone::Clone::clone(endpoint),
768+
kind,
769+
message,
770+
error: ::std::convert::Into::into(error),
771+
};
716772
::std::clone::Clone::clone(
717773
&::foundations::reexports_for_macros::prometools::serde::Family::get_or_create(
718774
&__oxy_Metrics.connections_errors_total,
719-
&connections_errors_total {
720-
endpoint: ::std::clone::Clone::clone(endpoint),
721-
kind,
722-
message,
723-
error: ::std::convert::Into::into(error),
724-
},
775+
&__args,
725776
)
726777
)
727778
}
@@ -827,15 +878,111 @@ mod tests {
827878
pub fn requests_per_connection(
828879
endpoint: String,
829880
) -> Histogram {
881+
let __args = requests_per_connection { endpoint, };
830882
::std::clone::Clone::clone(
831883
&::foundations::reexports_for_macros::prometools::serde::Family::get_or_create(
832884
&__oxy_Metrics.requests_per_connection,
833-
&requests_per_connection {
834-
endpoint,
885+
&__args,
886+
)
887+
)
888+
}
889+
}
890+
};
891+
892+
assert_eq!(actual, expected);
893+
}
894+
895+
#[cfg(foundations_unstable)]
896+
#[test]
897+
fn expand_with_removal() {
898+
let attr = parse_attr! {
899+
#[metrics]
900+
};
901+
902+
let src = parse_quote! {
903+
pub(crate) mod oxy {
904+
/// Total number of requests
905+
#[with_removal]
906+
pub(crate) fn requests_total(status: u16) -> Counter;
907+
}
908+
};
909+
910+
let actual = expand_from_parsed(attr, src).to_string();
911+
912+
let expected = code_str! {
913+
pub(crate) mod oxy {
914+
use super::*;
915+
916+
#[allow(non_camel_case_types)]
917+
struct __oxy_Metrics {
918+
requests_total:
919+
::foundations::reexports_for_macros::prometools::serde::Family<
920+
requests_total,
921+
Counter,
922+
>,
923+
}
924+
925+
#[allow(non_camel_case_types)]
926+
#[derive(
927+
::std::clone::Clone,
928+
::std::cmp::Eq,
929+
::std::hash::Hash,
930+
::std::cmp::PartialEq,
931+
::foundations::reexports_for_macros::serde::Serialize,
932+
)]
933+
#[serde(crate = ":: foundations :: reexports_for_macros :: serde")]
934+
struct requests_total {
935+
status: u16,
936+
}
937+
938+
#[allow(non_upper_case_globals)]
939+
static __oxy_Metrics: ::foundations::reexports_for_macros::once_cell::sync::Lazy<__oxy_Metrics> =
940+
::foundations::reexports_for_macros::once_cell::sync::Lazy::new(|| {
941+
let registry = &mut *::foundations::telemetry::metrics::internal::Registries::get_subsystem(stringify!(oxy), false, true);
942+
943+
__oxy_Metrics {
944+
requests_total: {
945+
let metric = ::std::default::Default::default();
946+
947+
::foundations::reexports_for_macros::prometheus_client::registry::Registry::register(
948+
registry,
949+
::std::stringify!(requests_total),
950+
str::trim(" Total number of requests"),
951+
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
952+
);
953+
954+
metric
835955
},
956+
}
957+
});
958+
959+
#[doc = " Total number of requests"]
960+
#[must_use]
961+
pub(crate) fn requests_total(status: u16,) -> Counter {
962+
let __args = requests_total { status, };
963+
::std::clone::Clone::clone(
964+
&::foundations::reexports_for_macros::prometools::serde::Family::get_or_create(
965+
&__oxy_Metrics.requests_total,
966+
&__args,
836967
)
837968
)
838969
}
970+
971+
#[doc = "Removes one label set from the `requests_total` family."]
972+
pub(crate) fn requests_total_remove(status: u16,) -> bool {
973+
let __args = requests_total { status, };
974+
::foundations::reexports_for_macros::prometools::serde::Family::remove(
975+
&__oxy_Metrics.requests_total,
976+
&__args,
977+
)
978+
}
979+
980+
#[doc = "Removes all label sets from the `requests_total` family."]
981+
pub(crate) fn requests_total_clear() {
982+
::foundations::reexports_for_macros::prometools::serde::Family::clear(
983+
&__oxy_Metrics.requests_total,
984+
)
985+
}
839986
}
840987
};
841988

foundations-macros/src/metrics/parsing.rs

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
use super::{ArgAttrs, ArgMode, FnArg, FnAttrs, ItemFn, MacroArgs, Mod};
22
use crate::common::{error, parse_attr_value, parse_meta_list, Result};
33
use darling::FromMeta;
4-
use quote::ToTokens as _;
54
use syn::parse::{Parse, ParseStream};
65
use syn::punctuated::Punctuated;
76
use syn::{
8-
braced, parenthesized, AngleBracketedGenericArguments, Attribute, GenericArgument, LitBool,
9-
LitStr, PathArguments, Token, TraitBound, TraitBoundModifier, Type, TypeImplTrait,
10-
TypeParamBound,
7+
braced, parenthesized, AngleBracketedGenericArguments, Attribute, GenericArgument,
8+
PathArguments, Token, TraitBound, TraitBoundModifier, Type, TypeImplTrait, TypeParamBound,
119
};
1210

1311
const IMPL_TRAIT_ERROR: &str = "Only `impl Into<T>` is allowed";
1412

1513
const FN_ATTR_ERROR: &str =
16-
"Only `#[cfg]`, `#[doc]`, `#[ctor]` and `#[optional]` are allowed on functions";
14+
"Only `#[cfg]`, `#[doc]`, `#[ctor]`, `#[optional]`, and `#[with_removal]` are allowed on functions";
1715

1816
const DUPLICATE_CTOR_ATTR_ERROR: &str = "Duplicate `#[ctor]` attribute";
1917
const DUPLICATE_OPTIONAL_ATTR_ERROR: &str = "Duplicate `#[optional]` attribute";
18+
const DUPLICATE_WITH_REMOVAL_ATTR_ERROR: &str = "Duplicate `#[with_removal]` attribute";
2019
const DUPLICATE_SERDE_ATTR_ERROR: &str = "Duplicate `#[serde]` attribute";
2120
const DUPLICATE_SERDE_AS_ATTR_ERROR: &str = "Duplicate `#[serde_as]` attribute";
2221

2322
const ARG_ATTR_ERROR: &str = "Only `#[serde]` and `#[serde_as]` are allowed on function arguments";
23+
const WITH_REMOVAL_NO_ARGS_ERROR: &str =
24+
"`#[with_removal]` can only be used on functions with arguments";
2425

2526
impl Parse for MacroArgs {
2627
fn parse(input: ParseStream) -> Result<Self> {
@@ -58,60 +59,63 @@ impl Parse for Mod {
5859
}
5960
}
6061

61-
impl Parse for ItemFn {
62-
fn parse(input: ParseStream) -> Result<Self> {
63-
fn parse_attrs(attrs: Vec<Attribute>) -> Result<FnAttrs> {
64-
let mut cfg = vec![];
65-
let mut doc = "".to_owned();
66-
let mut ctor = None;
67-
let mut optional = None;
68-
69-
for attr in attrs {
70-
let path = attr.path();
71-
72-
if path.is_ident("cfg") {
73-
cfg.push(attr);
74-
} else if path.is_ident("doc") {
75-
doc.push_str(&parse_attr_value::<LitStr>(attr)?.value());
76-
} else if path.is_ident("ctor") {
62+
impl FnAttrs {
63+
fn from_attrs(attrs: Vec<Attribute>) -> Result<Self> {
64+
let mut cfg = vec![];
65+
let mut doc = "".to_owned();
66+
let mut ctor = None;
67+
let mut optional = None;
68+
let mut with_removal = None;
69+
70+
for attr in attrs {
71+
let path = attr.path().get_ident().map(ToString::to_string);
72+
73+
match path.as_deref() {
74+
Some("cfg") => cfg.push(attr),
75+
Some("doc") => doc.push_str(&String::from_meta(&attr.meta)?),
76+
Some("ctor") => {
7777
if ctor.is_some() {
7878
return error(&attr, DUPLICATE_CTOR_ATTR_ERROR);
7979
}
8080

8181
ctor = Some(parse_attr_value(attr)?);
82-
} else if path.is_ident("optional") {
82+
}
83+
Some("optional") => {
8384
if optional.is_some() {
8485
return error(&attr, DUPLICATE_OPTIONAL_ATTR_ERROR);
8586
}
8687

87-
if attr.to_token_stream().is_empty() {
88-
optional = Some(true);
89-
} else {
90-
optional = Some(
91-
parse_attr_value::<LitBool>(attr)
92-
.map(|l| l.value)
93-
.unwrap_or(true),
94-
);
88+
optional = Some(bool::from_meta(&attr.meta).unwrap_or(true));
89+
}
90+
Some("with_removal") => {
91+
if with_removal.is_some() {
92+
return error(&attr, DUPLICATE_WITH_REMOVAL_ATTR_ERROR);
9593
}
96-
} else {
97-
return error(&attr, FN_ATTR_ERROR);
94+
95+
with_removal = Some(bool::from_meta(&attr.meta)?);
9896
}
97+
_ => return error(&attr, FN_ATTR_ERROR),
9998
}
100-
101-
Ok(FnAttrs {
102-
cfg,
103-
doc,
104-
ctor,
105-
optional: optional.unwrap_or(false),
106-
})
10799
}
108100

109-
let attrs = parse_attrs(input.call(Attribute::parse_outer)?)?;
101+
Ok(Self {
102+
cfg,
103+
doc,
104+
ctor,
105+
optional: optional.unwrap_or(false),
106+
with_removal: with_removal.unwrap_or(false),
107+
})
108+
}
109+
}
110+
111+
impl Parse for ItemFn {
112+
fn parse(input: ParseStream) -> Result<Self> {
113+
let attrs = FnAttrs::from_attrs(input.call(Attribute::parse_outer)?)?;
110114
let vis = input.parse()?;
111115
let fn_token = input.parse()?;
112116
let ident = input.parse()?;
113117
let args_content;
114-
let _paren_token = parenthesized!(args_content in input);
118+
let paren_token = parenthesized!(args_content in input);
115119
let mut args = Punctuated::new();
116120

117121
while !args_content.is_empty() {
@@ -128,6 +132,10 @@ impl Parse for ItemFn {
128132
let ty = input.parse()?;
129133
let _semi_token = input.parse::<Token![;]>()?;
130134

135+
if attrs.with_removal && args.is_empty() {
136+
return error(&paren_token.span, WITH_REMOVAL_NO_ARGS_ERROR);
137+
}
138+
131139
Ok(ItemFn {
132140
attrs,
133141
vis,

0 commit comments

Comments
 (0)