opentelemetry_sdk/metrics/
view.rs

1use super::instrument::{Instrument, Stream};
2use glob::Pattern;
3use opentelemetry::{
4    global,
5    metrics::{MetricsError, Result},
6};
7
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/// ```
48pub trait View: Send + Sync + 'static {
49    /// Defines how data should be collected for certain instruments.
50    ///
51    /// Return [Stream] to use for matching [Instrument]s,
52    /// otherwise if there is no match, return `None`.
53    fn match_inst(&self, inst: &Instrument) -> Option<Stream>;
54}
55
56impl<T> View for T
57where
58    T: Fn(&Instrument) -> Option<Stream> + Send + Sync + 'static,
59{
60    fn match_inst(&self, inst: &Instrument) -> Option<Stream> {
61        self(inst)
62    }
63}
64
65impl View for Box<dyn View> {
66    fn match_inst(&self, inst: &Instrument) -> Option<Stream> {
67        (**self).match_inst(inst)
68    }
69}
70
71/// Creates a [View] that applies the [Stream] mask for all instruments that
72/// match criteria.
73///
74/// The returned [View] will only apply the mask if all non-empty fields of
75/// criteria match the corresponding [Instrument] passed to the view. If all
76/// fields of the criteria are their default values, a view that matches no
77/// instruments is returned. If you need to match an empty-value field, create a
78/// [View] directly.
79///
80/// The [Instrument::name] field of criteria supports wildcard pattern matching.
81/// The wildcard `*` is recognized as matching zero or more characters, and `?`
82/// is recognized as matching exactly one character. For example, a pattern of
83/// `*` will match all instrument names.
84///
85/// The [Stream] mask only applies updates for non-empty fields. By default, the
86/// [Instrument] the [View] matches against will be use for the name,
87/// description, and unit of the returned [Stream] and no `aggregation` or
88/// `allowed_attribute_keys` are set. All non-empty fields of mask are used
89/// instead of the default. If you need to set a an empty value in the returned
90/// stream, create a custom [View] directly.
91///
92/// # Example
93///
94/// ```
95/// use opentelemetry_sdk::metrics::{new_view, Aggregation, Instrument, Stream};
96///
97/// let criteria = Instrument::new().name("counter_*");
98/// let mask = Stream::new().aggregation(Aggregation::Sum);
99///
100/// let view = new_view(criteria, mask);
101/// # drop(view);
102/// ```
103pub fn new_view(criteria: Instrument, mask: Stream) -> Result<Box<dyn View>> {
104    if criteria.is_empty() {
105        global::handle_error(MetricsError::Config(format!(
106            "no criteria provided, dropping view. mask: {mask:?}"
107        )));
108        return Ok(Box::new(empty_view));
109    }
110    let contains_wildcard = criteria.name.contains(|c| c == '*' || c == '?');
111    let err_msg_criteria = criteria.clone();
112
113    let match_fn: Box<dyn Fn(&Instrument) -> bool + Send + Sync> = if contains_wildcard {
114        if mask.name != "" {
115            global::handle_error(MetricsError::Config(format!(
116				"name replacement for multiple instruments, dropping view, criteria: {criteria:?}, mask: {mask:?}"
117			)));
118            return Ok(Box::new(empty_view));
119        }
120
121        let pattern = criteria.name.clone();
122        let glob_pattern =
123            Pattern::new(&pattern).map_err(|e| MetricsError::Config(e.to_string()))?;
124
125        Box::new(move |i| {
126            glob_pattern.matches(&i.name)
127                && criteria.matches_description(i)
128                && criteria.matches_kind(i)
129                && criteria.matches_unit(i)
130                && criteria.matches_scope(i)
131        })
132    } else {
133        Box::new(move |i| criteria.matches(i))
134    };
135
136    let mut agg = None;
137    if let Some(ma) = &mask.aggregation {
138        match ma.validate() {
139            Ok(_) => agg = Some(ma.clone()),
140            Err(err) => {
141                global::handle_error(MetricsError::Other(format!(
142                    "{}, proceeding as if view did not exist. criteria: {:?}, mask: {:?}",
143                    err, err_msg_criteria, mask
144                )));
145                return Ok(Box::new(empty_view));
146            }
147        }
148    }
149
150    Ok(Box::new(move |i: &Instrument| -> Option<Stream> {
151        if match_fn(i) {
152            Some(Stream {
153                name: if !mask.name.is_empty() {
154                    mask.name.clone()
155                } else {
156                    i.name.clone()
157                },
158                description: if !mask.description.is_empty() {
159                    mask.description.clone()
160                } else {
161                    i.description.clone()
162                },
163                unit: if !mask.unit.is_empty() {
164                    mask.unit.clone()
165                } else {
166                    i.unit.clone()
167                },
168                aggregation: agg.clone(),
169                allowed_attribute_keys: mask.allowed_attribute_keys.clone(),
170            })
171        } else {
172            None
173        }
174    }))
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    #[test]
181    fn test_new_view_matching_all() {
182        let criteria = Instrument::new().name("*");
183        let mask = Stream::new();
184
185        let view = new_view(criteria, mask).expect("Expected to create a new view");
186
187        let test_instrument = Instrument::new().name("test_instrument");
188        assert!(
189            view.match_inst(&test_instrument).is_some(),
190            "Expected to match all instruments with * pattern"
191        );
192    }
193
194    #[test]
195    fn test_new_view_exact_match() {
196        let criteria = Instrument::new().name("counter_exact_match");
197        let mask = Stream::new();
198
199        let view = new_view(criteria, mask).expect("Expected to create a new view");
200
201        let matching_instrument = Instrument::new().name("counter_exact_match");
202        assert!(
203            view.match_inst(&matching_instrument).is_some(),
204            "Expected to match instrument with exact name"
205        );
206
207        let non_matching_instrument = Instrument::new().name("counter_non_exact_match");
208        assert!(
209            view.match_inst(&non_matching_instrument).is_none(),
210            "Expected not to match instrument with different name"
211        );
212    }
213
214    #[test]
215    fn test_new_view_with_wildcard_pattern() {
216        let criteria = Instrument::new().name("prefix_*");
217        let mask = Stream::new();
218
219        let view = new_view(criteria, mask).expect("Expected to create a new view");
220
221        let matching_instrument = Instrument::new().name("prefix_counter");
222        assert!(
223            view.match_inst(&matching_instrument).is_some(),
224            "Expected to match instrument with matching prefix"
225        );
226
227        let non_matching_instrument = Instrument::new().name("nonprefix_counter");
228        assert!(
229            view.match_inst(&non_matching_instrument).is_none(),
230            "Expected not to match instrument with different prefix"
231        );
232    }
233
234    #[test]
235    fn test_new_view_wildcard_question_mark() {
236        let criteria = Instrument::new().name("test_?");
237        let mask = Stream::new();
238
239        let view = new_view(criteria, mask).expect("Expected to create a new view");
240
241        // Instrument name that should match the pattern "test_?".
242        let matching_instrument = Instrument::new().name("test_1");
243        assert!(
244            view.match_inst(&matching_instrument).is_some(),
245            "Expected to match instrument with test_? pattern"
246        );
247
248        // Instrument name that should not match the pattern "test_?".
249        let non_matching_instrument = Instrument::new().name("test_12");
250        assert!(
251            view.match_inst(&non_matching_instrument).is_none(),
252            "Expected not to match instrument with test_? pattern"
253        );
254    }
255}