Skip to content
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
111 changes: 99 additions & 12 deletions foundations-macros/src/metrics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ struct ArgAttrs {

enum ArgMode {
ByValue(Type),
Clone(Type),
ToOwned(Type),
Into(Type),
}

Expand All @@ -92,9 +92,8 @@ impl FnArg {
} = self;

let ty = match &self.mode {
ArgMode::ByValue(ty) => ty,
ArgMode::Clone(ty) => ty,
ArgMode::Into(ty) => ty,
ArgMode::ByValue(ty) | ArgMode::Into(ty) => quote! { #ty },
ArgMode::ToOwned(ty) => quote! { <#ty as ::std::borrow::ToOwned>::Owned },
};

quote! { #serde_as #serde #ident #colon_token #ty }
Expand All @@ -118,8 +117,8 @@ impl FnArg {

match &self.mode {
ArgMode::ByValue(_) => quote! { #ident },
ArgMode::Clone(_) => quote! {
#ident #colon_token ::std::clone::Clone::clone(#ident)
ArgMode::ToOwned(_) => quote! {
#ident #colon_token ::std::borrow::ToOwned::to_owned(#ident)

@TheJokr TheJokr Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ToOwned has a blanket impl for any T: Clone, I think we can simplify this by using to_owned for all ArgMode::Clone types? (rename the enum variant if you want)

that would make it strictly more powerful than the current released code, and we wouldn't have to add a check for is_unsized (thus also supporting type aliases and custom unsized types).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

pushed a new version, seems smaller and is indeed functionally identical as far as I can tell.

},
ArgMode::Into(_) => quote! {
#ident #colon_token ::std::convert::Into::into(#ident)
Expand Down Expand Up @@ -287,7 +286,7 @@ fn label_set_struct(foundations: &Path, fn_: &ItemFn) -> Option<proc_macro2::Tok
let low_cardinality_impl = low_cardinality.is_present().then(|| {
let cardinality_factors = args.iter().map(|arg| {
let ty = match &arg.mode {
ArgMode::ByValue(ty) | ArgMode::Clone(ty) | ArgMode::Into(ty) => ty,
ArgMode::ByValue(ty) | ArgMode::ToOwned(ty) | ArgMode::Into(ty) => ty,
};

quote! {
Expand All @@ -305,7 +304,7 @@ fn label_set_struct(foundations: &Path, fn_: &ItemFn) -> Option<proc_macro2::Tok
let index_steps = args.iter().map(|arg| {
let ident = &arg.ident;
let ty = match &arg.mode {
ArgMode::ByValue(ty) | ArgMode::Clone(ty) | ArgMode::Into(ty) => ty,
ArgMode::ByValue(ty) | ArgMode::ToOwned(ty) | ArgMode::Into(ty) => ty,
};

quote! {
Expand Down Expand Up @@ -840,7 +839,7 @@ mod tests {
#[serde(crate = ":: foundations :: reexports_for_macros :: serde")]
struct connections_errors_total {
#[serde(rename = "endpoint_name")]
endpoint: Arc<String>,
endpoint: <Arc<String> as ::std::borrow::ToOwned>::Owned,
#[serde_as(as = "DisplayFromStr")]
kind: Kind,
message: &'static str,
Expand Down Expand Up @@ -882,7 +881,7 @@ mod tests {
error: impl Into<String>,
) -> Counter {
let __args = connections_errors_total {
endpoint: ::std::clone::Clone::clone(endpoint),
endpoint: ::std::borrow::ToOwned::to_owned(endpoint),
kind,
message,
error: ::std::convert::Into::into(error),
Expand All @@ -900,6 +899,94 @@ mod tests {
assert_eq!(actual, expected);
}

#[test]
fn expand_str_label() {
let attr = parse_attr! {
#[metrics]
};

let src = parse_quote! {
pub mod oxy {
/// Total number of connection errors
pub fn connections_errors_total(endpoint: &str) -> Counter;
}
};

let actual = expand_from_parsed(attr, src).to_string();

let expected = code_str! {
pub mod oxy {
use super::*;

#[allow(non_camel_case_types)]
struct __oxy_Metrics {
connections_errors_total:
::foundations::telemetry::metrics::Family<
connections_errors_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 connections_errors_total {
endpoint: <str as ::std::borrow::ToOwned>::Owned,
}

#[allow(non_upper_case_globals)]
static __oxy_Metrics: ::std::sync::LazyLock<__oxy_Metrics> =
::std::sync::LazyLock::new(|| {
__oxy_Metrics {
connections_errors_total: {
let metric = ::std::default::Default::default();

::foundations::telemetry::metrics::internal::register_metric(
::std::stringify!(oxy),
::std::stringify!(connections_errors_total),
::std::concat!(
::std::stringify!(oxy),
"_",
::std::stringify!(connections_errors_total),
),
str::trim(" Total number of connection errors"),
::std::clone::Clone::clone(&metric),
false,
true,
);

metric
},
}
});

#[doc = " Total number of connection errors"]
#[must_use]
pub fn connections_errors_total(
endpoint: &str,
) -> Counter {
let __args = connections_errors_total {
endpoint: ::std::borrow::ToOwned::to_owned(endpoint),
};
::std::clone::Clone::clone(
&::foundations::telemetry::metrics::Family::get_or_create(
&__oxy_Metrics.connections_errors_total,
&__args,
)
)
}
}
};

assert_eq!(actual, expected);
}

#[test]
fn expand_ctor() {
let attr = parse_attr! {
Expand Down Expand Up @@ -1216,12 +1303,12 @@ mod tests {
.expect("explicit-lifetime shared reference must parse");

let arg = &parsed.fns[0].args[0];
assert!(matches!(&arg.mode, ArgMode::Clone(Type::Path(_))));
assert!(matches!(&arg.mode, ArgMode::ToOwned(Type::Path(_))));
assert_eq!(arg.ty.to_token_stream().to_string(), "& 'static Kind");
assert_eq!(arg.to_struct_member().to_string(), "kind : Kind");
assert_eq!(
arg.to_initializer().to_string(),
"kind : :: std :: clone :: Clone :: clone (kind)"
"kind : :: std :: clone :: ToOwned :: to_owned (kind)"
);
}

Expand Down
7 changes: 4 additions & 3 deletions foundations-macros/src/metrics/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ impl Parse for ItemFn {
return error(&reference, LOW_CARDINALITY_MUT_REFERENCE_ERROR);
}

arg.mode = ArgMode::Clone((*reference.elem).clone());
arg.mode = ArgMode::ToOwned((*reference.elem).clone());
}
}
}
Expand Down Expand Up @@ -221,7 +221,8 @@ impl Parse for FnArg {

let attrs = parse_attrs(input.call(Attribute::parse_outer)?)?;

/// If the type is a reference with an eluded lifetime, then the argument should be cloned.
/// If the type is a reference with an elided lifetime, then the argument should be cloned,
/// or converted with `ToOwned` if the pointee is `str` or a slice.
/// If the type is `impl Into<Foo>`, then the Into trait should be used.
/// Otherwise, use the value directly.
fn arg_mode(ty: &Type) -> Result<ArgMode> {
Expand Down Expand Up @@ -271,7 +272,7 @@ impl Parse for FnArg {

Ok(match ty {
Type::Reference(ref_) if ref_.lifetime.is_none() => {
ArgMode::Clone((*ref_.elem).clone())
ArgMode::ToOwned((*ref_.elem).clone())
}
Type::ImplTrait(impl_) => ArgMode::Into(as_into_target(impl_)?.clone()),
ty => ArgMode::ByValue(ty.clone()),
Expand Down
6 changes: 5 additions & 1 deletion foundations/src/telemetry/metrics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ fn report_nonfatal_collect_error(err: &dyn Display) {
/// # Labels
/// Arguments of the bodyless functions become labels for that metric.
///
/// References with an elided lifetime are converted to owned values: `&str` and `&[T]` with
/// [`ToOwned`], any other `&T` with [`Clone`]. `impl Into<T>` arguments are converted with
/// [`Into`]. All other types, including `&'static` references, are stored as is.
///
/// For ordinary labeled metrics, the generated accessor returns an owned metric handle after
/// looking up the label set in its [`Family`]. When the same labels are reused, call the accessor
/// once and stash or cache that handle (cloning it when another owner needs a copy) instead of
Expand Down Expand Up @@ -460,7 +464,7 @@ fn report_nonfatal_collect_error(err: &dyn Display) {
/// pub mod my_app_metrics {
/// /// Number of active client connections
/// pub fn client_connections_active(
/// // Labels with an anonymous reference type will get cloned.
/// // Labels with an elided-lifetime reference are cloned (`&str` and `&[T]` use `ToOwned`).
/// endpoint: &Arc<String>,
/// protocol: labels::L4Protocol,
/// ip_version: labels::IpVersion,
Expand Down
28 changes: 28 additions & 0 deletions foundations/tests/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ mod regular {
pub fn dynamic(label: &'static str) -> Counter;
}

#[metrics]
mod str_labels {
pub fn requests(path: &str) -> Counter;
}

#[metrics(unprefixed)]
mod library {
pub fn calls() -> Counter;
Expand Down Expand Up @@ -64,6 +69,29 @@ fn metrics_unprefixed() {
assert!(!metrics.contains("\nlibrary_optional"));
}

#[test]
fn str_labels_are_owned_with_to_owned() {
let path = "/metrics".to_owned();

str_labels::requests(&path).inc_by(2);
str_labels::requests("/health").inc();

let settings = MetricsSettings {
service_name_format: ServiceNameFormat::MetricPrefix,
report_optional: false,
};
let metrics = collect_text(&settings);

assert!(
metrics.contains("undefined_str_labels_requests{path=\"/metrics\"} 2\n"),
"collected output was: {metrics}"
);
assert!(
metrics.contains("undefined_str_labels_requests{path=\"/health\"} 1\n"),
"collected output was: {metrics}"
);
}

#[test]
fn encode_error_is_skipped() {
encode_error::valid().inc();
Expand Down