1use crate::{MetricDescriptor, MetricError, MetricHandle, MetricKind, MetricResult};
4use opentelemetry::metrics::{Meter, MeterProvider};
5use opentelemetry_sdk::{Resource, metrics::SdkMeterProvider};
6use parking_lot::Mutex;
7use std::{
8 collections::{BTreeMap, BTreeSet},
9 sync::{
10 Arc,
11 atomic::{AtomicBool, AtomicUsize, Ordering},
12 },
13 time::Duration,
14};
15use tokio::sync::Semaphore;
16
17#[derive(Clone, Debug)]
19pub struct MetricsLimits {
20 pub max_instruments: usize,
21 pub max_series_per_instrument: usize,
22 pub max_label_bytes: usize,
23 pub max_total_series: usize,
24 pub max_exposition_bytes: usize,
25}
26
27impl Default for MetricsLimits {
28 fn default() -> Self {
29 Self {
30 max_instruments: 512,
31 max_series_per_instrument: 4096,
32 max_label_bytes: 256,
33 max_total_series: 16384,
34 max_exposition_bytes: 16 * 1024 * 1024,
35 }
36 }
37}
38
39#[derive(Clone, Debug, Default)]
41pub struct MetricsResource {
42 pub service_name: String,
43 pub service_version: String,
44 pub environment: String,
45 pub role: String,
46 pub instance_id: String,
47}
48
49pub struct MetricsProviderBuilder {
51 pub(crate) sdk: opentelemetry_sdk::metrics::MeterProviderBuilder,
52 limits: MetricsLimits,
53 pub(crate) enabled: bool,
54 #[cfg(feature = "backend-prometheus")]
55 registry: Option<prometheus::Registry>,
56 #[cfg(feature = "otlp")]
57 pub(crate) otlp_diagnostics: Option<crate::OtlpExportDiagnostics>,
58}
59
60impl MetricsProviderBuilder {
61 #[must_use]
62 pub fn with_limits(mut self, limits: MetricsLimits) -> Self {
63 self.limits = limits;
64 self
65 }
66
67 pub fn with_resource(mut self, resource: MetricsResource) -> MetricResult<Self> {
71 if resource.service_name.is_empty() {
72 return Err(MetricError::InvalidConfiguration);
73 }
74 let mut attributes = Vec::new();
75 for (key, value) in [
76 ("service.name", resource.service_name),
77 ("service.version", resource.service_version),
78 ("deployment.environment.name", resource.environment),
79 ("aster.role", resource.role),
80 ("service.instance.id", resource.instance_id),
81 ] {
82 if value.len() > 128 || value.chars().any(char::is_control) {
83 return Err(MetricError::InvalidConfiguration);
84 }
85 if !value.is_empty() {
86 attributes.push(opentelemetry::KeyValue::new(key, value));
87 }
88 }
89 self.sdk = self.sdk.with_resource(
90 Resource::builder_empty()
91 .with_attributes(attributes)
92 .build(),
93 );
94 Ok(self)
95 }
96
97 #[cfg(feature = "backend-prometheus")]
98 pub fn with_prometheus(mut self) -> MetricResult<Self> {
101 if self.registry.is_some() {
102 return Err(MetricError::InvalidConfiguration);
103 }
104 let registry = prometheus::Registry::new();
105 let reader = opentelemetry_prometheus::exporter()
106 .with_registry(registry.clone())
107 .without_units()
108 .without_counter_suffixes()
109 .without_target_info()
110 .with_resource_selector(opentelemetry_prometheus::ResourceSelector::All)
111 .scope_info_enabled(false)
112 .build()
113 .map_err(|_| MetricError::Initialization)?;
114 self.sdk = self.sdk.with_reader(reader);
115 self.registry = Some(registry);
116 self.enabled = true;
117 Ok(self)
118 }
119
120 #[must_use]
122 pub fn with_reader<R: opentelemetry_sdk::metrics::reader::MetricReader>(
123 mut self,
124 reader: R,
125 ) -> Self {
126 self.sdk = self.sdk.with_reader(reader);
127 self.enabled = true;
128 self
129 }
130
131 pub fn build(self) -> MetricResult<MetricsProvider> {
134 if self.limits.max_instruments == 0
135 || self.limits.max_series_per_instrument == 0
136 || self.limits.max_label_bytes == 0
137 || self.limits.max_total_series == 0
138 || self.limits.max_total_series > 1_048_576
139 || self.limits.max_exposition_bytes == 0
140 || self.limits.max_instruments > 4096
141 || self.limits.max_series_per_instrument > 65536
142 || self.limits.max_label_bytes > 4096
143 || self.limits.max_exposition_bytes > 64 * 1024 * 1024
144 {
145 return Err(MetricError::InvalidConfiguration);
146 }
147 let limit = self.limits.max_series_per_instrument;
148 let sdk = self
149 .sdk
150 .with_view(move |_| {
151 opentelemetry_sdk::metrics::Stream::builder()
152 .with_cardinality_limit(limit)
153 .build()
154 .ok()
155 })
156 .build();
157 let meter = sdk.meter("aster_forge");
158 Ok(MetricsProvider {
159 inner: Arc::new(ProviderInner {
160 sdk,
161 meter,
162 enabled: self.enabled,
163 closed: Arc::new(AtomicBool::new(false)),
164 limits: self.limits,
165 descriptors: Mutex::new(BTreeMap::new()),
166 series_count: Arc::new(AtomicUsize::new(0)),
167 #[cfg(feature = "backend-prometheus")]
168 registry: self.registry,
169 #[cfg(feature = "otlp")]
170 otlp_diagnostics: self.otlp_diagnostics,
171 #[cfg(feature = "backend-prometheus")]
172 gather_limit: Arc::new(Semaphore::new(2)),
173 lifecycle_limit: Arc::new(Semaphore::new(1)),
174 shutdown_result: Arc::new(Mutex::new(None)),
175 }),
176 })
177 }
178}
179
180struct ProviderInner {
181 sdk: SdkMeterProvider,
182 meter: Meter,
183 enabled: bool,
184 closed: Arc<AtomicBool>,
185 limits: MetricsLimits,
186 descriptors: Mutex<BTreeMap<String, MetricDescriptor>>,
187 series_count: Arc<AtomicUsize>,
188 #[cfg(feature = "backend-prometheus")]
189 registry: Option<prometheus::Registry>,
190 #[cfg(feature = "otlp")]
191 otlp_diagnostics: Option<crate::OtlpExportDiagnostics>,
192 #[cfg(feature = "backend-prometheus")]
193 gather_limit: Arc<Semaphore>,
194 lifecycle_limit: Arc<Semaphore>,
195 shutdown_result: Arc<Mutex<Option<MetricResult<()>>>>,
196}
197
198#[derive(Clone)]
200pub struct MetricsProvider {
201 inner: Arc<ProviderInner>,
202}
203
204impl MetricsProvider {
205 #[must_use]
206 pub fn builder() -> MetricsProviderBuilder {
207 MetricsProviderBuilder {
208 sdk: SdkMeterProvider::builder().with_resource(Resource::builder_empty().build()),
209 limits: MetricsLimits::default(),
210 enabled: false,
211 #[cfg(feature = "backend-prometheus")]
212 registry: None,
213 #[cfg(feature = "otlp")]
214 otlp_diagnostics: None,
215 }
216 }
217
218 #[must_use]
219 pub fn enabled(&self) -> bool {
220 self.inner.enabled && !self.inner.closed.load(Ordering::Acquire)
221 }
222
223 #[cfg(feature = "otlp")]
225 #[must_use]
226 pub fn otlp_diagnostics(&self) -> Option<crate::OtlpExportDiagnostics> {
227 self.inner.otlp_diagnostics.clone()
228 }
229
230 pub fn register_batch(
235 &self,
236 descriptors: &[MetricDescriptor],
237 ) -> MetricResult<Vec<MetricHandle>> {
238 if self.inner.closed.load(Ordering::Acquire) {
239 return Err(MetricError::Closed);
240 }
241 let mut catalog = self.inner.descriptors.lock();
242 if catalog.len().saturating_add(descriptors.len()) > self.inner.limits.max_instruments {
243 return Err(MetricError::InstrumentLimit);
244 }
245 let mut names = BTreeSet::new();
246 for descriptor in catalog.values() {
247 names.extend(exported_names(descriptor));
248 }
249 for descriptor in descriptors {
250 validate(descriptor)?;
251 for name in exported_names(descriptor) {
252 if !names.insert(name) {
253 return Err(MetricError::DuplicateRegistration);
254 }
255 }
256 }
257 let handles = descriptors
258 .iter()
259 .map(|descriptor| {
260 MetricHandle::new(
261 *descriptor,
262 &self.inner.meter,
263 self.inner.enabled,
264 self.inner.closed.clone(),
265 self.inner.limits.clone(),
266 self.inner.series_count.clone(),
267 )
268 })
269 .collect();
270 for descriptor in descriptors {
271 catalog.insert(metric_name(descriptor), *descriptor);
272 }
273 Ok(handles)
274 }
275
276 #[must_use]
277 pub fn descriptors(&self) -> Vec<MetricDescriptor> {
278 self.inner.descriptors.lock().values().copied().collect()
279 }
280
281 #[cfg(feature = "backend-prometheus")]
284 pub async fn export_prometheus(&self, timeout: Duration) -> MetricResult<String> {
287 use prometheus::Encoder;
288 let registry = self
289 .inner
290 .registry
291 .clone()
292 .ok_or(MetricError::Unavailable)?;
293 if self.inner.closed.load(Ordering::Acquire) {
294 return Err(MetricError::Closed);
295 }
296 let permit = self
297 .inner
298 .gather_limit
299 .clone()
300 .try_acquire_owned()
301 .map_err(|_| MetricError::Busy)?;
302 let max_bytes = self.inner.limits.max_exposition_bytes;
303 let task = tokio::task::spawn_blocking(move || {
304 let _permit = permit;
305 let mut buffer = LimitedBuffer {
306 bytes: Vec::new(),
307 remaining: max_bytes,
308 };
309 prometheus::TextEncoder::new()
310 .encode(®istry.gather(), &mut buffer)
311 .map_err(|_| MetricError::Export)?;
312 String::from_utf8(buffer.bytes).map_err(|_| MetricError::Export)
313 });
314 tokio::time::timeout(timeout, task)
315 .await
316 .map_err(|_| MetricError::Timeout)?
317 .map_err(|_| MetricError::Export)?
318 }
319
320 pub async fn shutdown(&self, timeout: Duration) -> MetricResult<()> {
325 if let Some(result) = *self.inner.shutdown_result.lock() {
326 return result;
327 }
328 let permit = self
329 .inner
330 .lifecycle_limit
331 .clone()
332 .try_acquire_owned()
333 .map_err(|_| MetricError::Busy)?;
334 self.inner.closed.store(true, Ordering::Release);
335 let sdk = self.inner.sdk.clone();
336 let result_slot = self.inner.shutdown_result.clone();
337 let task = tokio::task::spawn_blocking(move || {
338 let _permit = permit;
339 let result = sdk
340 .shutdown_with_timeout(timeout)
341 .map_err(|_| MetricError::Shutdown);
342 *result_slot.lock() = Some(result);
343 result
344 });
345 tokio::time::timeout(timeout, task)
346 .await
347 .map_err(|_| MetricError::Timeout)?
348 .map_err(|_| MetricError::Shutdown)?
349 }
350
351 #[must_use]
352 pub fn shutdown_result(&self) -> Option<MetricResult<()>> {
353 *self.inner.shutdown_result.lock()
354 }
355
356 pub async fn force_flush(&self, timeout: Duration) -> MetricResult<()> {
359 if self.inner.closed.load(Ordering::Acquire) {
360 return Err(MetricError::Closed);
361 }
362 let permit = self
363 .inner
364 .lifecycle_limit
365 .clone()
366 .try_acquire_owned()
367 .map_err(|_| MetricError::Busy)?;
368 let sdk = self.inner.sdk.clone();
369 let task = tokio::task::spawn_blocking(move || {
370 let _permit = permit;
371 sdk.force_flush().map_err(|_| MetricError::Export)
372 });
373 tokio::time::timeout(timeout, task)
374 .await
375 .map_err(|_| MetricError::Timeout)?
376 .map_err(|_| MetricError::Export)?
377 }
378}
379
380pub(crate) fn metric_name(descriptor: &MetricDescriptor) -> String {
381 if descriptor.subsystem.is_empty() {
382 descriptor.name.to_owned()
383 } else {
384 format!("{}_{}", descriptor.subsystem, descriptor.name)
385 }
386}
387
388fn exported_names(descriptor: &MetricDescriptor) -> Vec<String> {
389 let name = metric_name(descriptor);
390 if descriptor.kind == MetricKind::Histogram {
391 vec![
392 name.clone(),
393 format!("{name}_bucket"),
394 format!("{name}_sum"),
395 format!("{name}_count"),
396 ]
397 } else {
398 vec![name]
399 }
400}
401
402fn validate(descriptor: &MetricDescriptor) -> MetricResult<()> {
403 let name = metric_name(descriptor);
404 let valid_name = |value: &str| {
405 value
406 .as_bytes()
407 .first()
408 .is_some_and(u8::is_ascii_alphabetic)
409 && value.len() <= 255
410 && value
411 .bytes()
412 .all(|c| c.is_ascii_alphanumeric() || c == b'_')
413 };
414 if !valid_name(&name)
415 || descriptor.help.is_empty()
416 || descriptor.labels.len() > 8
417 || descriptor.unit.is_empty()
418 || descriptor.unit.len() > 32
419 || !descriptor.unit.bytes().all(|b| b.is_ascii_graphic())
420 {
421 return Err(MetricError::InvalidDescriptor);
422 }
423 let mut labels = BTreeSet::new();
424 for label in descriptor.labels {
425 if !valid_name(label) || *label == "le" || !labels.insert(label) {
426 return Err(MetricError::InvalidDescriptor);
427 }
428 }
429 if (descriptor.kind != MetricKind::Histogram && !descriptor.buckets.is_empty())
430 || descriptor.buckets.len() > 64
431 || descriptor
432 .buckets
433 .iter()
434 .any(|value| !value.is_finite() || *value < 0.0)
435 || descriptor.buckets.windows(2).any(|pair| pair[0] >= pair[1])
436 {
437 return Err(MetricError::InvalidDescriptor);
438 }
439 Ok(())
440}
441
442#[cfg(feature = "backend-prometheus")]
443struct LimitedBuffer {
444 bytes: Vec<u8>,
445 remaining: usize,
446}
447
448#[cfg(feature = "backend-prometheus")]
449impl std::io::Write for LimitedBuffer {
450 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
451 if bytes.len() > self.remaining {
452 return Err(std::io::Error::other("exposition limit"));
453 }
454 self.bytes.extend_from_slice(bytes);
455 self.remaining -= bytes.len();
456 Ok(bytes.len())
457 }
458 fn flush(&mut self) -> std::io::Result<()> {
459 Ok(())
460 }
461}
462
463#[cfg(all(test, feature = "backend-prometheus"))]
464mod collection_tests {
465 use super::*;
466 use prometheus::core::Collector;
467 use std::sync::{Condvar, Mutex as StdMutex};
468
469 #[derive(Default)]
470 struct Gate {
471 released: StdMutex<bool>,
472 ready: Condvar,
473 }
474 impl Gate {
475 fn release(&self) {
476 *self.released.lock().unwrap() = true;
477 self.ready.notify_all();
478 }
479 }
480 struct ReleaseOnDrop(Arc<Gate>);
481 impl Drop for ReleaseOnDrop {
482 fn drop(&mut self) {
483 self.0.release();
484 }
485 }
486 struct SlowCollector {
487 gauge: prometheus::Gauge,
488 gate: Arc<Gate>,
489 }
490 impl Collector for SlowCollector {
491 fn desc(&self) -> Vec<&prometheus::core::Desc> {
492 self.gauge.desc()
493 }
494 fn collect(&self) -> Vec<prometheus::proto::MetricFamily> {
495 let mut released = self.gate.released.lock().unwrap();
496 while !*released {
497 released = self.gate.ready.wait(released).unwrap();
498 }
499 self.gauge.collect()
500 }
501 }
502
503 #[tokio::test]
504 async fn timed_out_gathers_keep_actual_work_credits_until_completion() {
505 let provider = MetricsProvider::builder()
506 .with_prometheus()
507 .unwrap()
508 .build()
509 .unwrap();
510 let gate = Arc::new(Gate::default());
511 let _release = ReleaseOnDrop(gate.clone());
512 provider
513 .inner
514 .registry
515 .as_ref()
516 .unwrap()
517 .register(Box::new(SlowCollector {
518 gauge: prometheus::Gauge::new("test_slow", "Slow test collector.").unwrap(),
519 gate: gate.clone(),
520 }))
521 .unwrap();
522 for _ in 0..2 {
523 assert_eq!(
524 provider.export_prometheus(Duration::from_millis(10)).await,
525 Err(MetricError::Timeout)
526 );
527 }
528 assert_eq!(provider.inner.gather_limit.available_permits(), 0);
529 assert_eq!(
530 provider.export_prometheus(Duration::from_secs(1)).await,
531 Err(MetricError::Busy)
532 );
533 gate.release();
534 tokio::time::timeout(Duration::from_secs(1), async {
535 while provider.inner.gather_limit.available_permits() != 2 {
536 tokio::task::yield_now().await;
537 }
538 })
539 .await
540 .unwrap();
541 assert!(
542 provider
543 .export_prometheus(Duration::from_secs(1))
544 .await
545 .unwrap()
546 .contains("test_slow")
547 );
548 provider.shutdown(Duration::from_secs(1)).await.unwrap();
549 }
550}
551
552#[cfg(test)]
553mod lifecycle_tests {
554 use super::*;
555 use opentelemetry_sdk::{
556 error::OTelSdkResult,
557 metrics::{
558 InstrumentKind, Pipeline, Temporality, data::ResourceMetrics, reader::MetricReader,
559 },
560 };
561 use std::sync::{Condvar, Mutex as StdMutex, Weak};
562
563 #[derive(Debug, Default)]
564 struct Gate(StdMutex<bool>, Condvar);
565 impl Gate {
566 fn wait(&self) {
567 let mut released = self.0.lock().unwrap();
568 while !*released {
569 released = self.1.wait(released).unwrap();
570 }
571 }
572 fn release(&self) {
573 *self.0.lock().unwrap() = true;
574 self.1.notify_all();
575 }
576 }
577 struct ReleaseOnDrop(Arc<Gate>, Arc<Gate>);
578 impl Drop for ReleaseOnDrop {
579 fn drop(&mut self) {
580 self.0.release();
581 self.1.release();
582 }
583 }
584 #[derive(Debug)]
585 struct SlowReader {
586 flush: Arc<Gate>,
587 stop: Arc<Gate>,
588 }
589 impl MetricReader for SlowReader {
590 fn register_pipeline(&self, _: Weak<Pipeline>) {}
591 fn collect(&self, _: &mut ResourceMetrics) -> OTelSdkResult {
592 Ok(())
593 }
594 fn force_flush(&self) -> OTelSdkResult {
595 self.flush.wait();
596 Ok(())
597 }
598 fn shutdown_with_timeout(&self, _: Duration) -> OTelSdkResult {
599 self.stop.wait();
600 Ok(())
601 }
602 fn temporality(&self, _: InstrumentKind) -> Temporality {
603 Temporality::Cumulative
604 }
605 }
606 #[tokio::test]
607 async fn lifecycle_timeouts_retain_work_permit_and_report_actual_shutdown_completion() {
608 let flush = Arc::new(Gate::default());
609 let stop = Arc::new(Gate::default());
610 let _release = ReleaseOnDrop(flush.clone(), stop.clone());
611 let provider = MetricsProvider::builder()
612 .with_reader(SlowReader {
613 flush: flush.clone(),
614 stop: stop.clone(),
615 })
616 .build()
617 .unwrap();
618 let handle = provider
619 .register_batch(&[MetricDescriptor::counter("test", "events", "Events.", &[])])
620 .unwrap()
621 .remove(0);
622 let counter = crate::ProductCounter::from_handle(handle).unwrap();
623 assert_eq!(
624 provider.force_flush(Duration::from_millis(10)).await,
625 Err(MetricError::Timeout)
626 );
627 assert_eq!(
628 provider.force_flush(Duration::from_secs(1)).await,
629 Err(MetricError::Busy)
630 );
631 assert_eq!(
632 provider.shutdown(Duration::from_secs(1)).await,
633 Err(MetricError::Busy)
634 );
635 assert!(
636 counter.try_inc(&[], 1).is_ok(),
637 "busy shutdown must not close the provider"
638 );
639 flush.release();
640 tokio::time::timeout(Duration::from_secs(1), async {
641 while provider.inner.lifecycle_limit.available_permits() == 0 {
642 tokio::task::yield_now().await;
643 }
644 })
645 .await
646 .unwrap();
647 assert_eq!(
648 provider.shutdown(Duration::from_millis(10)).await,
649 Err(MetricError::Timeout)
650 );
651 assert_eq!(provider.shutdown_result(), None);
652 assert_eq!(counter.try_inc(&[], 1), Err(MetricError::Closed));
653 assert_eq!(
654 provider.shutdown(Duration::from_secs(1)).await,
655 Err(MetricError::Busy)
656 );
657 stop.release();
658 tokio::time::timeout(Duration::from_secs(1), async {
659 while provider.shutdown_result().is_none() {
660 tokio::task::yield_now().await;
661 }
662 })
663 .await
664 .unwrap();
665 assert_eq!(provider.shutdown_result(), Some(Ok(())));
666 assert_eq!(provider.shutdown(Duration::from_secs(1)).await, Ok(()));
667 }
668}