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