feat(metrics): derive with dynamic scope (#785)

This commit is contained in:
Roman Krasiuk
2023-01-14 15:34:45 +02:00
committed by GitHub
parent 16ce828e27
commit d5fefa3a68
5 changed files with 204 additions and 64 deletions

View File

@ -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};
@ -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_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();
let describe_stmts = metric_fields
.iter()
.map(|metric| metric.describe_stmt(&metrics_attr))
.collect::<Result<Vec<_>>>()?;
quote! {
impl Default for #ty {
fn default() -> Self {
Self {
#(#defaults)*
}
}
}
Ok(quote! {
impl Default for #ty {
fn default() -> Self {
Self {
#(#default_fields)*
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;
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)*
}
}
}
}
};
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()
}
}
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;)*
}
}
})
}
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() {
@ -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 })
}
@ -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.")),
}
}

View File

@ -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:
@ -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.")]
@ -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,
@ -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() {
@ -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 {

View File

@ -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() {
@ -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() {
@ -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 })
}
}

View File

@ -50,3 +50,7 @@ struct CustomMetrics11;
#[derive(Metrics)]
#[metrics(random = "value")]
struct CustomMetrics12;
#[derive(Metrics)]
#[metrics(scope = "scope", dynamic = true)]
struct CustomMetrics13;

View File

@ -10,7 +10,7 @@ error: Duplicate `#[metrics(..)]` attribute provided.
11 | #[metrics()]
| ^^^^^^^^^^^^
error: `scope = ..` must be set.
error: Either `scope = ..` or `dynamic = true` must be set.
--> tests/compile-fail/metrics_attr.rs:15:1
|
15 | / #[metrics()]
@ -70,3 +70,10 @@ error: Unsupported attribute entry.
|
51 | #[metrics(random = "value")]
| ^^^^^^^^^^^^^^^^
error: `scope = ..` conflicts with `dynamic = true`.
--> tests/compile-fail/metrics_attr.rs:55:1
|
55 | / #[metrics(scope = "scope", dynamic = true)]
56 | | struct CustomMetrics13;
| |_______________________^