metrics_util/layers/
filter.rs

1use crate::layers::Layer;
2use aho_corasick::{AhoCorasick, AhoCorasickBuilder, AhoCorasickKind};
3use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
4
5/// Filters and discards metrics matching certain name patterns.
6///
7/// More information on the behavior of the layer can be found in [`FilterLayer`].
8#[derive(Debug)]
9pub struct Filter<R> {
10    inner: R,
11    automaton: AhoCorasick,
12}
13
14impl<R> Filter<R> {
15    fn should_filter(&self, key: &str) -> bool {
16        self.automaton.is_match(key)
17    }
18}
19
20impl<R: Recorder> Recorder for Filter<R> {
21    fn describe_counter(&self, key_name: KeyName, unit: Option<Unit>, description: SharedString) {
22        if self.should_filter(key_name.as_str()) {
23            return;
24        }
25        self.inner.describe_counter(key_name, unit, description)
26    }
27
28    fn describe_gauge(&self, key_name: KeyName, unit: Option<Unit>, description: SharedString) {
29        if self.should_filter(key_name.as_str()) {
30            return;
31        }
32        self.inner.describe_gauge(key_name, unit, description)
33    }
34
35    fn describe_histogram(&self, key_name: KeyName, unit: Option<Unit>, description: SharedString) {
36        if self.should_filter(key_name.as_str()) {
37            return;
38        }
39        self.inner.describe_histogram(key_name, unit, description)
40    }
41
42    fn register_counter(&self, key: &Key, metadata: &Metadata<'_>) -> Counter {
43        if self.should_filter(key.name()) {
44            return Counter::noop();
45        }
46        self.inner.register_counter(key, metadata)
47    }
48
49    fn register_gauge(&self, key: &Key, metadata: &Metadata<'_>) -> Gauge {
50        if self.should_filter(key.name()) {
51            return Gauge::noop();
52        }
53        self.inner.register_gauge(key, metadata)
54    }
55
56    fn register_histogram(&self, key: &Key, metadata: &Metadata<'_>) -> Histogram {
57        if self.should_filter(key.name()) {
58            return Histogram::noop();
59        }
60        self.inner.register_histogram(key, metadata)
61    }
62}
63
64/// A layer for filtering and discarding metrics matching certain name patterns.
65///
66/// Uses an [Aho-Corasick][ahocorasick] automaton to efficiently match a metric key against
67/// multiple patterns at once.  Patterns are matched across the entire key i.e. they are
68/// matched as substrings.
69///
70/// If a metric key matches any of the configured patterns, it will be skipped entirely.  This
71/// applies equally to metric registration and metric emission.
72///
73/// A number of options are exposed that control the underlying automaton, such as compilation to a
74/// DFA, or case sensitivity.
75///
76/// [ahocorasick]: https://en.wikipedia.org/wiki/Aho–Corasick_algorithm
77#[derive(Default, Debug)]
78pub struct FilterLayer {
79    patterns: Vec<String>,
80    case_insensitive: bool,
81    use_dfa: bool,
82}
83
84impl FilterLayer {
85    /// Creates a [`FilterLayer`] from an existing set of patterns.
86    pub fn from_patterns<P, I>(patterns: P) -> Self
87    where
88        P: IntoIterator<Item = I>,
89        I: AsRef<str>,
90    {
91        FilterLayer {
92            patterns: patterns.into_iter().map(|s| s.as_ref().to_string()).collect(),
93            case_insensitive: false,
94            use_dfa: true,
95        }
96    }
97
98    /// Adds a pattern to match.
99    pub fn add_pattern<P>(&mut self, pattern: P) -> &mut FilterLayer
100    where
101        P: AsRef<str>,
102    {
103        self.patterns.push(pattern.as_ref().to_string());
104        self
105    }
106
107    /// Sets the case sensitivity used for pattern matching.
108    ///
109    /// Defaults to `false` i.e. searches are case sensitive.
110    pub fn case_insensitive(&mut self, case_insensitive: bool) -> &mut FilterLayer {
111        self.case_insensitive = case_insensitive;
112        self
113    }
114
115    /// Sets whether or not to internally use a deterministic finite automaton.
116    ///
117    /// The main benefit to a DFA is that it can execute searches more quickly than a NFA (perhaps
118    /// 2-4 times as fast). The main drawback is that the DFA uses more space and can take much
119    /// longer to build.
120    ///
121    /// Enabling this option does not change the time complexity for constructing the underlying
122    /// Aho-Corasick automaton (which is O(p) where p is the total number of patterns being
123    /// compiled). Enabling this option does however reduce the time complexity of non-overlapping
124    /// searches from O(n + p) to O(n), where n is the length of the haystack.
125    ///
126    /// In general, it's a good idea to enable this if you're searching a small number of fairly
127    /// short patterns, or if you want the fastest possible search without regard to
128    /// compilation time or space usage.
129    ///
130    /// Defaults to `true`.
131    pub fn use_dfa(&mut self, dfa: bool) -> &mut FilterLayer {
132        self.use_dfa = dfa;
133        self
134    }
135}
136
137impl<R> Layer<R> for FilterLayer {
138    type Output = Filter<R>;
139
140    fn layer(&self, inner: R) -> Self::Output {
141        let mut automaton_builder = AhoCorasickBuilder::new();
142        let automaton = automaton_builder
143            .ascii_case_insensitive(self.case_insensitive)
144            .kind(self.use_dfa.then_some(AhoCorasickKind::DFA))
145            .build(&self.patterns)
146            // Documentation for `AhoCorasickBuilder::build` states that the error here will be
147            // related to exceeding some internal limits, but that those limits should generally be
148            // large enough for most use cases.. so I'm making the executive decision to consider
149            // that "good enough" and treat this as an exceptional error if it does occur.
150            .expect("should not fail to build filter automaton");
151        Filter { inner, automaton }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::FilterLayer;
158    use crate::{layers::Layer, test_util::*};
159    use metrics::{Counter, Gauge, Histogram, Unit};
160
161    static METADATA: metrics::Metadata =
162        metrics::Metadata::new(module_path!(), metrics::Level::INFO, Some(module_path!()));
163
164    #[test]
165    fn test_basic_functionality() {
166        let inputs = vec![
167            RecorderOperation::DescribeCounter(
168                "tokio.loops".into(),
169                Some(Unit::Count),
170                "counter desc".into(),
171            ),
172            RecorderOperation::DescribeGauge(
173                "hyper.bytes_read".into(),
174                Some(Unit::Bytes),
175                "gauge desc".into(),
176            ),
177            RecorderOperation::DescribeHistogram(
178                "hyper.response_latency".into(),
179                Some(Unit::Nanoseconds),
180                "histogram desc".into(),
181            ),
182            RecorderOperation::DescribeCounter(
183                "tokio.spurious_wakeups".into(),
184                Some(Unit::Count),
185                "counter desc".into(),
186            ),
187            RecorderOperation::DescribeGauge(
188                "bb8.pooled_conns".into(),
189                Some(Unit::Count),
190                "gauge desc".into(),
191            ),
192            RecorderOperation::RegisterCounter("tokio.loops".into(), Counter::noop(), &METADATA),
193            RecorderOperation::RegisterGauge("hyper.bytes_read".into(), Gauge::noop(), &METADATA),
194            RecorderOperation::RegisterHistogram(
195                "hyper.response_latency".into(),
196                Histogram::noop(),
197                &METADATA,
198            ),
199            RecorderOperation::RegisterCounter(
200                "tokio.spurious_wakeups".into(),
201                Counter::noop(),
202                &METADATA,
203            ),
204            RecorderOperation::RegisterGauge("bb8.pooled_conns".into(), Gauge::noop(), &METADATA),
205        ];
206
207        let expectations = vec![
208            RecorderOperation::DescribeGauge(
209                "hyper.bytes_read".into(),
210                Some(Unit::Bytes),
211                "gauge desc".into(),
212            ),
213            RecorderOperation::DescribeHistogram(
214                "hyper.response_latency".into(),
215                Some(Unit::Nanoseconds),
216                "histogram desc".into(),
217            ),
218            RecorderOperation::RegisterGauge("hyper.bytes_read".into(), Gauge::noop(), &METADATA),
219            RecorderOperation::RegisterHistogram(
220                "hyper.response_latency".into(),
221                Histogram::noop(),
222                &METADATA,
223            ),
224        ];
225
226        let recorder = MockBasicRecorder::from_operations(expectations);
227        let filter = FilterLayer::from_patterns(["tokio", "bb8"]);
228        let filter = filter.layer(recorder);
229
230        for operation in inputs {
231            operation.apply_to_recorder(&filter);
232        }
233    }
234
235    #[test]
236    fn test_case_insensitivity() {
237        let inputs = vec![
238            RecorderOperation::DescribeCounter(
239                "tokiO.loops".into(),
240                Some(Unit::Count),
241                "counter desc".into(),
242            ),
243            RecorderOperation::DescribeGauge(
244                "hyper.bytes_read".into(),
245                Some(Unit::Bytes),
246                "gauge desc".into(),
247            ),
248            RecorderOperation::DescribeHistogram(
249                "hyper.response_latency".into(),
250                Some(Unit::Nanoseconds),
251                "histogram desc".into(),
252            ),
253            RecorderOperation::DescribeCounter(
254                "Tokio.spurious_wakeups".into(),
255                Some(Unit::Count),
256                "counter desc".into(),
257            ),
258            RecorderOperation::DescribeGauge(
259                "bB8.pooled_conns".into(),
260                Some(Unit::Count),
261                "gauge desc".into(),
262            ),
263            RecorderOperation::RegisterCounter("tokiO.loops".into(), Counter::noop(), &METADATA),
264            RecorderOperation::RegisterGauge("hyper.bytes_read".into(), Gauge::noop(), &METADATA),
265            RecorderOperation::RegisterHistogram(
266                "hyper.response_latency".into(),
267                Histogram::noop(),
268                &METADATA,
269            ),
270            RecorderOperation::RegisterCounter(
271                "Tokio.spurious_wakeups".into(),
272                Counter::noop(),
273                &METADATA,
274            ),
275            RecorderOperation::RegisterGauge("bB8.pooled_conns".into(), Gauge::noop(), &METADATA),
276        ];
277
278        let expectations = vec![
279            RecorderOperation::DescribeGauge(
280                "hyper.bytes_read".into(),
281                Some(Unit::Bytes),
282                "gauge desc".into(),
283            ),
284            RecorderOperation::DescribeHistogram(
285                "hyper.response_latency".into(),
286                Some(Unit::Nanoseconds),
287                "histogram desc".into(),
288            ),
289            RecorderOperation::RegisterGauge("hyper.bytes_read".into(), Gauge::noop(), &METADATA),
290            RecorderOperation::RegisterHistogram(
291                "hyper.response_latency".into(),
292                Histogram::noop(),
293                &METADATA,
294            ),
295        ];
296
297        let recorder = MockBasicRecorder::from_operations(expectations);
298        let mut filter = FilterLayer::from_patterns(["tokio", "bb8"]);
299        let filter = filter.case_insensitive(true).layer(recorder);
300
301        for operation in inputs {
302            operation.apply_to_recorder(&filter);
303        }
304    }
305}