metrics/metrics-macros/src/lib.rs

441 lines
14 KiB
Rust

extern crate proc_macro;
use self::proc_macro::TokenStream;
use lazy_static::lazy_static;
use proc_macro_hack::proc_macro_hack;
use quote::{format_ident, quote, ToTokens};
use regex::Regex;
use syn::parse::discouraged::Speculative;
use syn::parse::{Error, Parse, ParseStream, Result};
use syn::{parse_macro_input, Expr, LitStr, Token};
#[cfg(test)]
mod tests;
enum Labels {
Existing(Expr),
Inline(Vec<(LitStr, Expr)>),
}
struct WithoutExpression {
key: LitStr,
labels: Option<Labels>,
}
struct WithExpression {
key: LitStr,
op_value: Expr,
labels: Option<Labels>,
}
struct Registration {
key: LitStr,
unit: Option<Expr>,
description: Option<LitStr>,
labels: Option<Labels>,
}
impl Parse for WithoutExpression {
fn parse(mut input: ParseStream) -> Result<Self> {
let key = read_key(&mut input)?;
let labels = parse_labels(&mut input)?;
Ok(WithoutExpression { key, labels })
}
}
impl Parse for WithExpression {
fn parse(mut input: ParseStream) -> Result<Self> {
let key = read_key(&mut input)?;
input.parse::<Token![,]>()?;
let op_value: Expr = input.parse()?;
let labels = parse_labels(&mut input)?;
Ok(WithExpression {
key,
op_value,
labels,
})
}
}
impl Parse for Registration {
fn parse(mut input: ParseStream) -> Result<Self> {
let key = read_key(&mut input)?;
// We accept three possible parameters: unit, description, and labels.
//
// If our first parameter is a literal string, we either have the description and no labels,
// or a description and labels. Peek at the trailing token after the description to see if
// we need to keep parsing.
// This may or may not be the start of labels, if the description has been omitted, so
// we hold on to it until we can make sure nothing else is behind it, or if it's a full
// fledged set of labels.
let (unit, description, labels) = if input.peek(Token![,]) && input.peek3(Token![=>]) {
// We have a ", <something> =>" pattern, which can only be labels, so we have no
// unit or description.
let labels = parse_labels(&mut input)?;
(None, None, labels)
} else if input.peek(Token![,]) && input.peek2(LitStr) {
// We already know we're not working with labels only, and if we have ", <literal
// string>" then we have to at least have a description, possibly with labels.
input.parse::<Token![,]>()?;
let description = input.parse::<LitStr>().ok();
let labels = parse_labels(&mut input)?;
(None, description, labels)
} else if input.peek(Token![,]) {
// We may or may not have anything left to parse here, but it could also be any
// combination of unit + description and/or labels.
//
// We speculatively try and parse an expression from the buffer, and see if we can match
// it to the qualified name of the Unit enum. We run all of the other normal parsing
// after that for description and labels.
let forked = input.fork();
forked.parse::<Token![,]>()?;
let unit = if let Ok(Expr::Path(path)) = forked.parse::<Expr>() {
let qname = path
.path
.segments
.iter()
.map(|x| x.ident.to_string())
.collect::<Vec<_>>()
.join("::");
if qname.starts_with("metrics::Unit") || qname.starts_with("Unit") {
Some(Expr::Path(path))
} else {
None
}
} else {
None
};
// If we succeeded, advance the main parse stream up to where the fork left off.
if unit.is_some() {
input.advance_to(&forked);
}
// We still have to check for a possible description.
let description =
if input.peek(Token![,]) && input.peek2(LitStr) && !input.peek3(Token![=>]) {
input.parse::<Token![,]>()?;
input.parse::<LitStr>().ok()
} else {
None
};
let labels = parse_labels(&mut input)?;
(unit, description, labels)
} else {
(None, None, None)
};
Ok(Registration {
key,
unit,
description,
labels,
})
}
}
#[proc_macro_hack]
pub fn register_counter(input: TokenStream) -> TokenStream {
let Registration {
key,
unit,
description,
labels,
} = parse_macro_input!(input as Registration);
get_expanded_registration("counter", key, unit, description, labels).into()
}
#[proc_macro_hack]
pub fn register_gauge(input: TokenStream) -> TokenStream {
let Registration {
key,
unit,
description,
labels,
} = parse_macro_input!(input as Registration);
get_expanded_registration("gauge", key, unit, description, labels).into()
}
#[proc_macro_hack]
pub fn register_histogram(input: TokenStream) -> TokenStream {
let Registration {
key,
unit,
description,
labels,
} = parse_macro_input!(input as Registration);
get_expanded_registration("histogram", key, unit, description, labels).into()
}
#[proc_macro_hack]
pub fn increment(input: TokenStream) -> TokenStream {
let WithoutExpression { key, labels } = parse_macro_input!(input as WithoutExpression);
let op_value = quote! { 1 };
get_expanded_callsite("counter", "increment", key, labels, op_value).into()
}
#[proc_macro_hack]
pub fn counter(input: TokenStream) -> TokenStream {
let WithExpression {
key,
op_value,
labels,
} = parse_macro_input!(input as WithExpression);
get_expanded_callsite("counter", "increment", key, labels, op_value).into()
}
#[proc_macro_hack]
pub fn gauge(input: TokenStream) -> TokenStream {
let WithExpression {
key,
op_value,
labels,
} = parse_macro_input!(input as WithExpression);
get_expanded_callsite("gauge", "update", key, labels, op_value).into()
}
#[proc_macro_hack]
pub fn histogram(input: TokenStream) -> TokenStream {
let WithExpression {
key,
op_value,
labels,
} = parse_macro_input!(input as WithExpression);
get_expanded_callsite("histogram", "record", key, labels, op_value).into()
}
fn get_expanded_registration(
metric_type: &str,
name: LitStr,
unit: Option<Expr>,
description: Option<LitStr>,
labels: Option<Labels>,
) -> proc_macro2::TokenStream {
let register_ident = format_ident!("register_{}", metric_type);
let key = key_to_quoted(labels);
let unit = match unit {
Some(e) => quote! { Some(#e) },
None => quote! { None },
};
let description = match description {
Some(s) => quote! { Some(#s) },
None => quote! { None },
};
quote! {
{
static METRIC_NAME: [metrics::SharedString; 1] = [metrics::SharedString::const_str(#name)];
// Only do this work if there's a recorder installed.
if let Some(recorder) = metrics::try_recorder() {
// Registrations are fairly rare, don't attempt to cache here
// and just use an owned ref.
recorder.#register_ident(metrics::Key::Owned(#key), #unit, #description);
}
}
}
}
fn get_expanded_callsite<V>(
metric_type: &str,
op_type: &str,
name: LitStr,
labels: Option<Labels>,
op_values: V,
) -> proc_macro2::TokenStream
where
V: ToTokens,
{
// We use a helper method for histogram values to coerce into u64, but otherwise,
// just pass through whatever the caller gave us.
let op_values = if metric_type == "histogram" {
quote! { metrics::__into_u64(#op_values) }
} else {
quote! { #op_values }
};
let op_ident = format_ident!("{}_{}", op_type, metric_type);
let use_fast_path = can_use_fast_path(&labels);
if use_fast_path {
// We're on the fast path here, so we'll build our key, statically cache it,
// and use a borrowed reference to it for this and future operations.
let statics = match labels {
Some(Labels::Inline(pairs)) => {
let labels = pairs
.into_iter()
.map(|(key, val)| quote! { metrics::Label::from_static_parts(#key, #val) })
.collect::<Vec<_>>();
let labels_len = labels.len();
let labels_len = quote! { #labels_len };
quote! {
static METRIC_NAME: [metrics::SharedString; 1] = [metrics::SharedString::const_str(#name)];
static METRIC_LABELS: [metrics::Label; #labels_len] = [#(#labels),*];
static METRIC_KEY: metrics::KeyData =
metrics::KeyData::from_static_parts(&METRIC_NAME, &METRIC_LABELS);
}
}
None => {
quote! {
static METRIC_NAME: [metrics::SharedString; 1] = [metrics::SharedString::const_str(#name)];
static METRIC_KEY: metrics::KeyData =
metrics::KeyData::from_static_name(&METRIC_NAME);
}
}
_ => unreachable!("use_fast_path == true, but found expression-based labels"),
};
quote! {
{
#statics
// Only do this work if there's a recorder installed.
if let Some(recorder) = metrics::try_recorder() {
recorder.#op_ident(metrics::Key::Borrowed(&METRIC_KEY), #op_values);
}
}
}
} else {
// We're on the slow path, so we allocate, womp.
let key = key_to_quoted(labels);
quote! {
{
static METRIC_NAME: [metrics::SharedString; 1] = [metrics::SharedString::const_str(#name)];
// Only do this work if there's a recorder installed.
if let Some(recorder) = metrics::try_recorder() {
recorder.#op_ident(metrics::Key::Owned(#key), #op_values);
}
}
}
}
}
fn can_use_fast_path(labels: &Option<Labels>) -> bool {
match labels {
None => true,
Some(labels) => match labels {
Labels::Existing(_) => false,
Labels::Inline(pairs) => pairs.iter().all(|(_, v)| matches!(v, Expr::Lit(_))),
},
}
}
fn read_key(input: &mut ParseStream) -> Result<LitStr> {
let key = input.parse::<LitStr>()?;
let inner = key.value();
lazy_static! {
static ref RE: Regex = Regex::new("^[a-zA-Z][a-zA-Z0-9_:\\.]*$").unwrap();
}
if !RE.is_match(&inner) {
return Err(Error::new(
key.span(),
"metric name must match ^[a-zA-Z][a-zA-Z0-9_:.]*$",
));
}
Ok(key)
}
fn key_to_quoted(labels: Option<Labels>) -> proc_macro2::TokenStream {
match labels {
None => quote! { metrics::KeyData::from_static_name(&METRIC_NAME) },
Some(labels) => match labels {
Labels::Inline(pairs) => {
let labels = pairs
.into_iter()
.map(|(key, val)| quote! { metrics::Label::new(#key, #val) });
quote! {
metrics::KeyData::from_parts(&METRIC_NAME[..], vec![#(#labels),*])
}
}
Labels::Existing(e) => quote! { metrics::KeyData::from_parts(&METRIC_NAME[..], #e) },
},
}
}
fn parse_labels(input: &mut ParseStream) -> Result<Option<Labels>> {
if input.is_empty() {
return Ok(None);
}
if !input.peek(Token![,]) {
// This is a hack to generate the proper error message for parsing the comma next without
// actually parsing it and thus removing it from the parse stream. Just makes the following
// code a bit cleaner.
input
.parse::<Token![,]>()
.map_err(|e| Error::new(e.span(), "expected labels, but comma not found"))?;
}
// Two possible states for labels: references to a label iterator, or key/value pairs.
//
// We check to see if we have the ", key =>" part, which tells us that we're taking in key/value
// pairs. If we don't have that, we check to see if we have a "`, <expr" part, which could us
// getting handed a labels iterator. The type checking for `IntoLabels` in `metrics::Recorder`
// will do the heavy lifting from that point forward.
if input.peek(Token![,]) && input.peek2(LitStr) && input.peek3(Token![=>]) {
let mut labels = Vec::new();
loop {
if input.is_empty() {
break;
}
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let lkey: LitStr = input.parse()?;
input.parse::<Token![=>]>()?;
let lvalue: Expr = input.parse()?;
labels.push((lkey, lvalue));
}
return Ok(Some(Labels::Inline(labels)));
}
// Has to be an expression otherwise, or a trailing comma.
input.parse::<Token![,]>()?;
// Unless it was an expression - clear the trailing comma.
if input.is_empty() {
return Ok(None);
}
let lvalue: Expr = input.parse().map_err(|e| {
Error::new(
e.span(),
"expected label expression, but expression not found",
)
})?;
// Expression can end with a trailing comma, handle it.
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
Ok(Some(Labels::Existing(lvalue)))
}