1#![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#![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#[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#[cfg(feature = "docs")]
252#[scuffle_changelog::changelog]
253pub mod changelog {}