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#![deny(missing_docs)]
48#![deny(unreachable_pub)]
49#![deny(clippy::undocumented_unsafe_blocks)]
50#![deny(clippy::multiple_unsafe_ops_per_block)]
51
52/// A copy of the opentelemetry-prometheus crate, updated to work with the
53/// latest version of opentelemetry.
54#[cfg(feature = "prometheus")]
55#[cfg_attr(docsrs, doc(cfg(feature = "prometheus")))]
56pub mod prometheus;
57
58#[doc(hidden)]
59pub mod value;
60
61pub mod collector;
62
63pub use collector::{
64    CounterF64, CounterU64, GaugeF64, GaugeI64, GaugeU64, HistogramF64, HistogramU64, UpDownCounterF64, UpDownCounterI64,
65};
66pub use opentelemetry;
67pub use scuffle_metrics_derive::{MetricEnum, metrics};
68
69#[cfg(test)]
70#[cfg_attr(all(test, coverage_nightly), coverage(off))]
71mod tests {
72    use std::sync::Arc;
73
74    use opentelemetry::{Key, KeyValue, Value};
75    use opentelemetry_sdk::Resource;
76    use opentelemetry_sdk::metrics::data::{ResourceMetrics, Sum};
77    use opentelemetry_sdk::metrics::reader::MetricReader;
78    use opentelemetry_sdk::metrics::{ManualReader, ManualReaderBuilder, SdkMeterProvider};
79
80    #[test]
81    fn derive_enum() {
82        insta::assert_snapshot!(postcompile::compile! {
83            use scuffle_metrics::MetricEnum;
84
85            #[derive(MetricEnum)]
86            pub enum Kind {
87                Http,
88                Grpc,
89            }
90        });
91    }
92
93    #[test]
94    fn opentelemetry() {
95        #[derive(Debug, Clone)]
96        struct TestReader(Arc<ManualReader>);
97
98        impl TestReader {
99            fn new() -> Self {
100                Self(Arc::new(ManualReaderBuilder::new().build()))
101            }
102
103            fn read(&self) -> ResourceMetrics {
104                let mut metrics = ResourceMetrics {
105                    resource: Resource::builder_empty().build(),
106                    scope_metrics: vec![],
107                };
108
109                self.0.collect(&mut metrics).expect("collect");
110
111                metrics
112            }
113        }
114
115        impl opentelemetry_sdk::metrics::reader::MetricReader for TestReader {
116            fn register_pipeline(&self, pipeline: std::sync::Weak<opentelemetry_sdk::metrics::Pipeline>) {
117                self.0.register_pipeline(pipeline)
118            }
119
120            fn collect(
121                &self,
122                rm: &mut opentelemetry_sdk::metrics::data::ResourceMetrics,
123            ) -> opentelemetry_sdk::metrics::MetricResult<()> {
124                self.0.collect(rm)
125            }
126
127            fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
128                self.0.force_flush()
129            }
130
131            fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
132                self.0.shutdown()
133            }
134
135            fn temporality(
136                &self,
137                kind: opentelemetry_sdk::metrics::InstrumentKind,
138            ) -> opentelemetry_sdk::metrics::Temporality {
139                self.0.temporality(kind)
140            }
141        }
142
143        #[crate::metrics(crate_path = "crate")]
144        mod example {
145            use crate::{CounterU64, MetricEnum};
146
147            #[derive(MetricEnum)]
148            #[metrics(crate_path = "crate")]
149            pub enum Kind {
150                Http,
151                Grpc,
152            }
153
154            #[metrics(unit = "requests")]
155            pub fn request(kind: Kind) -> CounterU64;
156        }
157
158        let reader = TestReader::new();
159        let provider = SdkMeterProvider::builder()
160            .with_resource(
161                Resource::builder()
162                    .with_attribute(KeyValue::new("service.name", "test_service"))
163                    .build(),
164            )
165            .with_reader(reader.clone())
166            .build();
167        opentelemetry::global::set_meter_provider(provider);
168
169        let metrics = reader.read();
170
171        assert!(!metrics.resource.is_empty());
172        assert_eq!(
173            metrics.resource.get(&Key::from_static_str("service.name")),
174            Some(Value::from("test_service"))
175        );
176        assert_eq!(
177            metrics.resource.get(&Key::from_static_str("telemetry.sdk.name")),
178            Some(Value::from("opentelemetry"))
179        );
180        assert!(metrics.resource.get(&Key::from_static_str("telemetry.sdk.version")).is_some());
181        assert_eq!(
182            metrics.resource.get(&Key::from_static_str("telemetry.sdk.language")),
183            Some(Value::from("rust"))
184        );
185
186        assert!(metrics.scope_metrics.is_empty());
187
188        example::request(example::Kind::Http).incr();
189
190        let metrics = reader.read();
191
192        assert_eq!(metrics.scope_metrics.len(), 1);
193        assert_eq!(metrics.scope_metrics[0].scope.name(), "scuffle-metrics");
194        assert!(metrics.scope_metrics[0].scope.version().is_some());
195        assert_eq!(metrics.scope_metrics[0].metrics.len(), 1);
196        assert_eq!(metrics.scope_metrics[0].metrics[0].name, "example_request");
197        assert_eq!(metrics.scope_metrics[0].metrics[0].description, "");
198        assert_eq!(metrics.scope_metrics[0].metrics[0].unit, "requests");
199        let sum: &Sum<u64> = metrics.scope_metrics[0].metrics[0]
200            .data
201            .as_any()
202            .downcast_ref()
203            .expect("wrong data type");
204        assert_eq!(sum.temporality, opentelemetry_sdk::metrics::Temporality::Cumulative);
205        assert!(sum.is_monotonic);
206        assert_eq!(sum.data_points.len(), 1);
207        assert_eq!(sum.data_points[0].value, 1);
208        assert_eq!(sum.data_points[0].attributes.len(), 1);
209        assert_eq!(sum.data_points[0].attributes[0].key, Key::from_static_str("kind"));
210        assert_eq!(sum.data_points[0].attributes[0].value, Value::from("Http"));
211
212        example::request(example::Kind::Http).incr();
213
214        let metrics = reader.read();
215
216        assert_eq!(metrics.scope_metrics.len(), 1);
217        assert_eq!(metrics.scope_metrics[0].metrics.len(), 1);
218        let sum: &Sum<u64> = metrics.scope_metrics[0].metrics[0]
219            .data
220            .as_any()
221            .downcast_ref()
222            .expect("wrong data type");
223        assert_eq!(sum.data_points.len(), 1);
224        assert_eq!(sum.data_points[0].value, 2);
225        assert_eq!(sum.data_points[0].attributes.len(), 1);
226        assert_eq!(sum.data_points[0].attributes[0].key, Key::from_static_str("kind"));
227        assert_eq!(sum.data_points[0].attributes[0].value, Value::from("Http"));
228
229        example::request(example::Kind::Grpc).incr();
230
231        let metrics = reader.read();
232
233        assert_eq!(metrics.scope_metrics.len(), 1);
234        assert_eq!(metrics.scope_metrics[0].metrics.len(), 1);
235        let sum: &Sum<u64> = metrics.scope_metrics[0].metrics[0]
236            .data
237            .as_any()
238            .downcast_ref()
239            .expect("wrong data type");
240        assert_eq!(sum.data_points.len(), 2);
241        let grpc = sum
242            .data_points
243            .iter()
244            .find(|dp| {
245                dp.attributes.len() == 1
246                    && dp.attributes[0].key == Key::from_static_str("kind")
247                    && dp.attributes[0].value == Value::from("Grpc")
248            })
249            .expect("grpc data point not found");
250        assert_eq!(grpc.value, 1);
251    }
252}