diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..05caea0a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 diff --git a/Cargo.toml b/Cargo.toml index bed9f658..c026295b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ members = [ "metrics-benchmark", "metrics-exporter-tcp", "metrics-exporter-prometheus", + "metrics-tracing-context", ] diff --git a/metrics-exporter-prometheus/src/lib.rs b/metrics-exporter-prometheus/src/lib.rs index e7435c1f..10b5b578 100644 --- a/metrics-exporter-prometheus/src/lib.rs +++ b/metrics-exporter-prometheus/src/lib.rs @@ -6,7 +6,7 @@ use hyper::{ service::{make_service_fn, service_fn}, {Body, Error as HyperError, Response, Server}, }; -use metrics::{Identifier, Key, Label, Recorder, SetRecorderError}; +use metrics::{Key, Recorder, SetRecorderError}; use metrics_util::{ parse_quantiles, CompositeKey, Handle, Histogram, MetricKind, Quantile, Registry, }; @@ -494,73 +494,77 @@ impl PrometheusBuilder { } impl Recorder for PrometheusRecorder { - fn register_counter(&self, key: Key, description: Option<&'static str>) -> Identifier { + fn register_counter(&self, key: Key, description: Option<&'static str>) { self.add_description_if_missing(&key, description); - self.inner - .registry() - .get_or_create_identifier(CompositeKey::new(MetricKind::Counter, key), |_| { - Handle::counter() - }) + self.inner.registry().op( + CompositeKey::new(MetricKind::Counter, key), + |_| {}, + || Handle::counter(), + ); } - fn register_gauge(&self, key: Key, description: Option<&'static str>) -> Identifier { + fn register_gauge(&self, key: Key, description: Option<&'static str>) { self.add_description_if_missing(&key, description); - self.inner - .registry() - .get_or_create_identifier(CompositeKey::new(MetricKind::Gauge, key), |_| { - Handle::gauge() - }) + self.inner.registry().op( + CompositeKey::new(MetricKind::Gauge, key), + |_| {}, + || Handle::gauge(), + ); } - fn register_histogram(&self, key: Key, description: Option<&'static str>) -> Identifier { + fn register_histogram(&self, key: Key, description: Option<&'static str>) { self.add_description_if_missing(&key, description); - self.inner - .registry() - .get_or_create_identifier(CompositeKey::new(MetricKind::Histogram, key), |_| { - Handle::histogram() - }) + self.inner.registry().op( + CompositeKey::new(MetricKind::Histogram, key), + |_| {}, + || Handle::histogram(), + ); } - fn increment_counter(&self, id: Identifier, value: u64) { - self.inner - .registry() - .with_handle(id, |h| h.increment_counter(value)); + fn increment_counter(&self, key: Key, value: u64) { + self.inner.registry().op( + CompositeKey::new(MetricKind::Counter, key), + |h| h.increment_counter(value), + || Handle::counter(), + ); } - fn update_gauge(&self, id: Identifier, value: f64) { - self.inner - .registry() - .with_handle(id, |h| h.update_gauge(value)); + fn update_gauge(&self, key: Key, value: f64) { + self.inner.registry().op( + CompositeKey::new(MetricKind::Gauge, key), + |h| h.update_gauge(value), + || Handle::gauge(), + ); } - fn record_histogram(&self, id: Identifier, value: u64) { - self.inner - .registry() - .with_handle(id, |h| h.record_histogram(value)); + fn record_histogram(&self, key: Key, value: u64) { + self.inner.registry().op( + CompositeKey::new(MetricKind::Histogram, key), + |h| h.record_histogram(value), + || Handle::histogram(), + ); } } fn key_to_parts(key: Key) -> (String, Vec) { - let (name, labels) = key.into_parts(); + let name = key.name(); + let labels = key.labels(); let sanitize = |c| c == '.' || c == '=' || c == '{' || c == '}' || c == '+' || c == '-'; let name = name.replace(sanitize, "_"); let labels = labels - .map(|labels| { - labels - .into_iter() - .map(Label::into_parts) - .map(|(k, v)| { - format!( - "{}=\"{}\"", - k, - v.replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - ) - }) - .collect() + .into_iter() + .map(|label| { + let k = label.key(); + let v = label.value(); + format!( + "{}=\"{}\"", + k, + v.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + ) }) - .unwrap_or_else(|| Vec::new()); + .collect(); (name, labels) } diff --git a/metrics-exporter-tcp/src/lib.rs b/metrics-exporter-tcp/src/lib.rs index eaadee40..c58fb3ca 100644 --- a/metrics-exporter-tcp/src/lib.rs +++ b/metrics-exporter-tcp/src/lib.rs @@ -53,8 +53,7 @@ use std::time::SystemTime; use bytes::Bytes; use crossbeam_channel::{bounded, unbounded, Receiver, Sender}; -use metrics::{Identifier, Key, Recorder, SetRecorderError}; -use metrics_util::Registry; +use metrics::{Key, Recorder, SetRecorderError}; use mio::{ net::{TcpListener, TcpStream}, Events, Interest, Poll, Token, Waker, @@ -71,30 +70,12 @@ mod proto { include!(concat!(env!("OUT_DIR"), "/event.proto.rs")); } -type TcpRegistry = Registry; - -#[derive(Eq, PartialEq, Hash, Clone)] -enum MetricKind { - Counter, - Gauge, - Histogram, -} - enum MetricValue { Counter(u64), Gauge(f64), Histogram(u64), } -#[derive(Eq, PartialEq, Hash, Clone)] -struct CompositeKey(MetricKind, Key); - -impl CompositeKey { - pub fn key(&self) -> &Key { - &self.1 - } -} - /// Errors that could occur while installing a TCP recorder/exporter. #[derive(Debug)] pub enum Error { @@ -119,8 +100,7 @@ impl From for Error { /// A TCP recorder. pub struct TcpRecorder { - registry: Arc, - tx: Sender<(Identifier, MetricValue)>, + tx: Sender<(Key, MetricValue)>, waker: Arc, } @@ -200,67 +180,48 @@ impl TcpBuilder { poll.registry() .register(&mut listener, LISTENER, Interest::READABLE)?; - let registry = Arc::new(Registry::new()); - let recorder = TcpRecorder { - registry: Arc::clone(®istry), tx, waker: Arc::clone(&waker), }; - thread::spawn(move || run_transport(registry, poll, waker, listener, rx, buffer_size)); + thread::spawn(move || run_transport(poll, waker, listener, rx, buffer_size)); Ok(recorder) } } impl TcpRecorder { - fn register_metric(&self, kind: MetricKind, key: Key) -> Identifier { - let ckey = CompositeKey(kind, key); - self.registry.get_or_create_identifier(ckey, |k| k.clone()) - } - - fn push_metric(&self, id: Identifier, value: MetricValue) { - let id = match id { - Identifier::Invalid => return, - Identifier::Valid(_) => id, - }; - let _ = self.tx.try_send((id, value)); + fn push_metric(&self, key: Key, value: MetricValue) { + let _ = self.tx.try_send((key, value)); let _ = self.waker.wake(); } } impl Recorder for TcpRecorder { - fn register_counter(&self, key: Key, _description: Option<&'static str>) -> Identifier { - self.register_metric(MetricKind::Counter, key) - } + fn register_counter(&self, _key: Key, _description: Option<&'static str>) {} - fn register_gauge(&self, key: Key, _description: Option<&'static str>) -> Identifier { - self.register_metric(MetricKind::Gauge, key) - } + fn register_gauge(&self, _key: Key, _description: Option<&'static str>) {} - fn register_histogram(&self, key: Key, _description: Option<&'static str>) -> Identifier { - self.register_metric(MetricKind::Histogram, key) - } + fn register_histogram(&self, _key: Key, _description: Option<&'static str>) {} - fn increment_counter(&self, id: Identifier, value: u64) { - self.push_metric(id, MetricValue::Counter(value)); + fn increment_counter(&self, key: Key, value: u64) { + self.push_metric(key, MetricValue::Counter(value)); } - fn update_gauge(&self, id: Identifier, value: f64) { - self.push_metric(id, MetricValue::Gauge(value)); + fn update_gauge(&self, key: Key, value: f64) { + self.push_metric(key, MetricValue::Gauge(value)); } - fn record_histogram(&self, id: Identifier, value: u64) { - self.push_metric(id, MetricValue::Histogram(value)); + fn record_histogram(&self, key: Key, value: u64) { + self.push_metric(key, MetricValue::Histogram(value)); } } fn run_transport( - registry: Arc, mut poll: Poll, waker: Arc, listener: TcpListener, - rx: Receiver<(Identifier, MetricValue)>, + rx: Receiver<(Key, MetricValue)>, buffer_size: Option, ) { let buffer_limit = buffer_size.unwrap_or(std::usize::MAX); @@ -309,10 +270,10 @@ fn run_transport( // If our sender is dead, we can't do anything else, so just return. Err(_) => return, }; - match convert_metric_to_protobuf_encoded(®istry, msg.0, msg.1) { - Some(Ok(pmsg)) => buffered_pmsgs.push_back(pmsg), - Some(Err(e)) => error!(error = ?e, "error encoding metric"), - None => error!(metric_id = ?msg.0, "unknown metric"), + let (key, value) = msg; + match convert_metric_to_protobuf_encoded(key, value) { + Ok(pmsg) => buffered_pmsgs.push_back(pmsg), + Err(e) => error!(error = ?e, "error encoding metric"), } } drop(_mrxspan); @@ -460,46 +421,31 @@ fn drive_connection( } } -fn convert_metric_to_protobuf_encoded( - registry: &Arc, - id: Identifier, - value: MetricValue, -) -> Option> { - let id = match id { - Identifier::Invalid => return None, - Identifier::Valid(_) => id, +fn convert_metric_to_protobuf_encoded(key: Key, value: MetricValue) -> Result { + let name = key.name().to_string(); + let labels = key + .labels() + .map(|label| (label.key().to_owned(), label.value().to_owned())) + .collect::>(); + let mvalue = match value { + MetricValue::Counter(cv) => proto::metric::Value::Counter(proto::Counter { value: cv }), + MetricValue::Gauge(gv) => proto::metric::Value::Gauge(proto::Gauge { value: gv }), + MetricValue::Histogram(hv) => { + proto::metric::Value::Histogram(proto::Histogram { value: hv }) + } }; - registry.with_handle(id, |ckey| { - let name = ckey.key().name().to_string(); - let labels = ckey - .key() - .labels() - .map(|labels| { - labels - .map(|label| (label.key().to_string(), label.value().to_string())) - .collect::>() - }) - .unwrap_or_else(|| BTreeMap::new()); - let mvalue = match value { - MetricValue::Counter(cv) => proto::metric::Value::Counter(proto::Counter { value: cv }), - MetricValue::Gauge(gv) => proto::metric::Value::Gauge(proto::Gauge { value: gv }), - MetricValue::Histogram(hv) => { - proto::metric::Value::Histogram(proto::Histogram { value: hv }) - } - }; - let now: prost_types::Timestamp = SystemTime::now().into(); - let metric = proto::Metric { - name, - labels, - timestamp: Some(now), - value: Some(mvalue), - }; + let now: prost_types::Timestamp = SystemTime::now().into(); + let metric = proto::Metric { + name, + labels, + timestamp: Some(now), + value: Some(mvalue), + }; - let mut buf = Vec::new(); - metric.encode_length_delimited(&mut buf)?; - Ok(Bytes::from(buf)) - }) + let mut buf = Vec::new(); + metric.encode_length_delimited(&mut buf)?; + Ok(Bytes::from(buf)) } fn next(current: &mut Token) -> Token { diff --git a/metrics-macros/src/lib.rs b/metrics-macros/src/lib.rs index f9fcfd21..b3046ca9 100644 --- a/metrics-macros/src/lib.rs +++ b/metrics-macros/src/lib.rs @@ -7,6 +7,9 @@ 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), @@ -102,7 +105,7 @@ pub fn register_counter(input: TokenStream) -> TokenStream { labels, } = parse_macro_input!(input as Registration); - get_expanded_registration("counter", key, description, labels) + get_expanded_registration("counter", key, description, labels).into() } #[proc_macro_hack] @@ -113,7 +116,7 @@ pub fn register_gauge(input: TokenStream) -> TokenStream { labels, } = parse_macro_input!(input as Registration); - get_expanded_registration("gauge", key, description, labels) + get_expanded_registration("gauge", key, description, labels).into() } #[proc_macro_hack] @@ -124,7 +127,7 @@ pub fn register_histogram(input: TokenStream) -> TokenStream { labels, } = parse_macro_input!(input as Registration); - get_expanded_registration("histogram", key, description, labels) + get_expanded_registration("histogram", key, description, labels).into() } #[proc_macro_hack] @@ -133,7 +136,7 @@ pub fn increment(input: TokenStream) -> TokenStream { let op_value = quote! { 1 }; - get_expanded_callsite("counter", "increment", key, labels, op_value) + get_expanded_callsite("counter", "increment", key, labels, op_value).into() } #[proc_macro_hack] @@ -144,7 +147,7 @@ pub fn counter(input: TokenStream) -> TokenStream { labels, } = parse_macro_input!(input as WithExpression); - get_expanded_callsite("counter", "increment", key, labels, op_value) + get_expanded_callsite("counter", "increment", key, labels, op_value).into() } #[proc_macro_hack] @@ -155,7 +158,7 @@ pub fn gauge(input: TokenStream) -> TokenStream { labels, } = parse_macro_input!(input as WithExpression); - get_expanded_callsite("gauge", "update", key, labels, op_value) + get_expanded_callsite("gauge", "update", key, labels, op_value).into() } #[proc_macro_hack] @@ -166,7 +169,7 @@ pub fn histogram(input: TokenStream) -> TokenStream { labels, } = parse_macro_input!(input as WithExpression); - get_expanded_callsite("histogram", "record", key, labels, op_value) + get_expanded_callsite("histogram", "record", key, labels, op_value).into() } fn get_expanded_registration( @@ -174,7 +177,7 @@ fn get_expanded_registration( key: Key, description: Option, labels: Option, -) -> TokenStream { +) -> proc_macro2::TokenStream { let register_ident = format_ident!("register_{}", metric_type); let key = key_to_quoted(key, labels); @@ -183,16 +186,16 @@ fn get_expanded_registration( None => quote! { None }, }; - let expanded = quote! { + quote! { { // Only do this work if there's a recorder installed. if let Some(recorder) = metrics::try_recorder() { - recorder.#register_ident(#key, #description); + // Registrations are fairly rare, don't attempt to cache here + // and just use an owned ref. + recorder.#register_ident(metrics::Key::Owned(#key), #description); } } - }; - - TokenStream::from(expanded) + } } fn get_expanded_callsite( @@ -201,12 +204,10 @@ fn get_expanded_callsite( key: Key, labels: Option, op_values: V, -) -> TokenStream +) -> proc_macro2::TokenStream where V: ToTokens, { - let register_ident = format_ident!("register_{}", metric_type); - let op_ident = format_ident!("{}_{}", op_type, metric_type); let use_fast_path = can_use_fast_path(&labels); let key = key_to_quoted(key, labels); @@ -218,21 +219,20 @@ where quote! { #op_values } }; - let expanded = if use_fast_path { - // We're on the fast path here, so we'll end up registering with the recorder - // and statically caching the identifier for our metric to speed up any future - // increment operations. + 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 METRICS_INIT: metrics::OnceIdentifier = metrics::OnceIdentifier::new(); + 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 cached identifier. - let id = METRICS_INIT.get_or_init(|| { - recorder.#register_ident(#key, None) - }); - recorder.#op_ident(id, #op_values); + // Initialize our fast path. + let key = CACHED_KEY.get_or_init(|| { #key }); + recorder.#op_ident(metrics::Key::Borrowed(&key), #op_values); } } } @@ -244,14 +244,21 @@ where { // Only do this work if there's a recorder installed. if let Some(recorder) = metrics::try_recorder() { - let id = recorder.#register_ident(#key, None); - recorder.#op_ident(id, #op_values); + recorder.#op_ident(metrics::Key::Owned(#key), #op_values); } } } - }; + } +} - TokenStream::from(expanded) +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 { @@ -265,8 +272,8 @@ fn read_key(input: &mut ParseStream) -> Result { } } -fn key_to_quoted(key: Key, labels: Option) -> proc_macro2::TokenStream { - let name = match key { +fn quote_key_name(key: Key) -> proc_macro2::TokenStream { + match key { Key::NotScoped(s) => { quote! { #s } } @@ -275,45 +282,22 @@ fn key_to_quoted(key: Key, labels: Option) -> proc_macro2::TokenStream { format!("{}.{}", std::module_path!().replace("::", "."), #s) } } - }; - - match labels { - None => quote! { metrics::Key::from_name(#name) }, - Some(labels) => match labels { - Labels::Inline(pairs) => { - let labels = pairs - .into_iter() - .map(|(k, v)| quote! { metrics::Label::new(#k, #v) }); - quote! { - metrics::Key::from_name_and_labels(#name, vec![#(#labels),*]) - } - } - Labels::Existing(e) => { - quote! { - metrics::Key::from_name_and_labels(#name, #e) - } - } - }, } } -fn can_use_fast_path(labels: &Option) -> bool { +fn key_to_quoted(key: Key, labels: Option) -> proc_macro2::TokenStream { + let name = quote_key_name(key); + match labels { - None => true, + None => quote! { metrics::KeyData::from_name(#name) }, Some(labels) => match labels { - Labels::Existing(_) => false, Labels::Inline(pairs) => { - let mut use_fast_path = true; - for (_, lvalue) in pairs { - match lvalue { - Expr::Lit(_) => {} - _ => { - use_fast_path = false; - } - } - } - use_fast_path + 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) }, }, } } @@ -345,6 +329,10 @@ fn parse_labels(input: &mut ParseStream) -> Result> { break; } input.parse::()?; + if input.is_empty() { + break; + } + let lkey: LitStr = input.parse()?; input.parse::]>()?; let lvalue: Expr = input.parse()?; @@ -355,13 +343,25 @@ fn parse_labels(input: &mut ParseStream) -> Result> { return Ok(Some(Labels::Inline(labels))); } - // Has to be an expression otherwise. + // 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))) } diff --git a/metrics-macros/src/tests.rs b/metrics-macros/src/tests.rs new file mode 100644 index 00000000..ff4f7868 --- /dev/null +++ b/metrics-macros/src/tests.rs @@ -0,0 +1,138 @@ +use syn::parse_quote; + +use super::*; + +#[test] +fn test_quote_key_name_scoped() { + let stream = quote_key_name(Key::Scoped(parse_quote! {"qwerty"})); + let expected = + "format ! ( \"{}.{}\" , std :: module_path ! ( ) . replace ( \"::\" , \".\" ) , \"qwerty\" )"; + assert_eq!(stream.to_string(), expected); +} + +#[test] +fn test_quote_key_name_not_scoped() { + let stream = quote_key_name(Key::NotScoped(parse_quote! {"qwerty"})); + let expected = "\"qwerty\""; + assert_eq!(stream.to_string(), expected); +} + +#[test] +fn test_get_expanded_registration() { + let stream = get_expanded_registration( + "mytype", + Key::NotScoped(parse_quote! {"mykeyname"}), + None, + None, + ); + + let expected = concat!( + "{ if let Some ( recorder ) = metrics :: try_recorder ( ) { ", + "recorder . register_mytype ( ", + "metrics :: Key :: Owned ( metrics :: KeyData :: from_name ( \"mykeyname\" ) ) , ", + "None ", + ") ; ", + "} }", + ); + + assert_eq!(stream.to_string(), expected); +} + +/// If there are no dynamic labels - generate an invocation with caching. +#[test] +fn test_get_expanded_callsite_fast_path() { + let stream = get_expanded_callsite( + "mytype", + "myop", + Key::NotScoped(parse_quote! {"mykeyname"}), + None, + quote! { 1 }, + ); + + let expected = concat!( + "{ ", + "static CACHED_KEY : metrics :: OnceKeyData = metrics :: OnceKeyData :: new ( ) ; ", + "if let Some ( recorder ) = metrics :: try_recorder ( ) { ", + "let key = CACHED_KEY . get_or_init ( || { ", + "metrics :: KeyData :: from_name ( \"mykeyname\" ) ", + "} ) ; ", + "recorder . myop_mytype ( metrics :: Key :: Borrowed ( & key ) , 1 ) ; ", + "} }", + ); + + assert_eq!(stream.to_string(), expected); +} + +/// If there are dynamic labels - generate a direct invocation. +#[test] +fn test_get_expanded_callsite_regular_path() { + let stream = get_expanded_callsite( + "mytype", + "myop", + Key::NotScoped(parse_quote! {"mykeyname"}), + Some(Labels::Existing(parse_quote! { mylabels })), + quote! { 1 }, + ); + + let expected = concat!( + "{ ", + "if let Some ( recorder ) = metrics :: try_recorder ( ) { ", + "recorder . myop_mytype ( ", + "metrics :: Key :: Owned ( metrics :: KeyData :: from_name_and_labels ( \"mykeyname\" , mylabels ) ) , ", + "1 ", + ") ; ", + "} }", + ); + + assert_eq!(stream.to_string(), expected); +} + +#[test] +fn test_key_to_quoted_no_labels() { + let stream = key_to_quoted(Key::NotScoped(parse_quote! {"mykeyname"}), None); + let expected = "metrics :: KeyData :: from_name ( \"mykeyname\" )"; + assert_eq!(stream.to_string(), expected); +} + +#[test] +fn test_key_to_quoted_existing_labels() { + let stream = key_to_quoted( + Key::NotScoped(parse_quote! {"mykeyname"}), + Some(Labels::Existing(Expr::Path(parse_quote! { mylabels }))), + ); + let expected = "metrics :: KeyData :: from_name_and_labels ( \"mykeyname\" , mylabels )"; + assert_eq!(stream.to_string(), expected); +} + +/// Registration can only operate on static labels (i.e. labels baked into the +/// Key). +#[test] +fn test_key_to_quoted_inline_labels() { + let stream = key_to_quoted( + Key::NotScoped(parse_quote! {"mykeyname"}), + Some(Labels::Inline(vec![ + (parse_quote! {"mylabel1"}, parse_quote! { mylabel1 }), + (parse_quote! {"mylabel2"}, parse_quote! { "mylabel2" }), + ])), + ); + let expected = concat!( + "metrics :: KeyData :: from_name_and_labels ( \"mykeyname\" , vec ! [ ", + "metrics :: Label :: new ( \"mylabel1\" , mylabel1 ) , ", + "metrics :: Label :: new ( \"mylabel2\" , \"mylabel2\" ) ", + "] )" + ); + assert_eq!(stream.to_string(), expected); +} + +#[test] +fn test_key_to_quoted_inline_labels_empty() { + let stream = key_to_quoted( + Key::NotScoped(parse_quote! {"mykeyname"}), + Some(Labels::Inline(vec![])), + ); + let expected = concat!( + "metrics :: KeyData :: from_name_and_labels ( \"mykeyname\" , vec ! [ ", + "] )" + ); + assert_eq!(stream.to_string(), expected); +} diff --git a/metrics-tracing-context/Cargo.toml b/metrics-tracing-context/Cargo.toml new file mode 100644 index 00000000..7fd39b2b --- /dev/null +++ b/metrics-tracing-context/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "metrics-tracing-context" +version = "0.1.0-alpha1" +authors = ["MOZGIII "] +edition = "2018" + +license = "MIT" + +description = "A crate to use tracing context as metrics labels." +homepage = "https://github.com/metrics-rs/metrics" +repository = "https://github.com/metrics-rs/metrics" +documentation = "https://docs.rs/metrics" +readme = "README.md" + +categories = ["development-tools::debugging"] +keywords = ["metrics", "tracing"] + +[dependencies] +metrics = { version = "0.13.0-alpha.1", path = "../metrics", features = ["std"] } +metrics-util = { version = "0.4.0-alpha.1", path = "../metrics-util" } +tracing = "0.1" +tracing-core = "0.1" +tracing-subscriber = "0.2" + +[dev-dependencies] +parking_lot = "0.11" diff --git a/metrics-tracing-context/README.md b/metrics-tracing-context/README.md new file mode 100644 index 00000000..6bbf810a --- /dev/null +++ b/metrics-tracing-context/README.md @@ -0,0 +1,3 @@ +# metrics-tracing-context + +A crate to use tracing context as metrics labels. diff --git a/metrics-tracing-context/src/label_filter.rs b/metrics-tracing-context/src/label_filter.rs new file mode 100644 index 00000000..a9b43ba3 --- /dev/null +++ b/metrics-tracing-context/src/label_filter.rs @@ -0,0 +1,20 @@ +//! Label filtering. + +use metrics::Label; + +/// [`LabelFilter`] trait encapsulates the ability to filter labels, i.e. +/// determining whether a particular span field should be included as a label or not. +pub trait LabelFilter { + /// Returns `true` if the passed label should be included in the key. + fn should_include_label(&self, label: &Label) -> bool; +} + +/// A [`LabelFilter`] that allows all labels. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct IncludeAll; + +impl LabelFilter for IncludeAll { + fn should_include_label(&self, _label: &Label) -> bool { + true + } +} diff --git a/metrics-tracing-context/src/lib.rs b/metrics-tracing-context/src/lib.rs new file mode 100644 index 00000000..c1b9368a --- /dev/null +++ b/metrics-tracing-context/src/lib.rs @@ -0,0 +1,165 @@ +//! Use [`tracing::span!`] fields as [`metrics`] labels. +//! +//! The `metrics-tracing-context` crate provides tools to enable injecting the +//! contextual data maintained via `span!` macro from the [`tracing`] crate +//! into the metrics. +//! +//! # Use +//! +//! First, set up `tracing` and `metrics` crates: +//! +//! ```rust +//! # use metrics_util::DebuggingRecorder; +//! # use tracing_subscriber::Registry; +//! use metrics_util::layers::Layer; +//! use tracing_subscriber::layer::SubscriberExt; +//! use metrics_tracing_context::{MetricsLayer, TracingContextLayer}; +//! +//! // Prepare tracing. +//! # let mysubscriber = Registry::default(); +//! let subscriber = mysubscriber.with(MetricsLayer::new()); +//! tracing::subscriber::set_global_default(subscriber).unwrap(); +//! +//! // Prepare metrics. +//! # let myrecorder = DebuggingRecorder::new(); +//! let recorder = TracingContextLayer::all().layer(myrecorder); +//! metrics::set_boxed_recorder(Box::new(recorder)).unwrap(); +//! ``` +//! +//! Then emit some metrics within spans and see the labels being injected! +//! +//! ```rust +//! # use metrics_util::{layers::Layer, DebuggingRecorder}; +//! # use tracing_subscriber::{layer::SubscriberExt, Registry}; +//! # use metrics_tracing_context::{MetricsLayer, TracingContextLayer}; +//! # let mysubscriber = Registry::default(); +//! # let subscriber = mysubscriber.with(MetricsLayer::new()); +//! # tracing::subscriber::set_global_default(subscriber).unwrap(); +//! # let myrecorder = DebuggingRecorder::new(); +//! # let recorder = TracingContextLayer::all().layer(myrecorder); +//! # metrics::set_boxed_recorder(Box::new(recorder)).unwrap(); +//! use tracing::{span, Level}; +//! use metrics::counter; +//! +//! let user = "ferris"; +//! let span = span!(Level::TRACE, "login", user); +//! let _guard = span.enter(); +//! +//! counter!("login_attempts", 1, "service" => "login_service"); +//! ``` +//! +//! The code above will emit a increment for a `login_attempts` counter with +//! the following labels: +//! - `service=login_service` +//! - `user=ferris` + +#![deny(missing_docs)] + +use metrics::{Key, KeyData, Label, Recorder}; +use metrics_util::layers::Layer; +use tracing::Span; + +pub mod label_filter; +mod tracing_integration; + +pub use label_filter::LabelFilter; +pub use tracing_integration::{MetricsLayer, SpanExt}; + +/// [`TracingContextLayer`] provides an implementation of a [`metrics::Layer`] +/// for [`TracingContext`]. +pub struct TracingContextLayer { + label_filter: F, +} + +impl TracingContextLayer { + /// Creates a new [`TracingContextLayer`]. + pub fn new(label_filter: F) -> Self { + Self { label_filter } + } +} + +impl TracingContextLayer { + /// Creates a new [`TracingContextLayer`]. + pub fn all() -> Self { + Self { + label_filter: label_filter::IncludeAll, + } + } +} + +impl Layer for TracingContextLayer +where + F: Clone, +{ + type Output = TracingContext; + + fn layer(&self, inner: R) -> Self::Output { + TracingContext { + inner, + label_filter: self.label_filter.clone(), + } + } +} + +/// [`TracingContext`] is a [`metrics::Recorder`] that injects labels from the +/// [`tracing::Span`]s. +pub struct TracingContext { + inner: R, + label_filter: F, +} + +impl TracingContext +where + F: LabelFilter, +{ + fn enhance_labels(&self, labels: &mut Vec