opentelemetry_sdk/metrics/
view.rs

1use super::instrument::{Instrument, Stream};
2#[cfg(feature = "spec_unstable_metrics_views")]
3use crate::metrics::{MetricError, MetricResult};
4#[cfg(feature = "spec_unstable_metrics_views")]
5use glob::Pattern;
6
7#[cfg(feature = "spec_unstable_metrics_views")]
8fn empty_view(_inst: &Instrument) -> Option<Stream> {
9    None
10}
11
12/// Used to customize the metrics that are output by the SDK.
13///
14/// Here are some examples when a [View] might be needed:
15///
16/// * Customize which Instruments are to be processed/ignored. For example, an
17///   instrumented library can provide both temperature and humidity, but the
18///   application developer might only want temperature.
19/// * Customize the aggregation - if the default aggregation associated with the
20///   [Instrument] does not meet the needs of the user. For example, an HTTP client
21///   library might expose HTTP client request duration as Histogram by default,
22///   but the application developer might only want the total count of outgoing
23///   requests.
24/// * Customize which attribute(s) are to be reported on metrics. For example,
25///   an HTTP server library might expose HTTP verb (e.g. GET, POST) and HTTP
26///   status code (e.g. 200, 301, 404). The application developer might only care
27///   about HTTP status code (e.g. reporting the total count of HTTP requests for
28///   each HTTP status code). There could also be extreme scenarios in which the
29///   application developer does not need any attributes (e.g. just get the total
30///   count of all incoming requests).
31///
32/// # Example Custom View
33///
34/// View is implemented for all `Fn(&Instrument) -> Option<Stream>`.
35///
36/// ```
37/// use opentelemetry_sdk::metrics::{Instrument, SdkMeterProvider, Stream};
38///
39/// // return streams for the given instrument
40/// let my_view = |i: &Instrument| {
41///   // return Some(Stream) or
42///   None
43/// };
44///
45/// let provider = SdkMeterProvider::builder().with_view(my_view).build();
46/// # drop(provider)
47/// ```
48#[allow(unreachable_pub)]
49pub trait View: Send + Sync + 'static {
50    /// Defines how data should be collected for certain instruments.
51    ///
52    /// Return [Stream] to use for matching [Instrument]s,
53    /// otherwise if there is no match, return `None`.
54    fn match_inst(&self, inst: &Instrument) -> Option<Stream>;
55}
56
57impl<T> View for T
58where
59    T: Fn(&Instrument) -> Option<Stream> + Send + Sync + 'static,
60{
61    fn match_inst(&self, inst: &Instrument) -> Option<Stream> {
62        self(inst)
63    }
64}
65
66impl View for Box<dyn View> {
67    fn match_inst(&self, inst: &Instrument) -> Option<Stream> {
68        (**self).match_inst(inst)
69    }
70}
71
72#[cfg(feature = "spec_unstable_metrics_views")]
73/// Creates a [View] that applies the [Stream] mask for all instruments that
74/// match criteria.
75///
76/// The returned [View] will only apply the mask if all non-empty fields of
77/// criteria match the corresponding [Instrument] passed to the view. If all
78/// fields of the criteria are their default values, a view that matches no
79/// instruments is returned. If you need to match an empty-value field, create a
80/// [View] directly.
81///
82/// The [Instrument::name] field of criteria supports wildcard pattern matching.
83/// The wildcard `*` is recognized as matching zero or more characters, and `?`
84/// is recognized as matching exactly one character. For example, a pattern of
85/// `*` will match all instrument names.
86///
87/// The [Stream] mask only applies updates for non-empty fields. By default, the
88/// [Instrument] the [View] matches against will be use for the name,
89/// description, and unit of the returned [Stream] and no `aggregation` or
90/// `allowed_attribute_keys` are set. All non-empty fields of mask are used
91/// instead of the default. If you need to set a an empty value in the returned
92/// stream, create a custom [View] directly.
93///
94/// # Example
95///
96/// ```
97/// use opentelemetry_sdk::metrics::{new_view, Aggregation, Instrument, Stream};
98///
99/// let criteria = Instrument::new().name("counter_*");
100/// let mask = Stream::new().aggregation(Aggregation::Sum);
101///
102/// let view = new_view(criteria, mask);
103/// # drop(view);
104/// ```
105pub fn new_view(criteria: Instrument, mask: Stream) -> MetricResult<Box<dyn View>> {
106    if criteria.is_empty() {
107        // TODO - The error is getting lost here. Need to return or log.
108        return Ok(Box::new(empty_view));
109    }
110    let contains_wildcard = criteria.name.contains(['*', '?']);
111
112    let match_fn: Box<dyn Fn(&Instrument) -> bool + Send + Sync> = if contains_wildcard {
113        if mask.name != "" {
114            // TODO - The error is getting lost here. Need to return or log.
115            return Ok(Box::new(empty_view));
116        }
117
118        let pattern = criteria.name.clone();
119        let glob_pattern =
120            Pattern::new(&pattern).map_err(|e| MetricError::Config(e.to_string()))?;
121
122        Box::new(move |i| {
123            glob_pattern.matches(&i.name)
124                && criteria.matches_description(i)
125                && criteria.matches_kind(i)
126                && criteria.matches_unit(i)
127                && criteria.matches_scope(i)
128        })
129    } else {
130        Box::new(move |i| criteria.matches(i))
131    };
132
133    let mut agg = None;
134    if let Some(ma) = &mask.aggregation {
135        match ma.validate() {
136            Ok(_) => agg = Some(ma.clone()),
137            Err(_) => {
138                // TODO - The error is getting lost here. Need to return or log.
139                return Ok(Box::new(empty_view));
140            }
141        }
142    }
143
144    Ok(Box::new(move |i: &Instrument| -> Option<Stream> {
145        if match_fn(i) {
146            Some(Stream {
147                name: if !mask.name.is_empty() {
148                    mask.name.clone()
149                } else {
150                    i.name.clone()
151                },
152                description: if !mask.description.is_empty() {
153                    mask.description.clone()
154                } else {
155                    i.description.clone()
156                },
157                unit: if !mask.unit.is_empty() {
158                    mask.unit.clone()
159                } else {
160                    i.unit.clone()
161                },
162                aggregation: agg.clone(),
163                allowed_attribute_keys: mask.allowed_attribute_keys.clone(),
164            })
165        } else {
166            None
167        }
168    }))
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    #[test]
175    fn test_new_view_matching_all() {
176        let criteria = Instrument::new().name("*");
177        let mask = Stream::new();
178
179        let view = new_view(criteria, mask).expect("Expected to create a new view");
180
181        let test_instrument = Instrument::new().name("test_instrument");
182        assert!(
183            view.match_inst(&test_instrument).is_some(),
184            "Expected to match all instruments with * pattern"
185        );
186    }
187
188    #[test]
189    fn test_new_view_exact_match() {
190        let criteria = Instrument::new().name("counter_exact_match");
191        let mask = Stream::new();
192
193        let view = new_view(criteria, mask).expect("Expected to create a new view");
194
195        let matching_instrument = Instrument::new().name("counter_exact_match");
196        assert!(
197            view.match_inst(&matching_instrument).is_some(),
198            "Expected to match instrument with exact name"
199        );
200
201        let non_matching_instrument = Instrument::new().name("counter_non_exact_match");
202        assert!(
203            view.match_inst(&non_matching_instrument).is_none(),
204            "Expected not to match instrument with different name"
205        );
206    }
207
208    #[test]
209    fn test_new_view_with_wildcard_pattern() {
210        let criteria = Instrument::new().name("prefix_*");
211        let mask = Stream::new();
212
213        let view = new_view(criteria, mask).expect("Expected to create a new view");
214
215        let matching_instrument = Instrument::new().name("prefix_counter");
216        assert!(
217            view.match_inst(&matching_instrument).is_some(),
218            "Expected to match instrument with matching prefix"
219        );
220
221        let non_matching_instrument = Instrument::new().name("nonprefix_counter");
222        assert!(
223            view.match_inst(&non_matching_instrument).is_none(),
224            "Expected not to match instrument with different prefix"
225        );
226    }
227
228    #[test]
229    fn test_new_view_wildcard_question_mark() {
230        let criteria = Instrument::new().name("test_?");
231        let mask = Stream::new();
232
233        let view = new_view(criteria, mask).expect("Expected to create a new view");
234
235        // Instrument name that should match the pattern "test_?".
236        let matching_instrument = Instrument::new().name("test_1");
237        assert!(
238            view.match_inst(&matching_instrument).is_some(),
239            "Expected to match instrument with test_? pattern"
240        );
241
242        // Instrument name that should not match the pattern "test_?".
243        let non_matching_instrument = Instrument::new().name("test_12");
244        assert!(
245            view.match_inst(&non_matching_instrument).is_none(),
246            "Expected not to match instrument with test_? pattern"
247        );
248    }
249}