scuffle_metrics/
lib.rs

1//! A wrapper around opentelemetry to provide a more ergonomic interface for
2//! creating metrics.
3//!
4//! This crate can be used together with the
5//! [`scuffle-bootstrap-telemetry`](../scuffle_bootstrap_telemetry) crate
6//! which provides a service that integrates with the
7//! [`scuffle-bootstrap`](../scuffle_bootstrap) ecosystem.
8//!
9//! ## Example
10//!
11//! ```rust
12//! #[scuffle_metrics::metrics]
13//! mod example {
14//!     use scuffle_metrics::{MetricEnum, collector::CounterU64};
15//!
16//!     #[derive(MetricEnum)]
17//!     pub enum Kind {
18//!         Http,
19//!         Grpc,
20//!     }
21//!
22//!     #[metrics(unit = "requests")]
23//!     pub fn request(kind: Kind) -> CounterU64;
24//! }
25//!
26//! // Increment the counter
27//! example::request(example::Kind::Http).incr();
28//! ```
29//!
30//! For details see [`metrics!`](metrics).
31//!
32//! ## Status
33//!
34//! This crate is currently under development and is not yet stable.
35//!
36//! Unit tests are not yet fully implemented. Use at your own risk.
37//!
38//! ## License
39//!
40//! This project is licensed under the [MIT](./LICENSE.MIT) or
41//! [Apache-2.0](./LICENSE.Apache-2.0) license. You can choose between one of
42//! them if you use this work.
43//!
44//! `SPDX-License-Identifier: MIT OR Apache-2.0`
45#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
46#![cfg_attr(docsrs, feature(doc_cfg))]
47
48/// A copy of the opentelemetry-prometheus crate, updated to work with the
49/// latest version of opentelemetry.
50#[cfg(feature = "prometheus")]
51#[cfg_attr(docsrs, doc(cfg(feature = "prometheus")))]
52pub mod prometheus;
53
54#[doc(hidden)]
55pub mod value;
56
57pub mod collector;
58
59pub use collector::{
60    CounterF64, CounterU64, GaugeF64, GaugeI64, GaugeU64, HistogramF64, HistogramU64, UpDownCounterF64, UpDownCounterI64,
61};
62pub use opentelemetry;
63pub use scuffle_metrics_derive::{metrics, MetricEnum};
64
65#[cfg(test)]
66#[cfg_attr(all(test, coverage_nightly), coverage(off))]
67mod tests {
68    use std::sync::Arc;
69
70    use opentelemetry::{Key, KeyValue, Value};
71    use opentelemetry_sdk::metrics::data::{ResourceMetrics, Sum};
72    use opentelemetry_sdk::metrics::reader::MetricReader;
73    use opentelemetry_sdk::metrics::{ManualReader, ManualReaderBuilder, SdkMeterProvider};
74    use opentelemetry_sdk::Resource;
75
76    #[test]
77    fn derive_enum() {
78        insta::assert_snapshot!(postcompile::compile! {
79            use scuffle_metrics::MetricEnum;
80
81            #[derive(MetricEnum)]
82            pub enum Kind {
83                Http,
84                Grpc,
85            }
86        });
87    }
88
89    #[test]
90    fn opentelemetry() {
91        #[derive(Debug, Clone)]
92        struct TestReader(Arc<ManualReader>);
93
94        impl TestReader {
95            fn new() -> Self {
96                Self(Arc::new(ManualReaderBuilder::new().build()))
97            }
98
99            fn read(&self) -> ResourceMetrics {
100                let mut metrics = ResourceMetrics {
101                    resource: Resource::builder_empty().build(),
102                    scope_metrics: vec![],
103                };
104
105                self.0.collect(&mut metrics).expect("collect");
106
107                metrics
108            }
109        }
110
111        impl opentelemetry_sdk::metrics::reader::MetricReader for TestReader {
112            fn register_pipeline(&self, pipeline: std::sync::Weak<opentelemetry_sdk::metrics::Pipeline>) {
113                self.0.register_pipeline(pipeline)
114            }
115
116            fn collect(
117                &self,
118                rm: &mut opentelemetry_sdk::metrics::data::ResourceMetrics,
119            ) -> opentelemetry_sdk::metrics::MetricResult<()> {
120                self.0.collect(rm)
121            }
122
123            fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
124                self.0.force_flush()
125            }
126
127            fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
128                self.0.shutdown()
129            }
130
131            fn temporality(
132                &self,
133                kind: opentelemetry_sdk::metrics::InstrumentKind,
134            ) -> opentelemetry_sdk::metrics::Temporality {
135                self.0.temporality(kind)
136            }
137        }
138
139        #[crate::metrics(crate_path = "crate")]
140        mod example {
141            use crate::{CounterU64, MetricEnum};
142
143            #[derive(MetricEnum)]
144            #[metrics(crate_path = "crate")]
145            pub enum Kind {
146                Http,
147                Grpc,
148            }
149
150            #[metrics(unit = "requests")]
151            pub fn request(kind: Kind) -> CounterU64;
152        }
153
154        let reader = TestReader::new();
155        let provider = SdkMeterProvider::builder()
156            .with_resource(
157                Resource::builder()
158                    .with_attribute(KeyValue::new("service.name", "test_service"))
159                    .build(),
160            )
161            .with_reader(reader.clone())
162            .build();
163        opentelemetry::global::set_meter_provider(provider);
164
165        let metrics = reader.read();
166
167        assert!(!metrics.resource.is_empty());
168        assert_eq!(
169            metrics.resource.get(&Key::from_static_str("service.name")),
170            Some(Value::from("test_service"))
171        );
172        assert_eq!(
173            metrics.resource.get(&Key::from_static_str("telemetry.sdk.name")),
174            Some(Value::from("opentelemetry"))
175        );
176        assert!(metrics.resource.get(&Key::from_static_str("telemetry.sdk.version")).is_some());
177        assert_eq!(
178            metrics.resource.get(&Key::from_static_str("telemetry.sdk.language")),
179            Some(Value::from("rust"))
180        );
181
182        assert!(metrics.scope_metrics.is_empty());
183
184        example::request(example::Kind::Http).incr();
185
186        let metrics = reader.read();
187
188        assert_eq!(metrics.scope_metrics.len(), 1);
189        assert_eq!(metrics.scope_metrics[0].scope.name(), "scuffle-metrics");
190        assert!(metrics.scope_metrics[0].scope.version().is_some());
191        assert_eq!(metrics.scope_metrics[0].metrics.len(), 1);
192        assert_eq!(metrics.scope_metrics[0].metrics[0].name, "example_request");
193        assert_eq!(metrics.scope_metrics[0].metrics[0].description, "");
194        assert_eq!(metrics.scope_metrics[0].metrics[0].unit, "requests");
195        let sum: &Sum<u64> = metrics.scope_metrics[0].metrics[0]
196            .data
197            .as_any()
198            .downcast_ref()
199            .expect("wrong data type");
200        assert_eq!(sum.temporality, opentelemetry_sdk::metrics::Temporality::Cumulative);
201        assert!(sum.is_monotonic);
202        assert_eq!(sum.data_points.len(), 1);
203        assert_eq!(sum.data_points[0].value, 1);
204        assert_eq!(sum.data_points[0].attributes.len(), 1);
205        assert_eq!(sum.data_points[0].attributes[0].key, Key::from_static_str("kind"));
206        assert_eq!(sum.data_points[0].attributes[0].value, Value::from("Http"));
207
208        example::request(example::Kind::Http).incr();
209
210        let metrics = reader.read();
211
212        assert_eq!(metrics.scope_metrics.len(), 1);
213        assert_eq!(metrics.scope_metrics[0].metrics.len(), 1);
214        let sum: &Sum<u64> = metrics.scope_metrics[0].metrics[0]
215            .data
216            .as_any()
217            .downcast_ref()
218            .expect("wrong data type");
219        assert_eq!(sum.data_points.len(), 1);
220        assert_eq!(sum.data_points[0].value, 2);
221        assert_eq!(sum.data_points[0].attributes.len(), 1);
222        assert_eq!(sum.data_points[0].attributes[0].key, Key::from_static_str("kind"));
223        assert_eq!(sum.data_points[0].attributes[0].value, Value::from("Http"));
224
225        example::request(example::Kind::Grpc).incr();
226
227        let metrics = reader.read();
228
229        assert_eq!(metrics.scope_metrics.len(), 1);
230        assert_eq!(metrics.scope_metrics[0].metrics.len(), 1);
231        let sum: &Sum<u64> = metrics.scope_metrics[0].metrics[0]
232            .data
233            .as_any()
234            .downcast_ref()
235            .expect("wrong data type");
236        assert_eq!(sum.data_points.len(), 2);
237        let grpc = sum
238            .data_points
239            .iter()
240            .find(|dp| {
241                dp.attributes.len() == 1
242                    && dp.attributes[0].key == Key::from_static_str("kind")
243                    && dp.attributes[0].value == Value::from("Grpc")
244            })
245            .expect("grpc data point not found");
246        assert_eq!(grpc.value, 1);
247    }
248}