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::{ResourceMetrics, Sum};
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 {
95                    resource: Resource::builder_empty().build(),
96                    scope_metrics: vec![],
97                };
98
99                self.0.collect(&mut metrics).expect("collect");
100
101                metrics
102            }
103        }
104
105        impl opentelemetry_sdk::metrics::reader::MetricReader for TestReader {
106            fn register_pipeline(&self, pipeline: std::sync::Weak<opentelemetry_sdk::metrics::Pipeline>) {
107                self.0.register_pipeline(pipeline)
108            }
109
110            fn collect(
111                &self,
112                rm: &mut opentelemetry_sdk::metrics::data::ResourceMetrics,
113            ) -> opentelemetry_sdk::metrics::MetricResult<()> {
114                self.0.collect(rm)
115            }
116
117            fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
118                self.0.force_flush()
119            }
120
121            fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
122                self.0.shutdown()
123            }
124
125            fn temporality(
126                &self,
127                kind: opentelemetry_sdk::metrics::InstrumentKind,
128            ) -> opentelemetry_sdk::metrics::Temporality {
129                self.0.temporality(kind)
130            }
131        }
132
133        #[crate::metrics(crate_path = "crate")]
134        mod example {
135            use crate::{CounterU64, MetricEnum};
136
137            #[derive(MetricEnum)]
138            #[metrics(crate_path = "crate")]
139            pub enum Kind {
140                Http,
141                Grpc,
142            }
143
144            #[metrics(unit = "requests")]
145            pub fn request(kind: Kind) -> CounterU64;
146        }
147
148        let reader = TestReader::new();
149        let provider = SdkMeterProvider::builder()
150            .with_resource(
151                Resource::builder()
152                    .with_attribute(KeyValue::new("service.name", "test_service"))
153                    .build(),
154            )
155            .with_reader(reader.clone())
156            .build();
157        opentelemetry::global::set_meter_provider(provider);
158
159        let metrics = reader.read();
160
161        assert!(!metrics.resource.is_empty());
162        assert_eq!(
163            metrics.resource.get(&Key::from_static_str("service.name")),
164            Some(Value::from("test_service"))
165        );
166        assert_eq!(
167            metrics.resource.get(&Key::from_static_str("telemetry.sdk.name")),
168            Some(Value::from("opentelemetry"))
169        );
170        assert!(metrics.resource.get(&Key::from_static_str("telemetry.sdk.version")).is_some());
171        assert_eq!(
172            metrics.resource.get(&Key::from_static_str("telemetry.sdk.language")),
173            Some(Value::from("rust"))
174        );
175
176        assert!(metrics.scope_metrics.is_empty());
177
178        example::request(example::Kind::Http).incr();
179
180        let metrics = reader.read();
181
182        assert_eq!(metrics.scope_metrics.len(), 1);
183        assert_eq!(metrics.scope_metrics[0].scope.name(), "scuffle-metrics");
184        assert!(metrics.scope_metrics[0].scope.version().is_some());
185        assert_eq!(metrics.scope_metrics[0].metrics.len(), 1);
186        assert_eq!(metrics.scope_metrics[0].metrics[0].name, "example_request");
187        assert_eq!(metrics.scope_metrics[0].metrics[0].description, "");
188        assert_eq!(metrics.scope_metrics[0].metrics[0].unit, "requests");
189        let sum: &Sum<u64> = metrics.scope_metrics[0].metrics[0]
190            .data
191            .as_any()
192            .downcast_ref()
193            .expect("wrong data type");
194        assert_eq!(sum.temporality, opentelemetry_sdk::metrics::Temporality::Cumulative);
195        assert!(sum.is_monotonic);
196        assert_eq!(sum.data_points.len(), 1);
197        assert_eq!(sum.data_points[0].value, 1);
198        assert_eq!(sum.data_points[0].attributes.len(), 1);
199        assert_eq!(sum.data_points[0].attributes[0].key, Key::from_static_str("kind"));
200        assert_eq!(sum.data_points[0].attributes[0].value, Value::from("Http"));
201
202        example::request(example::Kind::Http).incr();
203
204        let metrics = reader.read();
205
206        assert_eq!(metrics.scope_metrics.len(), 1);
207        assert_eq!(metrics.scope_metrics[0].metrics.len(), 1);
208        let sum: &Sum<u64> = metrics.scope_metrics[0].metrics[0]
209            .data
210            .as_any()
211            .downcast_ref()
212            .expect("wrong data type");
213        assert_eq!(sum.data_points.len(), 1);
214        assert_eq!(sum.data_points[0].value, 2);
215        assert_eq!(sum.data_points[0].attributes.len(), 1);
216        assert_eq!(sum.data_points[0].attributes[0].key, Key::from_static_str("kind"));
217        assert_eq!(sum.data_points[0].attributes[0].value, Value::from("Http"));
218
219        example::request(example::Kind::Grpc).incr();
220
221        let metrics = reader.read();
222
223        assert_eq!(metrics.scope_metrics.len(), 1);
224        assert_eq!(metrics.scope_metrics[0].metrics.len(), 1);
225        let sum: &Sum<u64> = metrics.scope_metrics[0].metrics[0]
226            .data
227            .as_any()
228            .downcast_ref()
229            .expect("wrong data type");
230        assert_eq!(sum.data_points.len(), 2);
231        let grpc = sum
232            .data_points
233            .iter()
234            .find(|dp| {
235                dp.attributes.len() == 1
236                    && dp.attributes[0].key == Key::from_static_str("kind")
237                    && dp.attributes[0].value == Value::from("Grpc")
238            })
239            .expect("grpc data point not found");
240        assert_eq!(grpc.value, 1);
241    }
242}
243
244/// Changelogs generated by [scuffle_changelog]
245#[cfg(feature = "docs")]
246#[scuffle_changelog::changelog]
247pub mod changelog {}