Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(metrics): derive with dynamic scope #785

Merged
merged 4 commits into from
Jan 14, 2023
Merged
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
171 changes: 137 additions & 34 deletions crates/metrics/metrics-derive/src/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use once_cell::sync::Lazy;
use quote::{quote, ToTokens};
use regex::Regex;
use syn::{
punctuated::Punctuated, Attribute, Data, DeriveInput, Error, Lit, LitStr, MetaNameValue,
Result, Token,
punctuated::Punctuated, Attribute, Data, DeriveInput, Error, Lit, LitBool, LitStr,
MetaNameValue, Result, Token,
};

use crate::{metric::Metric, with_attrs::WithAttrs};
Expand All @@ -15,63 +15,141 @@ static METRIC_NAME_RE: Lazy<Regex> =

pub(crate) fn derive(node: &DeriveInput) -> Result<proc_macro2::TokenStream> {
let ty = &node.ident;
let vis = &node.vis;
let ident_name = ty.to_string();

let metrics_attr = parse_metrics_attr(node)?;
let metric_fields = parse_metric_fields(node)?;

let default_fields = metric_fields
.iter()
.map(|metric| {
let field_name = &metric.field.ident;
let register_stmt = metric.register_stmt(&metrics_attr)?;
Ok(quote! {
#field_name: #register_stmt,
})
})
.collect::<Result<Vec<_>>>()?;

let describe_stmts = metric_fields
.iter()
.map(|metric| metric.describe_stmt(&metrics_attr))
.collect::<Result<Vec<_>>>()?;
let describe_doc = quote! {
/// Describe all exposed metrics. Internally calls `describe_*` macros from
/// the metrics crate according to the metric type.
/// Ref: https://docs.rs/metrics/0.20.1/metrics/index.html#macros
};
let register_and_describe = match &metrics_attr.scope {
MetricsScope::Static(scope) => {
let (defaults, describes): (Vec<_>, Vec<_>) = metric_fields
.iter()
.map(|metric| {
let field_name = &metric.field.ident;
let metric_name =
format!("{}{}{}", scope.value(), metrics_attr.separator(), metric.name());
let registrar = metric.register_stmt()?;
let describe = metric.describe_stmt()?;
let description = &metric.description;
Ok((
quote! {
#field_name: #registrar(#metric_name),
},
quote! {
#describe(#metric_name, #description);
},
))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.unzip();

Ok(quote! {
impl Default for #ty {
fn default() -> Self {
Self {
#(#default_fields)*
quote! {
impl Default for #ty {
fn default() -> Self {
Self {
#(#defaults)*
}
}
}

impl #ty {
#describe_doc
#vis fn describe() {
#(#describes)*
}
}
}
}
MetricsScope::Dynamic => {
let (defaults, describes): (Vec<_>, Vec<_>) = metric_fields
.iter()
.map(|metric| {
let name = metric.name();
let separator = metrics_attr.separator();
let metric_name = quote! {
format!("{}{}{}", scope, #separator, #name)
};
let field_name = &metric.field.ident;

impl std::fmt::Debug for #ty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(#ident_name).finish()
let registrar = metric.register_stmt()?;
let describe = metric.describe_stmt()?;
let description = &metric.description;

Ok((
quote! {
#field_name: #registrar(#metric_name),
},
quote! {
#describe(#metric_name, #description);
},
))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.unzip();

quote! {
impl #ty {
/// Create new instance of metrics with provided scope.
#vis fn new(scope: &str) -> Self {
Self {
#(#defaults)*
}
}

#describe_doc
#vis fn describe(scope: &str) {
#(#describes)*
}
}
}
}
};

impl #ty {
/// Describe all exposed metrics. Internally calls `describe_*` macros from
/// the metrics crate according to the metric type.
/// Ref: https://docs.rs/metrics/0.20.1/metrics/index.html#macros
pub fn describe() {
#(#describe_stmts;)*
Ok(quote! {
#register_and_describe

impl std::fmt::Debug for #ty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(#ident_name).finish()
}
}
})
}

pub(crate) struct MetricsAttr {
pub(crate) scope: LitStr,
pub(crate) scope: MetricsScope,
pub(crate) separator: Option<LitStr>,
}

impl MetricsAttr {
const DEFAULT_SEPARATOR: &str = "_";

fn separator(&self) -> String {
match &self.separator {
Some(sep) => sep.value(),
None => MetricsAttr::DEFAULT_SEPARATOR.to_owned(),
}
}
}

pub(crate) enum MetricsScope {
Static(LitStr),
Dynamic,
}

fn parse_metrics_attr(node: &DeriveInput) -> Result<MetricsAttr> {
let metrics_attr = parse_single_required_attr(node, "metrics")?;
let parsed =
metrics_attr.parse_args_with(Punctuated::<MetaNameValue, Token![,]>::parse_terminated)?;
let (mut scope, mut separator) = (None, None);
let (mut scope, mut separator, mut dynamic) = (None, None, None);
for kv in parsed {
if kv.path.is_ident("scope") {
if scope.is_some() {
Expand All @@ -92,12 +170,30 @@ fn parse_metrics_attr(node: &DeriveInput) -> Result<MetricsAttr> {
))
}
separator = Some(separator_lit);
} else if kv.path.is_ident("dynamic") {
if dynamic.is_some() {
return Err(Error::new_spanned(kv, "Duplicate `dynamic` flag provided."))
}
dynamic = Some(parse_bool_lit(&kv.lit)?.value);
} else {
return Err(Error::new_spanned(kv, "Unsupported attribute entry."))
}
}

let scope = scope.ok_or_else(|| Error::new_spanned(node, "`scope = ..` must be set."))?;
let scope = match (scope, dynamic) {
(Some(scope), None) | (Some(scope), Some(false)) => MetricsScope::Static(scope),
(None, Some(true)) => MetricsScope::Dynamic,
(Some(_), Some(_)) => {
return Err(Error::new_spanned(node, "`scope = ..` conflicts with `dynamic = true`."))
}
_ => {
return Err(Error::new_spanned(
node,
"Either `scope = ..` or `dynamic = true` must be set.",
))
}
};

Ok(MetricsAttr { scope, separator })
}

Expand Down Expand Up @@ -213,3 +309,10 @@ fn parse_str_lit(lit: &Lit) -> Result<LitStr> {
_ => Err(Error::new_spanned(lit, "Value **must** be a string literal.")),
}
}

fn parse_bool_lit(lit: &Lit) -> Result<LitBool> {
match lit {
Lit::Bool(lit_bool) => Ok(lit_bool.to_owned()),
_ => Err(Error::new_spanned(lit, "Value **must** be a string literal.")),
}
}
58 changes: 49 additions & 9 deletions crates/metrics/metrics-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ mod with_attrs;
/// creates a [Default] implementation for the struct registering all of
/// the metrics.
///
/// Additionally, it creates a `describe()` method on the struct, which
/// Additionally, it creates a `describe` method on the struct, which
/// internally calls the describe statements for all metric fields.
///
/// Sample usage:
Expand All @@ -31,7 +31,7 @@ mod with_attrs;
///
/// #[derive(Metrics)]
/// #[metrics(scope = "metrics_custom")]
/// struct CustomMetrics {
/// pub struct CustomMetrics {
/// /// A gauge with doc comment description.
/// gauge: Gauge,
/// #[metric(rename = "second_gauge", describe = "A gauge with metric attribute description.")]
Expand All @@ -47,7 +47,7 @@ mod with_attrs;
///
/// The example above will be expanded to:
/// ```
/// struct CustomMetrics {
/// pub struct CustomMetrics {
/// /// A gauge with doc comment description.
/// gauge: metrics::Gauge,
/// gauge2: metrics::Gauge,
Expand All @@ -68,12 +68,6 @@ mod with_attrs;
/// }
/// }
///
/// impl std::fmt::Debug for CustomMetrics {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// f.debug_struct("CustomMetrics").finish()
/// }
/// }
///
/// impl CustomMetrics {
/// /// Describe all exposed metrics
/// pub fn describe() {
Expand All @@ -83,6 +77,52 @@ mod with_attrs;
/// metrics::describe_histogram!("metrics_custom_histogram", "A renamed histogram.");
/// }
/// }
///
/// impl std::fmt::Debug for CustomMetrics {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// f.debug_struct("CustomMetrics").finish()
/// }
/// }
/// ```
///
/// Similarly, you can derive metrics with "dynamic" scope,
/// meaning their scope can be set at the time of instantiation.
/// For example:
/// ```
/// use reth_metrics_derive::Metrics;
///
/// #[derive(Metrics)]
/// #[metrics(dynamic = true)]
/// pub struct DynamicScopeMetrics {
/// /// A gauge with doc comment description.
/// gauge: metrics::Gauge,
/// }
/// ```
///
/// The example with dynamic scope will expand to
/// ```
/// pub struct DynamicScopeMetrics {
/// /// A gauge with doc comment description.
/// gauge: metrics::Gauge,
/// }
///
/// impl DynamicScopeMetrics {
/// pub fn new(scope: &str) -> Self {
/// Self {
/// gauge: metrics::register_gauge!(format!("{}{}{}", scope, "_", "gauge"))
/// }
/// }
///
/// pub fn describe(scope: &str) {
/// metrics::describe_gauge!(format!("{}{}{}", scope, "_", "gauge"), "A gauge with doc comment description.");
/// }
/// }
///
/// impl std::fmt::Debug for DynamicScopeMetrics {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// f.debug_struct("DynamicScopeMetrics").finish()
/// }
/// }
/// ```
#[proc_macro_derive(Metrics, attributes(metrics, metric))]
pub fn derive_metrics(input: TokenStream) -> TokenStream {
Expand Down
28 changes: 7 additions & 21 deletions crates/metrics/metrics-derive/src/metric.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,29 @@
use quote::quote;
use syn::{Error, Field, LitStr, Result, Type};

use crate::expand::MetricsAttr;

const COUNTER_TY: &str = "Counter";
const HISTOGRAM_TY: &str = "Histogram";
const GAUGE_TY: &str = "Gauge";

pub(crate) struct Metric<'a> {
pub(crate) field: &'a Field,
description: String,
pub(crate) description: String,
rename: Option<LitStr>,
}

impl<'a> Metric<'a> {
const DEFAULT_SEPARATOR: &str = "_";

pub(crate) fn new(field: &'a Field, description: String, rename: Option<LitStr>) -> Self {
Self { field, description, rename }
}

pub(crate) fn metric_name(&self, config: &MetricsAttr) -> String {
let scope = config.scope.value();
let metric = match self.rename.as_ref() {
pub(crate) fn name(&self) -> String {
match self.rename.as_ref() {
Some(name) => name.value(),
None => self.field.ident.as_ref().map(ToString::to_string).unwrap_or_default(),
};
match config.separator.as_ref() {
Some(separator) => format!("{scope}{}{metric}", separator.value()),
None => format!("{scope}{}{metric}", Metric::DEFAULT_SEPARATOR),
}
}

pub(crate) fn register_stmt(&self, config: &MetricsAttr) -> Result<proc_macro2::TokenStream> {
let metric_name = self.metric_name(config);

pub(crate) fn register_stmt(&self) -> Result<proc_macro2::TokenStream> {
if let Type::Path(ref path_ty) = self.field.ty {
if let Some(last) = path_ty.path.segments.last() {
let registrar = match last.ident.to_string().as_str() {
Expand All @@ -44,17 +33,14 @@ impl<'a> Metric<'a> {
_ => return Err(Error::new_spanned(path_ty, "Unsupported metric type")),
};

return Ok(quote! { #registrar(#metric_name) })
return Ok(quote! { #registrar })
}
}

Err(Error::new_spanned(&self.field.ty, "Unsupported metric type"))
}

pub(crate) fn describe_stmt(&self, config: &MetricsAttr) -> Result<proc_macro2::TokenStream> {
let metric_name = self.metric_name(config);
let description = &self.description;

pub(crate) fn describe_stmt(&self) -> Result<proc_macro2::TokenStream> {
if let Type::Path(ref path_ty) = self.field.ty {
if let Some(last) = path_ty.path.segments.last() {
let descriptor = match last.ident.to_string().as_str() {
Expand All @@ -64,7 +50,7 @@ impl<'a> Metric<'a> {
_ => return Err(Error::new_spanned(path_ty, "Unsupported metric type")),
};

return Ok(quote! { #descriptor(#metric_name, #description) })
return Ok(quote! { #descriptor })
}
}

Expand Down
Loading