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 [`scuffle-bootstrap-telemetry`](https://docs.rs/scuffle-bootstrap-telemetry) crate
5//! which provides a service that integrates with the [`scuffle-bootstrap`](https://docs.rs/scuffle-bootstrap) ecosystem.
6#![cfg_attr(feature = "docs", doc = "\n\nSee the [changelog][changelog] for a full release history.")]
7#![cfg_attr(feature = "docs", doc = "## Feature flags")]
8#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
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//! ## License
33//!
34//! This project is licensed under the MIT or Apache-2.0 license.
35//! You can choose between one of them if you use this work.
36//!
37//! `SPDX-License-Identifier: MIT OR Apache-2.0`
38#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
39#![cfg_attr(docsrs, feature(doc_auto_cfg))]
40#![deny(missing_docs)]
41#![deny(unreachable_pub)]
42#![deny(clippy::mod_module_files)]
43#![deny(clippy::undocumented_unsafe_blocks)]
44#![deny(clippy::multiple_unsafe_ops_per_block)]
45
46/// A copy of the opentelemetry-prometheus crate, updated to work with the
47/// latest version of opentelemetry.
48#[cfg(feature = "prometheus")]
49pub mod prometheus;
50
51#[doc(hidden)]
52pub mod value;
53
54pub mod collector;
55
56pub use collector::{
57    CounterF64, CounterU64, GaugeF64, GaugeI64, GaugeU64, HistogramF64, HistogramU64, UpDownCounterF64, UpDownCounterI64,
58};
59pub use opentelemetry;
60pub use scuffle_metrics_derive::{MetricEnum, metrics};
61
62#[cfg(test)]
63#[cfg_attr(all(test, coverage_nightly), coverage(off))]
64mod tests {
65    use std::sync::Arc;
66
67    use opentelemetry::{Key, KeyValue, Value};
68    use opentelemetry_sdk::Resource;
69    use opentelemetry_sdk::metrics::data::{AggregatedMetrics, MetricData, ResourceMetrics};
70    use opentelemetry_sdk::metrics::reader::MetricReader;
71    use opentelemetry_sdk::metrics::{ManualReader, ManualReaderBuilder, SdkMeterProvider};
72
73    #[test]
74    fn derive_enum() {
75        insta::assert_snapshot!(postcompile::compile!({
76            #[derive(scuffle_metrics::MetricEnum)]
77            pub enum Kind {
78                Http,
79                Grpc,
80            }
81        }));
82    }
83
84    #[test]
85    fn opentelemetry() {
86        #[derive(Debug, Clone)]
87        struct TestReader(Arc<ManualReader>);
88
89        impl TestReader {
90            fn new() -> Self {
91                Self(Arc::new(ManualReaderBuilder::new().build()))
92            }
93
94            fn read(&self) -> ResourceMetrics {
95                let mut metrics = ResourceMetrics::default();
96
97                self.0.collect(&mut metrics).expect("collect");
98
99                metrics
100            }
101        }
102
103        impl opentelemetry_sdk::metrics::reader::MetricReader for TestReader {
104            fn register_pipeline(&self, pipeline: std::sync::Weak<opentelemetry_sdk::metrics::Pipeline>) {
105                self.0.register_pipeline(pipeline)
106            }
107
108            fn collect(
109                &self,
110                rm: &mut opentelemetry_sdk::metrics::data::ResourceMetrics,
111            ) -> opentelemetry_sdk::error::OTelSdkResult {
112                self.0.collect(rm)
113            }
114
115            fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
116                self.0.force_flush()
117            }
118
119            fn shutdown_with_timeout(&self, timeout: std::time::Duration) -> opentelemetry_sdk::error::OTelSdkResult {
120                self.0.shutdown_with_timeout(timeout)
121            }
122
123            fn temporality(
124                &self,
125                kind: opentelemetry_sdk::metrics::InstrumentKind,
126            ) -> opentelemetry_sdk::metrics::Temporality {
127                self.0.temporality(kind)
128            }
129        }
130
131        #[crate::metrics(crate_path = "crate")]
132        mod example {
133            use crate::{CounterU64, MetricEnum};
134
135            #[derive(MetricEnum)]
136            #[metrics(crate_path = "crate")]
137            pub enum Kind {
138                Http,
139                Grpc,
140            }
141
142            #[metrics(unit = "requests")]
143            pub fn request(kind: Kind) -> CounterU64;
144        }
145
146        let reader = TestReader::new();
147        let provider = SdkMeterProvider::builder()
148            .with_resource(
149                Resource::builder()
150                    .with_attribute(KeyValue::new("service.name", "test_service"))
151                    .build(),
152            )
153            .with_reader(reader.clone())
154            .build();
155        opentelemetry::global::set_meter_provider(provider);
156
157        let metrics = reader.read();
158
159        assert!(!metrics.resource().is_empty());
160        assert_eq!(
161            metrics.resource().get(&Key::from_static_str("service.name")),
162            Some(Value::from("test_service"))
163        );
164        assert_eq!(
165            metrics.resource().get(&Key::from_static_str("telemetry.sdk.name")),
166            Some(Value::from("opentelemetry"))
167        );
168        assert!(
169            metrics
170                .resource()
171                .get(&Key::from_static_str("telemetry.sdk.version"))
172                .is_some()
173        );
174        assert_eq!(
175            metrics.resource().get(&Key::from_static_str("telemetry.sdk.language")),
176            Some(Value::from("rust"))
177        );
178
179        assert!(metrics.scope_metrics().next().is_none());
180
181        example::request(example::Kind::Http).incr();
182
183        let metrics = reader.read();
184
185        assert_eq!(metrics.scope_metrics().count(), 1);
186        let scoped_metric = metrics.scope_metrics().next().unwrap();
187        assert_eq!(scoped_metric.scope().name(), "scuffle-metrics");
188        assert!(scoped_metric.scope().version().is_some());
189        assert_eq!(scoped_metric.metrics().count(), 1);
190        let scoped_metric_metric = scoped_metric.metrics().next().unwrap();
191        assert_eq!(scoped_metric_metric.name(), "example_request");
192        assert_eq!(scoped_metric_metric.description(), "");
193        assert_eq!(scoped_metric_metric.unit(), "requests");
194        let AggregatedMetrics::U64(MetricData::Sum(sum)) = scoped_metric_metric.data() else {
195            unreachable!()
196        };
197        assert_eq!(sum.temporality(), opentelemetry_sdk::metrics::Temporality::Cumulative);
198        assert!(sum.is_monotonic());
199        assert_eq!(sum.data_points().count(), 1);
200        let data_point = sum.data_points().next().unwrap();
201        assert_eq!(data_point.value(), 1);
202        assert_eq!(data_point.attributes().count(), 1);
203        let attribute = data_point.attributes().next().unwrap();
204        assert_eq!(attribute.key, Key::from_static_str("kind"));
205        assert_eq!(attribute.value, Value::from("Http"));
206
207        example::request(example::Kind::Http).incr();
208
209        let metrics = reader.read();
210
211        assert_eq!(metrics.scope_metrics().count(), 1);
212        let scope_metric = metrics.scope_metrics().next().unwrap();
213        assert_eq!(scope_metric.metrics().count(), 1);
214        let scope_metric_metric = scope_metric.metrics().next().unwrap();
215        let AggregatedMetrics::U64(MetricData::Sum(sum)) = scope_metric_metric.data() else {
216            unreachable!()
217        };
218        assert_eq!(sum.data_points().count(), 1);
219        let data_point = sum.data_points().next().unwrap();
220        assert_eq!(data_point.value(), 2);
221        assert_eq!(data_point.attributes().count(), 1);
222        let attribute = data_point.attributes().next().unwrap();
223        assert_eq!(attribute.key, Key::from_static_str("kind"));
224        assert_eq!(attribute.value, Value::from("Http"));
225
226        example::request(example::Kind::Grpc).incr();
227
228        let metrics = reader.read();
229
230        assert_eq!(metrics.scope_metrics().count(), 1);
231        let scope_metric = metrics.scope_metrics().next().unwrap();
232        assert_eq!(scope_metric.metrics().count(), 1);
233        let scope_metric_metric = scope_metric.metrics().next().unwrap();
234        let AggregatedMetrics::U64(MetricData::Sum(sum)) = scope_metric_metric.data() else {
235            unreachable!()
236        };
237        assert_eq!(sum.data_points().count(), 2);
238        let grpc = sum
239            .data_points()
240            .find(|dp| {
241                dp.attributes().count() == 1
242                    && dp.attributes().next().unwrap().key == Key::from_static_str("kind")
243                    && dp.attributes().next().unwrap().value == Value::from("Grpc")
244            })
245            .expect("grpc data point not found");
246        assert_eq!(grpc.value(), 1);
247    }
248}
249
250/// Changelogs generated by [scuffle_changelog]
251#[cfg(feature = "docs")]
252#[scuffle_changelog::changelog]
253pub mod changelog {}