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