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, } struct WithExpression { key: LitStr, op_value: Expr, labels: Option, } struct Registration { key: LitStr, unit: Option, description: Option, labels: Option, } impl Parse for WithoutExpression { fn parse(mut input: ParseStream) -> Result { 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 { let key = read_key(&mut input)?; input.parse::()?; 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 { 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 ", =>" 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 ", " then we have to at least have a description, possibly with labels. input.parse::()?; let description = input.parse::().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::()?; let unit = if let Ok(Expr::Path(path)) = forked.parse::() { let qname = path .path .segments .iter() .map(|x| x.ident.to_string()) .collect::>() .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::()?; input.parse::().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, description: Option, labels: Option, ) -> 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( metric_type: &str, op_type: &str, name: LitStr, labels: Option, 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::>(); 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) -> 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 { let key = input.parse::()?; 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) -> 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> { 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::() .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 "`, ]) { let mut labels = Vec::new(); loop { if input.is_empty() { break; } input.parse::()?; if input.is_empty() { break; } let lkey: LitStr = input.parse()?; input.parse::]>()?; 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::()?; // 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::()?; } Ok(Some(Labels::Existing(lvalue))) }