Skip to content
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = "2.0"
syn = { version = "2.0", features = ["full"] }

[dev-dependencies]
metrics = "0.24.0"
Expand Down
188 changes: 161 additions & 27 deletions src/expand.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use crate::{metric::Metric, with_attrs::WithAttrs};
use crate::{
metric::{LabelPair, Metric},
with_attrs::WithAttrs,
};
use quote::{quote, ToTokens};
use syn::{
punctuated::Punctuated, Attribute, Data, DeriveInput, Error, Expr, Field, Lit, LitBool, LitStr,
Meta, MetaNameValue, Result, Token,
punctuated::Punctuated, Attribute, Data, DeriveInput, Error, Expr, ExprTuple, Field, Lit,
LitBool, LitStr, Meta, MetaNameValue, Result, Token,
};

enum MetricField<'a> {
Expand All @@ -26,6 +29,18 @@ pub(crate) fn derive(node: &DeriveInput) -> Result<proc_macro2::TokenStream> {
let metrics_attr = parse_metrics_attr(node)?;
let metric_fields = parse_metric_fields(node)?;

let global_labels_init = if metrics_attr.labels.is_empty() {
quote! {}
} else {
let label_keys = metrics_attr.labels.iter().map(|(k, _)| k);
let label_values = metrics_attr.labels.iter().map(|(_, v)| v);
quote! {
__labels.extend([
#(metrics::Label::new(#label_keys, #label_values)),*
]);
}
};

let register_and_describe = match &metrics_attr.scope {
MetricsScope::Static(scope) => {
let mut field_inits = Vec::with_capacity(metric_fields.len());
Expand All @@ -45,11 +60,33 @@ pub(crate) fn derive(node: &DeriveInput) -> Result<proc_macro2::TokenStream> {
let register_method = metric.register_method()?;
let describe_method = metric.describe_method()?;
let description = &metric.description;

let field_init = if metric.labels.is_empty() {
quote! {
__recorder.#register_method(
&metrics::Key::from_parts(#metric_name, __labels.clone()),
__metadata,
)
}
} else {
let label_keys = metric.labels.iter().map(|(k, _)| k);
let label_values = metric.labels.iter().map(|(_, v)| v);
quote! {
{
let mut __field_labels = __labels.clone();
__field_labels.extend([
#(metrics::Label::new(#label_keys, #label_values)),*
]);
__recorder.#register_method(
&metrics::Key::from_parts(#metric_name, __field_labels),
__metadata,
)
}
}
};

field_inits.push(quote! {
#field_name: __recorder.#register_method(
&metrics::Key::from_parts(#metric_name, __labels.clone()),
__metadata,
),
#field_name: #field_init,
});
describes.push(quote! {
__recorder.#describe_method(
Expand Down Expand Up @@ -108,7 +145,9 @@ pub(crate) fn derive(node: &DeriveInput) -> Result<proc_macro2::TokenStream> {
::core::option::Option::Some(module_path!()),
);
let __metadata = &__METADATA;
let __labels = labels;
#[allow(unused_mut)]
let mut __labels = labels;
#global_labels_init
Self {
#(#field_inits)*
}
Expand Down Expand Up @@ -162,14 +201,38 @@ pub(crate) fn derive(node: &DeriveInput) -> Result<proc_macro2::TokenStream> {
let describe_method = metric.describe_method()?;
let description = &metric.description;

let field_init = if metric.labels.is_empty() {
quote! {
__recorder.#register_method(
&metrics::Key::from_parts(
format!("{}{}{}", __scope, #separator, #name),
__labels.clone(),
),
__metadata,
)
}
} else {
let label_keys = metric.labels.iter().map(|(k, _)| k);
let label_values = metric.labels.iter().map(|(_, v)| v);
quote! {
{
let mut __field_labels = __labels.clone();
__field_labels.extend([
#(metrics::Label::new(#label_keys, #label_values)),*
]);
__recorder.#register_method(
&metrics::Key::from_parts(
format!("{}{}{}", __scope, #separator, #name),
__field_labels,
),
__metadata,
)
}
}
};

field_inits.push(quote! {
#field_name: __recorder.#register_method(
&metrics::Key::from_parts(
format!("{}{}{}", __scope, #separator, #name),
__labels.clone(),
),
__metadata,
),
#field_name: #field_init,
});
describes.push(quote! {
__recorder.#describe_method(
Expand Down Expand Up @@ -211,7 +274,9 @@ pub(crate) fn derive(node: &DeriveInput) -> Result<proc_macro2::TokenStream> {
);
let __metadata = &__METADATA;
let __scope = scope;
let __labels = labels;
#[allow(unused_mut)]
let mut __labels = labels;
#global_labels_init
Self {
#(#field_inits)*
}
Expand Down Expand Up @@ -247,6 +312,7 @@ pub(crate) fn derive(node: &DeriveInput) -> Result<proc_macro2::TokenStream> {
pub(crate) struct MetricsAttr {
pub(crate) scope: MetricsScope,
pub(crate) separator: Option<LitStr>,
pub(crate) labels: Vec<LabelPair>,
}

impl MetricsAttr {
Expand All @@ -269,11 +335,19 @@ 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, mut dynamic) = (None, None, None);
let (mut scope, mut separator, mut dynamic, mut labels) = (None, None, None, None);
for kv in parsed {
if kv.path.is_ident("labels") {
if labels.is_some() {
return Err(Error::new_spanned(kv, "duplicate `labels` value provided"));
}
labels = Some(parse_labels_expr(&kv.value)?);
continue;
}

let lit = match kv.value {
Expr::Lit(ref expr) => &expr.lit,
_ => continue,
_ => return Err(Error::new_spanned(&kv.value, "value must be a literal")),
};
if kv.path.is_ident("scope") {
if scope.is_some() {
Expand Down Expand Up @@ -309,7 +383,8 @@ fn parse_metrics_attr(node: &DeriveInput) -> Result<MetricsAttr> {
}
};

Ok(MetricsAttr { scope, separator })
let labels = labels.unwrap_or_default();
Ok(MetricsAttr { scope, separator, labels })
}

fn parse_metric_fields(node: &DeriveInput) -> Result<Vec<MetricField<'_>>> {
Expand All @@ -319,25 +394,30 @@ fn parse_metric_fields(node: &DeriveInput) -> Result<Vec<MetricField<'_>>> {

let mut metrics = Vec::with_capacity(data.fields.len());
for field in &data.fields {
let (mut describe, mut rename, mut skip) = (None, None, false);
let (mut describe, mut rename, mut labels, mut skip) = (None, None, None, false);
if let Some(metric_attr) = parse_single_attr(field, "metric")? {
let parsed =
metric_attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
for meta in parsed {
match meta {
Meta::Path(path) if path.is_ident("skip") => skip = true,
Meta::NameValue(kv) => {
let lit = match kv.value {
Expr::Lit(ref expr) => &expr.lit,
_ => continue,
};
if kv.path.is_ident("describe") {
if kv.path.is_ident("labels") {
if labels.is_some() {
return Err(Error::new_spanned(
kv,
"duplicate `labels` value provided",
));
}
labels = Some(parse_labels_expr(&kv.value)?);
} else if kv.path.is_ident("describe") {
if describe.is_some() {
return Err(Error::new_spanned(
kv,
"duplicate `describe` value provided",
));
}
let lit = parse_expr_lit(&kv.value)?;
describe = Some(parse_str_lit(lit)?);
} else if kv.path.is_ident("rename") {
if rename.is_some() {
Expand All @@ -346,7 +426,8 @@ fn parse_metric_fields(node: &DeriveInput) -> Result<Vec<MetricField<'_>>> {
"duplicate `rename` value provided",
));
}
rename = Some(parse_str_lit(lit)?)
let lit = parse_expr_lit(&kv.value)?;
rename = Some(parse_str_lit(lit)?);
} else {
return Err(Error::new_spanned(kv, "unsupported attribute entry"));
}
Expand Down Expand Up @@ -375,7 +456,8 @@ fn parse_metric_fields(node: &DeriveInput) -> Result<Vec<MetricField<'_>>> {
},
};

metrics.push(MetricField::Included(Metric::new(field, description, rename)));
let labels = labels.unwrap_or_default();
metrics.push(MetricField::Included(Metric::new(field, description, rename, labels)));
}

Ok(metrics)
Expand Down Expand Up @@ -443,3 +525,55 @@ fn parse_bool_lit(lit: &Lit) -> Result<LitBool> {
_ => Err(Error::new_spanned(lit, "value must be a boolean literal")),
}
}

fn parse_expr_lit(expr: &Expr) -> Result<&Lit> {
match expr {
Expr::Lit(expr_lit) => Ok(&expr_lit.lit),
_ => Err(Error::new_spanned(expr, "value must be a literal")),
}
}

/// Parses `labels = [("key", expr), ...]` into a vector of label pairs.
/// Keys must be string literals; values can be arbitrary expressions.
fn parse_labels_expr(expr: &Expr) -> Result<Vec<LabelPair>> {
Comment thread
DaniPopes marked this conversation as resolved.
let Expr::Array(arr) = expr else {
return Err(Error::new_spanned(
expr,
"labels must be an array of tuples, e.g. `labels = [(\"key\", \"value\")]`",
));
};

let mut labels = Vec::with_capacity(arr.elems.len());
let mut seen_keys = std::collections::HashSet::new();

for elem in &arr.elems {
let Expr::Tuple(ExprTuple { elems, .. }) = elem else {
return Err(Error::new_spanned(
elem,
"each label must be a tuple, e.g. `(\"key\", \"value\")` or `(\"key\", CONST)`",
));
};

if elems.len() != 2 {
return Err(Error::new_spanned(
elem,
"each label tuple must have exactly two elements: (key, value)",
));
}

// Key must be a string literal
let key = parse_str_lit(parse_expr_lit(&elems[0])?)?;

// Check for duplicate keys
let key_str = key.value();
if !seen_keys.insert(key_str.clone()) {
return Err(Error::new_spanned(&elems[0], format!("duplicate label key `{key_str}`")));
}

// Value can be any expression (string literal, constant, etc.)
let value = elems[1].clone();
labels.push((key, value));
}

Ok(labels)
}
76 changes: 76 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,82 @@ mod with_attrs;
/// }
/// }
/// ```
///
/// ## Per-field labels
///
/// You can attach labels to individual fields using the `labels` attribute.
/// This is useful when you want multiple fields to share the same metric name
/// but be distinguished by label values:
///
/// ```
/// use metrics::Counter;
/// use metrics_derive::Metrics;
///
/// #[derive(Metrics)]
/// #[metrics(scope = "forwarder")]
/// pub struct TransactionMetrics {
/// /// Number of transactions.
/// #[metric(rename = "transactions", labels = [("outcome", "forwarded")])]
/// forwarded: Counter,
/// /// Number of transactions.
/// #[metric(rename = "transactions", labels = [("outcome", "dropped")])]
/// dropped: Counter,
/// }
/// ```
///
/// Field-level labels are appended to any struct-level labels passed via
/// `new_with_labels()`. Multiple labels can be specified per field:
///
/// ```
/// use metrics::Counter;
/// use metrics_derive::Metrics;
///
/// #[derive(Metrics)]
/// #[metrics(scope = "api")]
/// pub struct RequestMetrics {
/// /// Request count.
/// #[metric(rename = "requests", labels = [("method", "GET"), ("status", "200")])]
/// get_success: Counter,
/// }
/// ```
///
/// ## Global labels
///
/// You can also specify labels at the struct level that apply to all fields.
/// These are combined with any instance labels passed via `new_with_labels()`:
///
/// ```
/// use metrics::Counter;
/// use metrics_derive::Metrics;
///
/// #[derive(Metrics)]
/// #[metrics(scope = "api", labels = [("service", "gateway"), ("version", "v1")])]
/// pub struct ApiMetrics {
/// /// Total requests.
/// requests: Counter,
/// /// Successful requests (with additional field-level labels).
/// #[metric(labels = [("status", "success")])]
/// success: Counter,
/// }
/// ```
///
/// Label order: instance labels (from `new_with_labels`) → global labels → field labels.
///
/// Label values can also be arbitrary expressions, such as constants:
///
/// ```
/// use metrics::Counter;
/// use metrics_derive::Metrics;
///
/// const SERVICE_NAME: &str = "gateway";
///
/// #[derive(Metrics)]
/// #[metrics(scope = "api", labels = [("service", SERVICE_NAME)])]
/// pub struct ConstLabelMetrics {
/// /// Total requests.
/// requests: Counter,
/// }
/// ```
#[proc_macro_derive(Metrics, attributes(metrics, metric))]
pub fn derive_metrics(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
Expand Down
Loading