derive_prom_metrics/lib.rs
1//! Build [Prometheus] metrics declaratively as structs.
2//!
3//! This crate is in the very early stages of development.
4//!
5//! [Prometheus]: https://prometheus.io
6//!
7//! # Example
8//!
9//! ```no_run
10//! use derive_prom_metrics::Metrics;
11//! use prometheus::{linear_buckets, Gauge};
12//!
13//! #[derive(Metrics, Debug)]
14//! struct Metrics {
15//! /// A simple counter.
16//! counter: prometheus::Counter,
17//!
18//! /// A simple gauge.
19//! /// Is this in the same line? No.
20//! ///
21//! /// This will make it into the help text.
22//! gauge: Gauge,
23//!
24//! /**
25//! * My help
26//! *
27//! * This will make it into the help text, as well as all of the leading asterisks.
28//! */
29//! int_counter: prometheus::IntCounter,
30//!
31//! /// An integer gauge.
32//! int_gauge: prometheus::IntGauge,
33//!
34//! /// A histogram.
35//! #[prometheus(buckets = linear_buckets(0.005, 0.005, 999)?)]
36//! histogram: prometheus::Histogram,
37//!
38//! /// A vector of counters, one for each label.
39//! #[prometheus(label_names = &["label"])]
40//! counter_vec: prometheus::CounterVec,
41//!
42//! /// A vector of gauges, one for each label.
43//! #[prometheus(label_names = &["label"])]
44//! gauge_vec: prometheus::GaugeVec,
45//!
46//! /// A vector of integer counters, one for each label.
47//! #[prometheus(label_names = &["label"])]
48//! int_counter_vec: prometheus::IntCounterVec,
49//!
50//! /// A vector of integer gauges, one for each label.
51//! #[prometheus(label_names = &["label"])]
52//! int_gauge_vec: prometheus::GaugeVec,
53//!
54//! /// A vector of histograms, one for each label.
55//! #[prometheus(
56//! buckets = linear_buckets(0.005, 0.005, 999)?,
57//! label_names = &["label"],
58//! )]
59//! histogram_vec: prometheus::HistogramVec,
60//! }
61//! ```
62#![crate_type = "proc-macro"]
63
64use proc_macro2::TokenStream;
65use quote::quote;
66use syn::spanned::Spanned as _;
67use syn::{DeriveInput, Error, Result};
68
69mod metric_type;
70mod opts;
71
72use self::metric_type::MetricType;
73
74#[proc_macro_derive(Metrics, attributes(prometheus))]
75pub fn metrics_derive_macro(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
76 impl_metrics_new(item.into())
77 .unwrap_or_else(Error::into_compile_error)
78 .into()
79}
80
81/// The `new` method implementation for the metrics struct.
82fn impl_metrics_new(item: TokenStream) -> Result<TokenStream> {
83 let ast: DeriveInput = syn::parse2(item)?;
84
85 let syn::Data::Struct(data) = ast.data else {
86 return Err(Error::new(ast.span(), "Metrics only derived for structs"));
87 };
88
89 let syn::Fields::Named(fields) = data.fields else {
90 return Err(Error::new(
91 data.fields.span(),
92 "Tuple or unit structs aren't supported",
93 ));
94 };
95
96 let struct_name = ast.ident;
97
98 let field_inits = field_initializers(fields)?;
99
100 Ok(quote! {
101 impl #struct_name {
102 pub fn new(registry: &::prometheus::Registry) -> ::prometheus::Result<Self> {
103 Ok(Self {
104 #(#field_inits),*
105 })
106 }
107 }
108 })
109}
110
111fn field_initializers(fields: syn::FieldsNamed) -> Result<Vec<TokenStream>> {
112 let mut result = vec![];
113
114 for field in fields.named {
115 result.push(field_initializer(&field)?);
116 }
117
118 Ok(result)
119}
120
121fn field_initializer(field: &syn::Field) -> Result<TokenStream> {
122 let syn::Type::Path(ref ty) = field.ty else {
123 return Err(Error::new(
124 field.ty.span(),
125 format!("Field type '{:?}' unsupported", field.ty),
126 ));
127 };
128
129 let metric_type: MetricType = ty.try_into()?;
130
131 let ident = field
132 .ident
133 .clone()
134 .ok_or_else(|| Error::new(field.span(), "Field must be named"))?;
135
136 let metric_init = metric_type.init_expr(ident.clone(), field)?;
137
138 Ok(quote! {
139 #ident: {
140 let metric = #metric_init;
141 registry.register(Box::new(metric.clone()))?;
142 metric
143 }
144 })
145}