Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
WIP: Get ResultLabels to work outside of results
  • Loading branch information
Gerry Agbobada committed Apr 28, 2023
commit 2175cccd9ce758ce2661bcd531da65125d45cba4
87 changes: 60 additions & 27 deletions autometrics-macros/src/result_labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,28 @@ pub(crate) fn expand(input: DeriveInput) -> Result<TokenStream> {
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let conditional_clauses_for_labels = conditional_label_clauses(variants, enum_name)?;

// NOTE: we cannot reuse the GetResultLabel implementation in the GetLabels implementation,
// because the GetResultLabel implementation is for a type T, while the GetLabels argument
// is a reference &T, and the blanket impl of GetResultLabel for &T will be used instead of
// the implementation we just wrote.
Ok(quote! {
#[automatically_derived]
impl #impl_generics ::autometrics::__private::GetResultLabel for #enum_name #ty_generics #where_clause {
fn __autometrics_get_result_label(&self) -> &'static str {
fn __autometrics_get_result_label(&self) -> Option<&'static str> {
#(#conditional_clauses_for_labels)*
#ERROR_KEY
}
}

#[automatically_derived]
impl #impl_generics ::autometrics::__private::GetLabels for #enum_name #ty_generics #where_clause {
fn __autometrics_get_labels(&self) -> Option<::autometrics::__private::ResultAndReturnTypeLabels> {
use ::autometrics::__private::GetStaticStr;

let result_label = {
#(#conditional_clauses_for_labels)*
};

result_label.map(|label| (label, label.__autometrics_static_str()))
}
}
})
Expand All @@ -70,27 +86,35 @@ fn conditional_label_clauses(
variants: &Punctuated<Variant, Comma>,
enum_name: &Ident,
) -> Result<Vec<TokenStream>> {
variants
.iter()
.map(|variant| {
let variant_name = &variant.ident;
let variant_matcher: TokenStream = match variant.fields {
syn::Fields::Named(_) => quote! { #variant_name {..} },
syn::Fields::Unnamed(_) => quote! { #variant_name (_) },
syn::Fields::Unit => quote! { #variant_name },
};
if let Some(key) = extract_label_attribute(&variant.attrs)? {
Ok(quote! [
if ::std::matches!(self, & #enum_name :: #variant_matcher) {
return #key
}
])
} else {
// Let the code flow through the last value
Ok(quote! {})
}
})
.collect()
// Dummy first clause to write all the useful payload with 'else if's
std::iter::once(Ok(quote![if false {
None
}]))
.chain(variants.iter().map(|variant| {
let variant_name = &variant.ident;
let variant_matcher: TokenStream = match variant.fields {
syn::Fields::Named(_) => quote! { #variant_name {..} },
syn::Fields::Unnamed(_) => quote! { #variant_name (_) },
syn::Fields::Unit => quote! { #variant_name },
};
if let Some(key) = extract_label_attribute(&variant.attrs)? {
Ok(quote! [
else if ::std::matches!(self, & #enum_name :: #variant_matcher) {
Some(#key)
}
])
} else {
// Let the code flow through the last value
Ok(quote! {})
}
}))
// Fallback case: we return None
.chain(std::iter::once(Ok(quote! [
else {
None
}
])))
.collect()
}

/// Extract the wanted label from the annotation in the variant, if present.
Expand Down Expand Up @@ -119,7 +143,7 @@ fn extract_label_attribute(attrs: &[Attribute]) -> Result<Option<LitStr>> {
// Only lists are allowed
let Some(syn::NestedMeta::Meta(syn::Meta::NameValue(pair))) = list.nested.first() else {
return Some(Err(Error::new_spanned(
meta,
meta,
format!("Only `{ATTR_LABEL}({RESULT_KEY} = \"RES\")` (RES can be {OK_KEY:?} or {ERROR_KEY:?}) is supported"),
)))
};
Expand Down Expand Up @@ -150,10 +174,19 @@ fn extract_label_attribute(attrs: &[Attribute]) -> Result<Option<LitStr>> {

Some(Ok(lit_str.clone()))
},
_ => Some(Err(Error::new_spanned(
meta,
syn::Meta::NameValue(nv) if nv.path.segments.len() == 1 && nv.path.segments[0].ident == ATTR_LABEL => {
Some(Err(Error::new_spanned(
nv,
format!("Only `{ATTR_LABEL}({RESULT_KEY} = \"RES\")` (RES can be {OK_KEY:?} or {ERROR_KEY:?}) is supported"),
))),
)))
},
syn::Meta::Path(p) if p.segments.len() == 1 && p.segments[0].ident == ATTR_LABEL => {
Some(Err(Error::new_spanned(
p,
format!("Only `{ATTR_LABEL}({RESULT_KEY} = \"RES\")` (RES can be {OK_KEY:?} or {ERROR_KEY:?}) is supported"),
)))
},
_ => None,
},
Err(e) => Some(Err(Error::new_spanned(
att,
Expand Down
13 changes: 8 additions & 5 deletions autometrics/src/labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{constants::*, objectives::*};
use std::ops::Deref;

pub(crate) type Label = (&'static str, &'static str);
type ResultAndReturnTypeLabels = (&'static str, Option<&'static str>);
pub type ResultAndReturnTypeLabels = (&'static str, Option<&'static str>);

/// These are the labels used for the `build_info` metric.
pub struct BuildInfoLabels {
Expand Down Expand Up @@ -158,9 +158,12 @@ pub trait GetLabelsFromResult {
impl<T, E> GetLabelsFromResult for Result<T, E> {
fn __autometrics_get_labels(&self) -> Option<ResultAndReturnTypeLabels> {
match self {
Ok(ok) => Some((OK_KEY, ok.__autometrics_static_str())),
Ok(ok) => Some((
ok.__autometrics_get_result_label().unwrap_or(OK_KEY),
ok.__autometrics_static_str(),
)),
Err(err) => Some((
err.__autometrics_get_result_label(),
err.__autometrics_get_result_label().unwrap_or(ERROR_KEY),
err.__autometrics_static_str(),
)),
}
Expand Down Expand Up @@ -253,8 +256,8 @@ impl_trait_for_types!(GetStaticStr);
/// "result" label
pub trait GetResultLabel {
/// Return the value to use for the [result](RESULT_KEY) value in the reported metrics
fn __autometrics_get_result_label(&self) -> &'static str {
ERROR_KEY
fn __autometrics_get_result_label(&self) -> Option<&'static str> {
None
}
}
impl_trait_for_types!(GetResultLabel);
126 changes: 109 additions & 17 deletions autometrics/tests/result_labels/pass/macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,126 @@
//!
//! The goal here is to make sure that the macro has the effect we want.
//! autometrics (the library) is then responsible for orchestrating the
//! calls to `__autometrics_get_error_label` correctly when observing
//! calls to `__autometrics_get_result_label` correctly when observing
//! function call results for the metrics.
use autometrics::__private::GetResultLabel;
use autometrics::__private::{GetLabels, GetLabelsFromResult, GetResultLabel};
use autometrics_macros::ResultLabels;

#[derive(Clone)]
struct Inner {}

#[derive(ResultLabels)]
enum MyError {
#[derive(ResultLabels, Clone)]
enum MyEnum {
/// When manually marked as 'error', returning this variant will
/// _ALWAYS_ be considered as an error for Autometrics.
/// Notably, even if you return Ok(MyEnum::Empty) from a function.
#[label(result = "error")]
Empty,
/// When manually marked as 'ok', returning this variant will
/// _ALWAYS_ be considered as a succesful result for Autometrics.
/// Notably, even if you return Err(MyEnum::Empty) from a function.
#[label(result = "ok")]
ClientError {
inner: Inner,
},
ServerError(u64),
ClientError { inner: Inner },
/// Without any manual override, Autometrics will guess from the
/// context when possible to know whether something is an issue or
/// not. This means:
/// - Ok(MyEnum::AmbiguousValue(_)) is a success for Autometrics
/// - Err(MyEnum::AmbiguousValue(_)) is an error for Autometrics
/// - Just returning MyEnum::AmbiguousValue(_) won't do anything (just like returning
/// a bare primitive type like usize)
AmbiguousValue(u64),
}

fn main() {
let err = MyError::ClientError { inner: Inner {} };
assert_eq!(err.__autometrics_get_result_label(), "ok");
assert_eq!((&err).__autometrics_get_result_label(), "ok");
let is_ok = MyEnum::ClientError { inner: Inner {} };
assert_eq!(is_ok.__autometrics_get_result_label().unwrap(), "ok");
assert_eq!((&is_ok).__autometrics_get_result_label().unwrap(), "ok");
assert_eq!(is_ok.__autometrics_get_labels().unwrap().0, "ok");
assert_eq!((&is_ok).__autometrics_get_labels().unwrap().0, "ok");

let err = MyError::Empty;
assert_eq!(err.__autometrics_get_result_label(), "error");
assert_eq!((&err).__autometrics_get_result_label(), "error");
let err = MyEnum::Empty;
assert_eq!(err.__autometrics_get_result_label().unwrap(), "error");
assert_eq!((&err).__autometrics_get_result_label().unwrap(), "error");
assert_eq!(err.__autometrics_get_labels().unwrap().0, "error");
assert_eq!((&err).__autometrics_get_labels().unwrap().0, "error");

let err = MyError::ServerError(502);
assert_eq!(err.__autometrics_get_result_label(), "error");
assert_eq!((&err).__autometrics_get_result_label(), "error");
let no_idea = MyEnum::AmbiguousValue(42);
assert_eq!(no_idea.__autometrics_get_result_label(), None);
assert_eq!((&no_idea).__autometrics_get_result_label(), None);
assert_eq!(no_idea.__autometrics_get_labels(), None);
assert_eq!((&no_idea).__autometrics_get_labels(), None);

// Testing behaviour within an Ok() error variant
let ok: Result<MyEnum, ()> = Ok(is_ok.clone());
assert_eq!(
ok.__autometrics_get_labels().unwrap().0,
"ok",
"When wrapped as the Ok variant of a result, a manually marked 'ok' variant translates to 'ok'."
);
assert_eq!(
(&ok).__autometrics_get_labels().unwrap().0,
"ok",
"When wrapped as the Ok variant of a result, a manually marked 'ok' variant translates to 'ok'."
);

let ok: Result<MyEnum, ()> = Ok(no_idea.clone());
assert_eq!(
ok.__autometrics_get_labels().unwrap().0,
"ok",
"When wrapped as the Ok variant of a result, an ambiguous variant translates to 'ok'."
);
assert_eq!(
(&ok).__autometrics_get_labels().unwrap().0,
"ok",
"When wrapped as the Ok variant of a result, an ambiguous variant translates to 'ok'."
);

let err_in_ok: Result<MyEnum, ()> = Ok(err.clone());
assert_eq!(
err_in_ok.__autometrics_get_labels().unwrap().0,
"error",
"When wrapped as the Ok variant of a result, a manually marked 'error' variant translates to 'error'."
);
assert_eq!(
(&err_in_ok).__autometrics_get_labels().unwrap().0,
"error",
"When wrapped as the Ok variant of a result, a manually marked 'error' variant translates to 'error'."
);

// Testing behaviour within an Err() error variant
let ok_in_err: Result<(), MyEnum> = Err(is_ok);
assert_eq!(
ok_in_err.__autometrics_get_labels().unwrap().0,
"ok",
"When wrapped as the Err variant of a result, a manually marked 'ok' variant translates to 'ok'."
);
assert_eq!(
(&ok_in_err).__autometrics_get_labels().unwrap().0,
"ok",
"When wrapped as the Err variant of a result, a manually marked 'ok' variant translates to 'ok'."
);

let not_ok: Result<(), MyEnum> = Err(err);
assert_eq!(
not_ok.__autometrics_get_labels().unwrap().0,
"error",
"When wrapped as the Err variant of a result, a manually marked 'error' variant translates to 'error'."
);
assert_eq!(
(&not_ok).__autometrics_get_labels().unwrap().0,
"error",
"When wrapped as the Err variant of a result, a manually marked 'error' variant translates to 'error'."
);

let ambiguous: Result<(), MyEnum> = Err(no_idea);
assert_eq!(
ambiguous.__autometrics_get_labels().unwrap().0,
"error",
"When wrapped as the Err variant of a result, an ambiguous variant translates to 'error'."
);
assert_eq!(
(&ambiguous).__autometrics_get_labels().unwrap().0,
"error",
"When wrapped as the Err variant of a result, an ambiguous variant translates to 'error'."
);
}