368 lines
10 KiB
Rust
368 lines
10 KiB
Rust
extern crate proc_macro;
|
|
|
|
use self::proc_macro::TokenStream;
|
|
|
|
use proc_macro_hack::proc_macro_hack;
|
|
use quote::{format_ident, quote, ToTokens};
|
|
use syn::parse::{Error, Parse, ParseStream, Result};
|
|
use syn::{parse_macro_input, Expr, LitStr, Token};
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
enum Key {
|
|
NotScoped(LitStr),
|
|
Scoped(LitStr),
|
|
}
|
|
|
|
enum Labels {
|
|
Existing(Expr),
|
|
Inline(Vec<(LitStr, Expr)>),
|
|
}
|
|
|
|
struct WithoutExpression {
|
|
key: Key,
|
|
labels: Option<Labels>,
|
|
}
|
|
|
|
struct WithExpression {
|
|
key: Key,
|
|
op_value: Expr,
|
|
labels: Option<Labels>,
|
|
}
|
|
|
|
struct Registration {
|
|
key: Key,
|
|
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)?;
|
|
|
|
// 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 (description, labels) = if input.peek(Token![,]) && input.peek3(Token![=>]) {
|
|
// We have a ", <something> =>" pattern, which can only be labels, so we have no
|
|
// description.
|
|
let labels = parse_labels(&mut input)?;
|
|
|
|
(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)?;
|
|
(description, labels)
|
|
} else {
|
|
// We might have labels passed as an expression.
|
|
let labels = parse_labels(&mut input)?;
|
|
(None, labels)
|
|
};
|
|
|
|
Ok(Registration {
|
|
key,
|
|
description,
|
|
labels,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[proc_macro_hack]
|
|
pub fn register_counter(input: TokenStream) -> TokenStream {
|
|
let Registration {
|
|
key,
|
|
description,
|
|
labels,
|
|
} = parse_macro_input!(input as Registration);
|
|
|
|
get_expanded_registration("counter", key, description, labels).into()
|
|
}
|
|
|
|
#[proc_macro_hack]
|
|
pub fn register_gauge(input: TokenStream) -> TokenStream {
|
|
let Registration {
|
|
key,
|
|
description,
|
|
labels,
|
|
} = parse_macro_input!(input as Registration);
|
|
|
|
get_expanded_registration("gauge", key, description, labels).into()
|
|
}
|
|
|
|
#[proc_macro_hack]
|
|
pub fn register_histogram(input: TokenStream) -> TokenStream {
|
|
let Registration {
|
|
key,
|
|
description,
|
|
labels,
|
|
} = parse_macro_input!(input as Registration);
|
|
|
|
get_expanded_registration("histogram", key, 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,
|
|
key: Key,
|
|
description: Option<LitStr>,
|
|
labels: Option<Labels>,
|
|
) -> proc_macro2::TokenStream {
|
|
let register_ident = format_ident!("register_{}", metric_type);
|
|
let key = key_to_quoted(key, labels);
|
|
|
|
let description = match description {
|
|
Some(s) => quote! { Some(#s) },
|
|
None => quote! { None },
|
|
};
|
|
|
|
quote! {
|
|
{
|
|
// 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), #description);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_expanded_callsite<V>(
|
|
metric_type: &str,
|
|
op_type: &str,
|
|
key: Key,
|
|
labels: Option<Labels>,
|
|
op_values: V,
|
|
) -> proc_macro2::TokenStream
|
|
where
|
|
V: ToTokens,
|
|
{
|
|
let use_fast_path = can_use_fast_path(&labels);
|
|
let key = key_to_quoted(key, labels);
|
|
|
|
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);
|
|
|
|
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.
|
|
quote! {
|
|
{
|
|
static CACHED_KEY: metrics::OnceKeyData = metrics::OnceKeyData::new();
|
|
|
|
// Only do this work if there's a recorder installed.
|
|
if let Some(recorder) = metrics::try_recorder() {
|
|
// Initialize our fast path.
|
|
let key = CACHED_KEY.get_or_init(|| { #key });
|
|
recorder.#op_ident(metrics::Key::Borrowed(&key), #op_values);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// We're on the slow path, so basically we register every single time.
|
|
//
|
|
// Recorders are expected to deduplicate any duplicate registrations.
|
|
quote! {
|
|
{
|
|
// 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<Key> {
|
|
if let Ok(_) = input.parse::<Token![<]>() {
|
|
let s = input.parse::<LitStr>()?;
|
|
input.parse::<Token![>]>()?;
|
|
Ok(Key::Scoped(s))
|
|
} else {
|
|
let s = input.parse::<LitStr>()?;
|
|
Ok(Key::NotScoped(s))
|
|
}
|
|
}
|
|
|
|
fn quote_key_name(key: Key) -> proc_macro2::TokenStream {
|
|
match key {
|
|
Key::NotScoped(s) => {
|
|
quote! { #s }
|
|
}
|
|
Key::Scoped(s) => {
|
|
quote! {
|
|
format!("{}.{}", std::module_path!().replace("::", "."), #s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn key_to_quoted(key: Key, labels: Option<Labels>) -> proc_macro2::TokenStream {
|
|
let name = quote_key_name(key);
|
|
|
|
match labels {
|
|
None => quote! { metrics::KeyData::from_name(#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_name_and_labels(#name, vec![#(#labels),*]) }
|
|
}
|
|
Labels::Existing(e) => quote! { metrics::KeyData::from_name_and_labels(#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)))
|
|
}
|