#![recursion_limit = "128"]
#[macro_use]
extern crate quote;
use proc_macro::TokenStream;
use slog::Level;
use std::collections::HashMap;
use std::str::FromStr;
enum StatTriggerAction {
Increment,
Decrement,
SetValue,
Ignore,
}
impl FromStr for StatTriggerAction {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Incr" => Ok(StatTriggerAction::Increment),
"Decr" => Ok(StatTriggerAction::Decrement),
"SetVal" => Ok(StatTriggerAction::SetValue),
"None" => Ok(StatTriggerAction::Ignore),
s => Err(format!("Unknown action {}", s)),
}
}
}
enum StatTriggerValue {
Fixed(i64),
Expr(Box<syn::Expr>),
}
struct StatTriggerData {
id: syn::Ident,
condition_body: syn::Expr,
action: StatTriggerAction,
val: StatTriggerValue,
fixed_groups: HashMap<String, String>,
field_groups: Vec<syn::Ident>,
bucket_by: Option<syn::Ident>,
}
#[proc_macro_derive(SlogValue)]
pub fn slog_value(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
let gen = impl_value_traits(&ast);
TokenStream::from(gen)
}
#[proc_macro_derive(
ExtLoggable,
attributes(LogDetails, FixedFields, StatTrigger, StatGroup, BucketBy)
)]
pub fn loggable(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
let gen = impl_loggable(&ast);
TokenStream::from(gen)
}
fn impl_value_traits(ast: &syn::DeriveInput) -> proc_macro2::TokenStream {
let name = &ast.ident;
let lifetimes = ast.generics.lifetimes();
let lifetimes_2 = ast.generics.lifetimes();
let lifetimes_3 = ast.generics.lifetimes();
let lifetimes_4 = ast.generics.lifetimes();
let ty_params: Vec<_> = ast.generics.type_params().collect();
let (tys, bounds) = get_types_bounds(&ty_params);
let tys_2 = tys.clone();
let tys_3 = tys.clone();
let tys_4 = tys.clone();
let tys_5 = tys.clone();
let tys_6 = tys.clone();
let bounds2 = bounds.clone();
quote! {
impl <#(#lifetimes,)* #(#tys),*> slog::SerdeValue for #name<#(#lifetimes_2,)* #(#tys_2),*>
#(where #tys_3: #(#bounds + )* serde::Serialize + slog::Value),* {
fn as_serde(&self) -> &erased_serde::Serialize {
self
}
fn to_sendable(&self) -> Box<slog::SerdeValue + Send + 'static> {
Box::new(self.clone())
}
}
impl<#(#lifetimes_3,)* #(#tys_4),*> slog::Value for #name<#(#lifetimes_4,)* #(#tys_5),*>
#(where #tys_6: #(#bounds2 + )* slog::Value),* {
fn serialize(&self,
_record: &slog::Record,
key: slog::Key,
serializer: &mut slog::Serializer) -> slog::Result {
serializer.emit_serde(key, self)
}
}
}
}
fn get_types_bounds<'a>(
ty_params: &[&'a syn::TypeParam],
) -> (Vec<&'a syn::Ident>, Vec<Vec<&'a syn::TraitBound>>) {
let tys: Vec<&syn::Ident> = ty_params.iter().map(|param| ¶m.ident).collect();
let bounds = ty_params
.iter()
.map(|p| {
p.bounds
.iter()
.filter_map(|t| {
if let syn::TypeParamBound::Trait(ref tr) = *t {
Some(tr)
} else {
None
}
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
(tys, bounds)
}
fn impl_stats_trigger(ast: &syn::DeriveInput) -> proc_macro2::TokenStream {
let triggers = ast
.attrs
.iter()
.filter(|a| a.path.is_ident("StatTrigger"))
.map(|a| match a.parse_meta() {
Ok(syn::Meta::List(metalist)) => {
let nested = metalist.nested.iter();
parse_stat_trigger(nested, &ast.data)
}
_ => panic!("Invalid format for #[StatTrigger(attr=\"val\")]"),
})
.collect::<Vec<_>>();
let stat_ids = triggers
.iter()
.map(|t| {
let (keys, vals): (Vec<_>, Vec<_>) = t
.fixed_groups
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.unzip();
let id = &t.id;
quote! {
slog_extlog::stats::StatDefinitionTagged { defn: &#id, fixed_tags: &[#( (#keys, #vals) ),*] }
}
})
.collect::<Vec<_>>();
let stat_ids_cond = triggers
.iter()
.map(|t| {
let (keys, vals): (Vec<_>, Vec<_>) = t
.fixed_groups
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.unzip();
let id = t.id.to_string();
quote! {
#id if (true #(&& stat_id.fixed_tags.iter().filter(|tag| tag.0 == #keys && tag.1 == #vals).count() != 0) *)
}
})
.collect::<Vec<_>>();
let stat_conds = triggers
.iter()
.map(|ref t| &t.condition_body)
.collect::<Vec<_>>();
let stat_ids_change = stat_ids_cond.clone();
let stat_changes = triggers
.iter()
.map(|t| {
let val = &(match t.val {
StatTriggerValue::Fixed(v) => quote! {#v as usize},
StatTriggerValue::Expr(ref e) => quote! {(#e) as usize },
});
match t.action {
StatTriggerAction::Increment => quote! {
Some(slog_extlog::stats::ChangeType::Incr(#val))
},
StatTriggerAction::Decrement => quote! {
Some(slog_extlog::stats::ChangeType::Decr(#val))
},
StatTriggerAction::SetValue => quote! {
Some(slog_extlog::stats::ChangeType::SetTo(#val as isize))
},
StatTriggerAction::Ignore => quote! { None },
}
})
.collect::<Vec<_>>();
let mut stats_groups = quote! {};
for t in &triggers {
let id = &t.id.to_string();
let dyn_groups = t.field_groups.clone();
let dyn_groups_str = dyn_groups
.clone()
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
stats_groups = quote! { #stats_groups
#id => { match tag_name {
#(#dyn_groups_str => self.#dyn_groups.to_string(),)*
_ => "".to_string() }
},
}
}
let mut stats_buckets = quote! {};
for t in &triggers {
let id = &t.id.to_string();
let bucket = t.bucket_by.clone();
if let Some(bucket) = bucket {
stats_buckets = quote! { #stats_buckets
#id => Some(self.#bucket as f64),
}
}
}
let tag_name_ident = if !triggers.is_empty() {
quote! { tag_name }
} else {
quote! { _tag_name }
};
let name = &ast.ident;
let lifetimes = ast.generics.lifetimes();
let lifetimes_2 = ast.generics.lifetimes();
let ty_params: Vec<_> = ast.generics.type_params().collect();
let (tys, bounds) = get_types_bounds(&ty_params);
let tys_2 = tys.clone();
let tys_3 = tys.clone();
let stat_ids_name = format_ident!("STATS_LIST_{}", name.to_string().to_uppercase());
quote! {
static #stat_ids_name: &'static [slog_extlog::stats::StatDefinitionTagged] = &[#(#stat_ids),*];
impl<#(#lifetimes,)* #(#tys),*> slog_extlog::stats::StatTrigger
for #name<#(#lifetimes_2,)* #(#tys_2),*>
#(where #tys_3: #(#bounds + )* slog::Value),*{
fn stat_list(
&self) -> &'static[slog_extlog::stats::StatDefinitionTagged] {
#stat_ids_name
}
fn condition(&self, stat_id: &slog_extlog::stats::StatDefinitionTagged) -> bool {
match stat_id.defn.name() {
#(#stat_ids_cond => #stat_conds,)*
s => panic!("Condition requested for unknown stat {}", s)
}
}
fn change(&self,
stat_id: &slog_extlog::stats::StatDefinitionTagged) ->
Option<slog_extlog::stats::ChangeType> {
match stat_id.defn.name() {
#(#stat_ids_change => #stat_changes,)*
s => panic!("Change requested for unknown stat {}", s)
}
}
fn tag_value(&self,
stat_id: &slog_extlog::stats::StatDefinitionTagged,
#tag_name_ident: &'static str) -> String {
if let Some(v) = stat_id.fixed_tags.iter().find(|name| #tag_name_ident == name.0) {
v.1.to_string()
} else {
match stat_id.defn.name() {
#stats_groups
_ => "".to_string(),
}
}
}
fn bucket_value(&self,
stat_id: &slog_extlog::stats::StatDefinitionTagged) -> Option<f64> {
match stat_id.defn.name() {
# stats_buckets
_ => None,
}
}
}
}
}
fn impl_loggable(ast: &syn::DeriveInput) -> proc_macro2::TokenStream {
let name = &ast.ident;
let lifetimes = ast.generics.lifetimes();
let lifetimes_2 = ast.generics.lifetimes();
let ty_params: Vec<_> = ast.generics.type_params().collect();
let (tys, bounds) = get_types_bounds(&ty_params);
let tys_2 = tys.clone();
let tys_3 = tys.clone();
let vals = ast
.attrs
.iter()
.filter(|a| a.path.is_ident("LogDetails"))
.collect::<Vec<_>>();
if vals.len() != 1 {
panic!("Unable to find LogDetails attribute, or multiple LogDetails supplied")
}
let (level, text, id) = match vals[0].parse_meta() {
Ok(syn::Meta::List(metalist)) => {
let nested: Vec<_> = metalist.nested.iter().collect();
parse_log_details(&nested)
}
_ => panic!("Invalid format for #[LogDetails(id, level, text)]"),
};
let fields = ast
.attrs
.iter()
.filter(|a| a.path.is_ident("FixedFields"))
.flat_map(|val| {
let meta = val.parse_meta();
match meta {
Ok(syn::Meta::List(metalist)) => metalist
.nested
.iter()
.map(|attr_val| parse_fixed_field(attr_val))
.map(|(key, value)| quote!( #key => #value ))
.collect::<Vec<_>>(),
_ => panic!("Invalid format for #[FixedFields(key = value)]"),
}
});
let kv_gen = impl_value_traits(ast);
let match_gen = match level {
Level::Critical => {
quote! { slog::crit!(logger, #text; "log_id" => id_val, #(#fields, )* "details" => self) }
}
Level::Error => {
quote! { slog::error!(logger, #text; "log_id" => id_val, #(#fields, )* "details" => self) }
}
Level::Warning => {
quote! { slog::warn!(logger, #text; "log_id" => id_val, #(#fields, )* "details" => self) }
}
Level::Info => {
quote! { slog::info!(logger, #text; "log_id" => id_val, #(#fields, )* "details" => self) }
}
Level::Debug => {
quote! { slog::debug!(logger, #text; "log_id" => id_val, #(#fields, )* "details" => self) }
}
Level::Trace => {
quote! { slog::trace!(logger, #text; "log_id" => id_val, #(#fields, )* "details" => self) }
}
};
let stat_gen = impl_stats_trigger(ast);
quote! {
impl<#(#lifetimes,)* #(#tys),*> slog_extlog::ExtLoggable
for #name<#(#lifetimes_2,)* #(#tys_2),*>
#(where #tys_3: #(#bounds + )* slog::Value),*{
fn ext_log<T>(&self, logger: &slog_extlog::stats::StatisticsLogger<T>)
where T: slog_extlog::stats::StatisticsLogFormatter + Send + Sync + 'static {
logger.update_stats(self);
let id_val = slog::FnValue(|_| format!("{}-{}", CRATE_LOG_NAME, #id));
#match_gen
}
}
#kv_gen
#stat_gen
}
}
fn parse_log_details(attr_val: &[&syn::NestedMeta]) -> (Level, String, u64) {
if attr_val.len() != 3 {
panic!("Must have exactly 3 parameters for LogDetails - ID, level, text")
}
let mut id = None;
let mut level = None;
let mut text = None;
for attr in attr_val {
match *attr {
syn::NestedMeta::Meta(syn::Meta::NameValue(ref name_value)) => {
if name_value.path.is_ident("Id") {
id = match name_value.lit {
syn::Lit::Str(ref s) => Some(s.value().parse::<u64>().expect(
"Invalid format for LogDetails - Id attribute must be an \
unsigned integer",
)),
_ => panic!(
"Invalid format for LogDetails - Id attribute must be a \
string-quoted unsigned integer"
),
};
} else if name_value.path.is_ident("Level") {
level = match name_value.lit {
syn::Lit::Str(ref s) => {
let s = s.value();
Some(
if s == "Warning" {
Level::Warning
} else {
Level::from_str(&s).unwrap_or_else(|_| {
panic!("Invalid log level provided: {}", s)
})
},
)
}
_ => panic!(
"Invalid format for LogDetails - Level attribute must be a \
string-quoted slog::Level"
),
};
} else if name_value.path.is_ident("Text") {
text = match name_value.lit {
syn::Lit::Str(ref s) => Some(s.value().clone()),
_ => panic!(
"Invalid format for LogDetails - Text attribute must be a \
string literal"
),
};
} else {
panic!("Unknown attribute in LogDetails")
}
}
_ => panic!("Invalid format for LogDetails - parameters must be key-value pairs"),
}
}
(
level.expect("No Level provided in LogDetails"),
text.expect("No Text provided in LogDetails"),
id.expect("No Id provided in LogDetails"),
)
}
fn parse_fixed_field(attr_val: &syn::NestedMeta) -> (String, String) {
match *attr_val {
syn::NestedMeta::Meta(ref item) => match *item {
syn::Meta::NameValue(ref name_value) => {
let ident = name_value
.path
.get_ident()
.expect("Invalid format for FixedFields");
if let syn::Lit::Str(ref s) = name_value.lit {
(ident.to_string(), s.value())
} else {
panic!("Invalid format for FixedFields - value must be a string");
}
}
_ => panic!("Invalid format for FixedFields - value must be a string"),
},
_ => panic!("Invalid format for FixedFields - parameters must be key-value pairs"),
}
}
fn is_attr_stat_id(attr: &syn::Attribute, id: &syn::Ident) -> bool {
match attr.parse_meta() {
Ok(syn::Meta::List(ref list)) => list.nested.iter().any(|inner| {
if let syn::NestedMeta::Meta(syn::Meta::NameValue(ref name_value)) = *inner {
if let syn::Lit::Str(ref s) = name_value.lit {
let parsed_value = format_ident!("{}", s.value());
name_value.path.is_ident("StatName") && &parsed_value == id
} else {
false
}
} else {
false
}
}),
_ => false,
}
}
fn parse_stat_trigger<'a>(
attr_val: impl Iterator<Item = &'a syn::NestedMeta>,
body: &syn::Data,
) -> StatTriggerData {
let mut id = None;
let mut cond = None;
let mut action = None;
let mut value = None;
let mut fixed_groups = HashMap::new();
for attr in attr_val {
let (name, val) = match *attr {
syn::NestedMeta::Meta(ref item) => match *item {
syn::Meta::NameValue(ref name_value) => {
let ident = name_value
.path
.get_ident()
.expect("Invalid format for StatTrigger");
if let syn::Lit::Str(ref s) = name_value.lit {
(ident.to_string(), s.value())
} else {
panic!("Invalid format for StatTrigger - value must be a string");
}
}
_ => panic!("Invalid format for StatTrigger - value must be a string"),
},
_ => panic!("Invalid format for StatTrigger - parameters must be key-value pairs"),
};
match name.as_ref() {
"StatName" => id = Some(format_ident!("{}", val)),
"Condition" => {
let token_stream: TokenStream = val.parse().unwrap();
cond = Some(
syn::parse(token_stream).expect("Could not parse condition in StatTrigger"),
);
}
"Action" => {
action =
Some(StatTriggerAction::from_str(&val).expect("Invalid Action in StatTrigger"))
}
"Value" => {
value = Some(StatTriggerValue::Fixed(
val.parse::<i64>().expect("Invalid Value in StatTrigger"),
))
}
"ValueFrom" => {
let token_stream: TokenStream = val.parse().unwrap();
value = Some(StatTriggerValue::Expr(
syn::parse(token_stream).expect("Invalid ValueFrom in StatTrigger"),
));
}
"FixedGroups" => {
let groups = val.split(',');
for group in groups {
let mut split = group.splitn(2, '=');
let group_name = split.next().expect("Invalid format for FixedGroups");
let group_val = split.next().expect("Invalid format for FixedGroups");
fixed_groups.insert(group_name.to_string(), group_val.to_string());
}
}
_ => panic!("Unrecognised key in StatTrigger attribute"),
}
}
let id = id.expect("StatTrigger missing value for StatName");
let field_groups = if let syn::Data::Struct(ref data_struct) = *body {
data_struct
.fields
.iter()
.filter(|f| {
f.attrs
.iter()
.any(|a| a.path.is_ident("StatGroup") && is_attr_stat_id(a, &id))
})
.map(|f| f.clone().ident.expect("No identifier for field!"))
.collect::<Vec<_>>()
} else {
vec![]
};
let bucket_field = if let syn::Data::Struct(ref data_struct) = *body {
let bucket_by_fields = data_struct
.fields
.iter()
.filter(|f| {
f.attrs
.iter()
.any(|a| a.path.is_ident("BucketBy") && is_attr_stat_id(a, &id))
})
.map(|f| f.clone().ident.expect("No identifier for field!"))
.collect::<Vec<_>>();
if bucket_by_fields.len() > 1 {
panic!("The BucketBy attribute can be added to at most one field");
}
bucket_by_fields.into_iter().next()
} else {
None
};
StatTriggerData {
id,
condition_body: cond.unwrap_or_else(|| syn::parse_quote!(true)),
action: action.expect("StatTrigger missing value for Action"),
val: value.expect("StatTrigger missing value for Value or ValueFrom"),
fixed_groups,
field_groups,
bucket_by: bucket_field,
}
}