diff --git a/Cargo.toml b/Cargo.toml index 6dc8dbf..60034b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,18 @@ publish = false [profile.release] debug = 1 +[workspace.lints.rust.unexpected_cfgs] +level = "warn" +check-cfg = [ + 'cfg(foundations_generic_telemetry_wrapper)', + 'cfg(foundations_unstable)', + 'cfg(tokio_unstable)', + # for docs.rs builds only + 'cfg(foundations_docsrs)', + # slog feature + 'cfg(integer128)', +] + [workspace.dependencies] anyhow = "1.0.75" foundations = { version = "5.3.0", path = "./foundations" } @@ -53,7 +65,7 @@ pin-project-lite = "0.2.16" proc-macro2 = { version = "1", default-features = false } prometheus = { version = "0.14", default-features = false } prometheus-client = "0.18" -prometools = "0.2.2" +prometools = "0.2.3" rand = "0.9" percent-encoding = "2.3" quote = "1" diff --git a/foundations-macros/Cargo.toml b/foundations-macros/Cargo.toml index f9961fa..bbcab06 100644 --- a/foundations-macros/Cargo.toml +++ b/foundations-macros/Cargo.toml @@ -15,6 +15,9 @@ settings_deny_unknown_fields_by_default = [] [lib] proc-macro = true +[lints] +workspace = true + [dependencies] darling = { workspace = true } proc-macro2 = { workspace = true } diff --git a/foundations-macros/src/metrics/mod.rs b/foundations-macros/src/metrics/mod.rs index ee8f964..607ea6d 100644 --- a/foundations-macros/src/metrics/mod.rs +++ b/foundations-macros/src/metrics/mod.rs @@ -58,6 +58,7 @@ struct FnAttrs { doc: String, ctor: Option, optional: bool, + with_removal: bool, } struct FnArg { @@ -80,6 +81,52 @@ enum ArgMode { Into(Type), } +impl FnArg { + fn to_struct_member(&self) -> proc_macro2::TokenStream { + let Self { + attrs: ArgAttrs { serde, serde_as }, + ident, + colon_token, + .. + } = self; + + let ty = match &self.mode { + ArgMode::ByValue(ty) => ty, + ArgMode::Clone(ty) => ty, + ArgMode::Into(ty) => ty, + }; + + quote! { #serde_as #serde #ident #colon_token #ty } + } + + fn to_arg(&self) -> proc_macro2::TokenStream { + let Self { + ident, + colon_token, + ty, + .. + } = self; + + quote! { #ident #colon_token #ty } + } + + fn to_initializer(&self) -> proc_macro2::TokenStream { + let Self { + ident, colon_token, .. + } = self; + + match &self.mode { + ArgMode::ByValue(_) => quote! { #ident }, + ArgMode::Clone(_) => quote! { + #ident #colon_token ::std::clone::Clone::clone(#ident) + }, + ArgMode::Into(_) => quote! { + #ident #colon_token ::std::convert::Into::into(#ident) + }, + } + } +} + pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream { let args = parse_macro_input!(args as MacroArgs); let mod_ = parse_macro_input!(item as Mod); @@ -224,25 +271,7 @@ fn label_set_struct(foundations: &Path, fn_: &ItemFn) -> Option ty, - ArgMode::Clone(ty) => ty, - ArgMode::Into(ty) => ty, - }; - - quote! { #serde_as #serde #label_name #colon_token #label_type } - }); + let labels = args.iter().map(|arg| arg.to_struct_member()); Some(quote! { #(#cfg)* @@ -270,6 +299,7 @@ fn metric_init(foundations: &Path, fn_: &ItemFn) -> proc_macro2::TokenStream { doc, optional, ctor, + .. }, ident: field_name, args, @@ -327,7 +357,13 @@ fn metric_init(foundations: &Path, fn_: &ItemFn) -> proc_macro2::TokenStream { fn metric_fn(foundations: &Path, metrics_struct: &Ident, fn_: &ItemFn) -> proc_macro2::TokenStream { let ItemFn { - attrs: FnAttrs { cfg, doc, .. }, + attrs: + FnAttrs { + cfg, + doc, + with_removal, + .. + }, fn_token, vis: fn_vis, ident: metric_name, @@ -336,51 +372,66 @@ fn metric_fn(foundations: &Path, metrics_struct: &Ident, fn_: &ItemFn) -> proc_m ty: metric_type, } = fn_; - let fn_args = args.iter().map(|arg| { - let FnArg { - ident: arg_name, - colon_token, - ty: arg_ty, - .. - } = arg; + let fn_args: Vec<_> = args.iter().map(|arg| arg.to_arg()).collect(); - quote! { #arg_name #colon_token #arg_ty } - }); - - let fn_body = if args.is_empty() { - quote! { + let (convert_args, access_metric) = if args.is_empty() { + let accessor = quote! { ::std::clone::Clone::clone(&#metrics_struct.#metric_name) - } + }; + (quote! {}, accessor) } else { - let label_inits = args.iter().map(|arg| { - let FnArg { - ident: arg_name, - colon_token, - mode, - .. - } = arg; - - match mode { - ArgMode::ByValue(_) => quote! { #arg_name }, - ArgMode::Clone(_) => { - quote! { #arg_name #colon_token ::std::clone::Clone::clone(#arg_name) } - } - ArgMode::Into(_) => { - quote! { #arg_name #colon_token ::std::convert::Into::into(#arg_name) } - } - } - }); + let label_inits = args.iter().map(|arg| arg.to_initializer()); + let convert = quote! { + let __args = #metric_name { + #(#label_inits,)* + }; + }; - quote! { + let accessor = quote! { ::std::clone::Clone::clone( &#foundations::reexports_for_macros::prometools::serde::Family::get_or_create( &#metrics_struct.#metric_name, - &#metric_name { - #(#label_inits,)* - }, + &__args, ) ) + }; + (convert, accessor) + }; + + let removal_fns = if cfg!(foundations_unstable) && *with_removal { + let remove_ident = format_ident!("{metric_name}_remove"); + let remove_doc = LitStr::new( + &format!("Removes one label set from the `{metric_name}` family."), + Span::call_site(), + ); + + let clear_ident = format_ident!("{metric_name}_clear"); + let clear_doc = LitStr::new( + &format!("Removes all label sets from the `{metric_name}` family."), + Span::call_site(), + ); + + quote! { + #[doc = #remove_doc] + #(#cfg)* + #fn_vis #fn_token #remove_ident(#(#fn_args,)*) #arrow_token bool { + #convert_args + #foundations::reexports_for_macros::prometools::serde::Family::remove( + &#metrics_struct.#metric_name, + &__args, + ) + } + + #[doc = #clear_doc] + #(#cfg)* + #fn_vis #fn_token #clear_ident() { + #foundations::reexports_for_macros::prometools::serde::Family::clear( + &#metrics_struct.#metric_name, + ) + } } + } else { + quote! {} }; quote! { @@ -388,8 +439,10 @@ fn metric_fn(foundations: &Path, metrics_struct: &Ident, fn_: &ItemFn) -> proc_m #(#cfg)* #[must_use] #fn_vis #fn_token #metric_name(#(#fn_args,)*) #arrow_token #metric_type { - #fn_body + #convert_args + #access_metric } + #removal_fns } } @@ -708,15 +761,16 @@ mod tests { message: &'static str, error: impl Into, ) -> Counter { + let __args = connections_errors_total { + endpoint: ::std::clone::Clone::clone(endpoint), + kind, + message, + error: ::std::convert::Into::into(error), + }; ::std::clone::Clone::clone( &::foundations::reexports_for_macros::prometools::serde::Family::get_or_create( &__oxy_Metrics.connections_errors_total, - &connections_errors_total { - endpoint: ::std::clone::Clone::clone(endpoint), - kind, - message, - error: ::std::convert::Into::into(error), - }, + &__args, ) ) } @@ -822,15 +876,111 @@ mod tests { pub fn requests_per_connection( endpoint: String, ) -> Histogram { + let __args = requests_per_connection { endpoint, }; ::std::clone::Clone::clone( &::foundations::reexports_for_macros::prometools::serde::Family::get_or_create( &__oxy_Metrics.requests_per_connection, - &requests_per_connection { - endpoint, + &__args, + ) + ) + } + } + }; + + assert_eq!(actual, expected); + } + + #[cfg(foundations_unstable)] + #[test] + fn expand_with_removal() { + let attr = parse_attr! { + #[metrics] + }; + + let src = parse_quote! { + pub(crate) mod oxy { + /// Total number of requests + #[with_removal] + pub(crate) fn requests_total(status: u16) -> Counter; + } + }; + + let actual = expand_from_parsed(attr, src).to_string(); + + let expected = code_str! { + pub(crate) mod oxy { + use super::*; + + #[allow(non_camel_case_types)] + struct __oxy_Metrics { + requests_total: + ::foundations::reexports_for_macros::prometools::serde::Family< + requests_total, + Counter, + >, + } + + #[allow(non_camel_case_types)] + #[derive( + ::std::clone::Clone, + ::std::cmp::Eq, + ::std::hash::Hash, + ::std::cmp::PartialEq, + ::foundations::reexports_for_macros::serde::Serialize, + )] + #[serde(crate = ":: foundations :: reexports_for_macros :: serde")] + struct requests_total { + status: u16, + } + + #[allow(non_upper_case_globals)] + static __oxy_Metrics: ::std::sync::LazyLock<__oxy_Metrics> = + ::std::sync::LazyLock::new(|| { + let registry = &mut *::foundations::telemetry::metrics::internal::Registries::get_subsystem(stringify!(oxy), false, true); + + __oxy_Metrics { + requests_total: { + let metric = ::std::default::Default::default(); + + ::foundations::reexports_for_macros::prometheus_client::registry::Registry::register( + registry, + ::std::stringify!(requests_total), + str::trim(" Total number of requests"), + ::std::boxed::Box::new(::std::clone::Clone::clone(&metric)) + ); + + metric }, + } + }); + + #[doc = " Total number of requests"] + #[must_use] + pub(crate) fn requests_total(status: u16,) -> Counter { + let __args = requests_total { status, }; + ::std::clone::Clone::clone( + &::foundations::reexports_for_macros::prometools::serde::Family::get_or_create( + &__oxy_Metrics.requests_total, + &__args, ) ) } + + #[doc = "Removes one label set from the `requests_total` family."] + pub(crate) fn requests_total_remove(status: u16,) -> bool { + let __args = requests_total { status, }; + ::foundations::reexports_for_macros::prometools::serde::Family::remove( + &__oxy_Metrics.requests_total, + &__args, + ) + } + + #[doc = "Removes all label sets from the `requests_total` family."] + pub(crate) fn requests_total_clear() { + ::foundations::reexports_for_macros::prometools::serde::Family::clear( + &__oxy_Metrics.requests_total, + ) + } } }; diff --git a/foundations-macros/src/metrics/parsing.rs b/foundations-macros/src/metrics/parsing.rs index 60b8961..bf749a6 100644 --- a/foundations-macros/src/metrics/parsing.rs +++ b/foundations-macros/src/metrics/parsing.rs @@ -1,26 +1,27 @@ use super::{ArgAttrs, ArgMode, FnArg, FnAttrs, ItemFn, MacroArgs, Mod}; use crate::common::{error, parse_attr_value, parse_meta_list, Result}; use darling::FromMeta; -use quote::ToTokens as _; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::{ - braced, parenthesized, AngleBracketedGenericArguments, Attribute, GenericArgument, LitBool, - LitStr, PathArguments, Token, TraitBound, TraitBoundModifier, Type, TypeImplTrait, - TypeParamBound, + braced, parenthesized, AngleBracketedGenericArguments, Attribute, GenericArgument, + PathArguments, Token, TraitBound, TraitBoundModifier, Type, TypeImplTrait, TypeParamBound, }; const IMPL_TRAIT_ERROR: &str = "Only `impl Into` is allowed"; const FN_ATTR_ERROR: &str = - "Only `#[cfg]`, `#[doc]`, `#[ctor]` and `#[optional]` are allowed on functions"; + "Only `#[cfg]`, `#[doc]`, `#[ctor]`, `#[optional]`, and `#[with_removal]` are allowed on functions"; const DUPLICATE_CTOR_ATTR_ERROR: &str = "Duplicate `#[ctor]` attribute"; const DUPLICATE_OPTIONAL_ATTR_ERROR: &str = "Duplicate `#[optional]` attribute"; +const DUPLICATE_WITH_REMOVAL_ATTR_ERROR: &str = "Duplicate `#[with_removal]` attribute"; const DUPLICATE_SERDE_ATTR_ERROR: &str = "Duplicate `#[serde]` attribute"; const DUPLICATE_SERDE_AS_ATTR_ERROR: &str = "Duplicate `#[serde_as]` attribute"; const ARG_ATTR_ERROR: &str = "Only `#[serde]` and `#[serde_as]` are allowed on function arguments"; +const WITH_REMOVAL_NO_ARGS_ERROR: &str = + "`#[with_removal]` can only be used on functions with arguments"; impl Parse for MacroArgs { fn parse(input: ParseStream) -> Result { @@ -58,60 +59,63 @@ impl Parse for Mod { } } -impl Parse for ItemFn { - fn parse(input: ParseStream) -> Result { - fn parse_attrs(attrs: Vec) -> Result { - let mut cfg = vec![]; - let mut doc = "".to_owned(); - let mut ctor = None; - let mut optional = None; - - for attr in attrs { - let path = attr.path(); - - if path.is_ident("cfg") { - cfg.push(attr); - } else if path.is_ident("doc") { - doc.push_str(&parse_attr_value::(attr)?.value()); - } else if path.is_ident("ctor") { +impl FnAttrs { + fn from_attrs(attrs: Vec) -> Result { + let mut cfg = vec![]; + let mut doc = "".to_owned(); + let mut ctor = None; + let mut optional = None; + let mut with_removal = None; + + for attr in attrs { + let path = attr.path().get_ident().map(ToString::to_string); + + match path.as_deref() { + Some("cfg") => cfg.push(attr), + Some("doc") => doc.push_str(&String::from_meta(&attr.meta)?), + Some("ctor") => { if ctor.is_some() { return error(&attr, DUPLICATE_CTOR_ATTR_ERROR); } ctor = Some(parse_attr_value(attr)?); - } else if path.is_ident("optional") { + } + Some("optional") => { if optional.is_some() { return error(&attr, DUPLICATE_OPTIONAL_ATTR_ERROR); } - if attr.to_token_stream().is_empty() { - optional = Some(true); - } else { - optional = Some( - parse_attr_value::(attr) - .map(|l| l.value) - .unwrap_or(true), - ); + optional = Some(bool::from_meta(&attr.meta).unwrap_or(true)); + } + Some("with_removal") => { + if with_removal.is_some() { + return error(&attr, DUPLICATE_WITH_REMOVAL_ATTR_ERROR); } - } else { - return error(&attr, FN_ATTR_ERROR); + + with_removal = Some(bool::from_meta(&attr.meta)?); } + _ => return error(&attr, FN_ATTR_ERROR), } - - Ok(FnAttrs { - cfg, - doc, - ctor, - optional: optional.unwrap_or(false), - }) } - let attrs = parse_attrs(input.call(Attribute::parse_outer)?)?; + Ok(Self { + cfg, + doc, + ctor, + optional: optional.unwrap_or(false), + with_removal: with_removal.unwrap_or(false), + }) + } +} + +impl Parse for ItemFn { + fn parse(input: ParseStream) -> Result { + let attrs = FnAttrs::from_attrs(input.call(Attribute::parse_outer)?)?; let vis = input.parse()?; let fn_token = input.parse()?; let ident = input.parse()?; let args_content; - let _paren_token = parenthesized!(args_content in input); + let paren_token = parenthesized!(args_content in input); let mut args = Punctuated::new(); while !args_content.is_empty() { @@ -128,6 +132,10 @@ impl Parse for ItemFn { let ty = input.parse()?; let _semi_token = input.parse::()?; + if attrs.with_removal && args.is_empty() { + return error(&paren_token.span, WITH_REMOVAL_NO_ARGS_ERROR); + } + Ok(ItemFn { attrs, vis, diff --git a/foundations-macros/src/span_fn.rs b/foundations-macros/src/span_fn.rs index 4a88d75..227a04c 100644 --- a/foundations-macros/src/span_fn.rs +++ b/foundations-macros/src/span_fn.rs @@ -1,6 +1,3 @@ -// NOTE: required to allow `foundations_generic_telemetry_wrapper` cfg -#![allow(unexpected_cfgs)] - use crate::common::parse_optional_trailing_meta_list; use darling::FromMeta; use proc_macro::TokenStream; diff --git a/foundations/Cargo.toml b/foundations/Cargo.toml index 08d0ea5..e068dde 100644 --- a/foundations/Cargo.toml +++ b/foundations/Cargo.toml @@ -181,6 +181,9 @@ rustdoc-args = ["--cfg", "foundations_docsrs", "--cfg", "tokio_unstable", "--cfg # to rustc, or else dependencies will not be enabled, and the docs build will fail. rustc-args = ["--cfg", "tokio_unstable", "--cfg", "foundations_unstable", "--cfg", "foundations_generic_telemetry_wrapper"] +[lints] +workspace = true + [dependencies] anyhow = { workspace = true, features = ["backtrace", "std"] } foundations-macros = { workspace = true, optional = true, default-features = false } diff --git a/foundations/src/lib.rs b/foundations/src/lib.rs index 391a634..8599d5d 100644 --- a/foundations/src/lib.rs +++ b/foundations/src/lib.rs @@ -64,9 +64,6 @@ //! [OpenTelemetry]: https://opentelemetry.io/ //! [gRPC]: https://grpc.io/ //! [`settings`]: crate::settings::Settings - -// NOTE: required to allow cfgs like `tokio_unstable` on nightly which is used in tests. -#![allow(unexpected_cfgs)] #![warn(missing_docs)] #![cfg_attr(foundations_docsrs, feature(doc_cfg))] diff --git a/foundations/src/telemetry/metrics/mod.rs b/foundations/src/telemetry/metrics/mod.rs index d487ffc..2a86df1 100644 --- a/foundations/src/telemetry/metrics/mod.rs +++ b/foundations/src/telemetry/metrics/mod.rs @@ -91,6 +91,22 @@ pub fn collect(settings: &MetricsSettings) -> Result { /// Can be used for heavy-weight metrics (e.g. with high cardinality) that don't need to be reported /// on a regular basis. /// +/// ## `#[with_removal]` (unstable) +/// +/// **This feature is unstable and becomes a noop without `cfg(foundations_unstable)`.** +/// +/// Metrics with labels make up a shared [`Family`]. Occasionally, it can be useful to +/// remove one or all existing metrics from a family. This functionality is provided by +/// the `#[with_removal]` attribute. Single metrics (without labels) do not support this +/// argument. +/// +/// If the attribute is present on a metric function, two additional functions are +/// generated in addition to the metric itself. These are called `_remove` and +/// `_clear`. The `_remove` variant takes the same arguments as the original +/// function and removes that instance from the family. It returns a boolean indicating +/// whether the labels were present before. The `_clear` variant takes no arguments +/// and removes all existing metrics from the family. +/// /// # Example /// /// ``` @@ -194,6 +210,10 @@ pub fn collect(settings: &MetricsSettings) -> Result { /// /// Number of Proxy-Status serialization errors /// // Metrics with no labels are also obviously supported. /// pub fn proxy_status_serialization_error_count() -> Counter; +/// +/// /// Number of HTTP requests +/// #[with_removal] +/// pub fn requests_total(endpoint: &Arc) -> Counter; /// } /// /// fn usage() { @@ -217,8 +237,15 @@ pub fn collect(settings: &MetricsSettings) -> Result { /// client_connections_active.inc(); /// /// my_app_metrics::proxy_status_serialization_error_count().inc(); +/// my_app_metrics::requests_total(&endpoint).inc(); /// /// client_connections_active.dec(); +/// +/// # #[cfg(foundations_unstable)] { +/// my_app_metrics::requests_total_remove(&endpoint); +/// // Or remove all existing instances: +/// my_app_metrics::requests_total_clear(); +/// # } /// } /// # } /// ``` diff --git a/foundations/tests/metrics.rs b/foundations/tests/metrics.rs index ee94c69..aeec974 100644 --- a/foundations/tests/metrics.rs +++ b/foundations/tests/metrics.rs @@ -6,6 +6,9 @@ mod regular { pub fn requests() -> Counter; #[optional] pub fn optional() -> Counter; + #[cfg(foundations_unstable)] + #[with_removal] + pub fn dynamic(label: &'static str) -> Counter; } #[metrics(unprefixed)] @@ -22,6 +25,16 @@ fn metrics_unprefixed() { library::calls().inc(); library::optional().inc(); + #[cfg(foundations_unstable)] + { + regular::dynamic("foo").inc(); + regular::dynamic("bar").inc(); + + assert!(regular::dynamic_remove("foo")); + assert!(!regular::dynamic_remove("baz")); + regular::dynamic_clear(); + } + let settings = MetricsSettings { service_name_format: ServiceNameFormat::MetricPrefix, report_optional: false, @@ -31,6 +44,7 @@ fn metrics_unprefixed() { // Global prefix defaults to "undefined" if not initialized assert!(metrics.contains("\nundefined_regular_requests 1\n")); assert!(!metrics.contains("\nundefined_regular_optional")); + assert!(!metrics.contains("\nundefined_regular_dynamic")); assert!(metrics.contains("\nlibrary_calls 1\n")); assert!(!metrics.contains("\nlibrary_optional")); }