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