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::undocumented_unsafe_blocks)]
43#![deny(clippy::multiple_unsafe_ops_per_block)]
44
45#[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#[cfg(feature = "docs")]
251#[scuffle_changelog::changelog]
252pub mod changelog {}