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}