use crate::error::{ObservabilityError, Result};
use crate::telemetry::TelemetryConfig;
use opentelemetry::KeyValue;
use opentelemetry::global;
use opentelemetry::metrics::{Counter, Histogram, Meter, ObservableGauge, UpDownCounter};
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
use parking_lot::RwLock;
use std::sync::Arc;
use std::time::Duration;
pub fn init_metrics(config: &TelemetryConfig, resource: Resource) -> Result<SdkMeterProvider> {
let provider = if let Some(ref endpoint) = config.prometheus_endpoint {
create_prometheus_provider(endpoint, resource)?
} else if let Some(ref endpoint) = config.otlp_endpoint {
#[cfg(feature = "otlp")]
{
create_otlp_provider(endpoint, resource)?
}
#[cfg(not(feature = "otlp"))]
{
return Err(ObservabilityError::ConfigError(
"OTLP feature not enabled".to_string(),
));
}
} else {
create_stdout_provider(resource)?
};
global::set_meter_provider(provider.clone());
Ok(provider)
}
fn create_prometheus_provider(_endpoint: &str, resource: Resource) -> Result<SdkMeterProvider> {
let exporter = opentelemetry_stdout::MetricExporter::default();
let reader = PeriodicReader::builder(exporter)
.with_interval(Duration::from_secs(60))
.build();
let provider = SdkMeterProvider::builder()
.with_resource(resource)
.with_reader(reader)
.build();
Ok(provider)
}
#[cfg(feature = "otlp")]
fn create_otlp_provider(endpoint: &str, resource: Resource) -> Result<SdkMeterProvider> {
use opentelemetry_otlp::WithExportConfig;
let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()
.map_err(|e| ObservabilityError::MetricsExportFailed(e.to_string()))?;
let reader = PeriodicReader::builder(exporter)
.with_interval(Duration::from_secs(60))
.build();
let provider = SdkMeterProvider::builder()
.with_resource(resource)
.with_reader(reader)
.build();
Ok(provider)
}
fn create_stdout_provider(resource: Resource) -> Result<SdkMeterProvider> {
let exporter = opentelemetry_stdout::MetricExporter::default();
let reader = PeriodicReader::builder(exporter)
.with_interval(Duration::from_secs(60))
.build();
let provider = SdkMeterProvider::builder()
.with_resource(resource)
.with_reader(reader)
.build();
Ok(provider)
}
pub struct MetricRegistry {
meter: Meter,
counters: Arc<RwLock<Vec<String>>>,
histograms: Arc<RwLock<Vec<String>>>,
gauges: Arc<RwLock<Vec<String>>>,
}
impl MetricRegistry {
pub fn new(meter: Meter) -> Self {
Self {
meter,
counters: Arc::new(RwLock::new(Vec::new())),
histograms: Arc::new(RwLock::new(Vec::new())),
gauges: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn create_counter(
&self,
name: impl Into<String>,
description: impl Into<String>,
) -> Result<Counter<u64>> {
let name_str = name.into();
let counter = self
.meter
.u64_counter(name_str.clone())
.with_description(description.into())
.build();
self.counters.write().push(name_str);
Ok(counter)
}
pub fn create_up_down_counter(
&self,
name: impl Into<String>,
description: impl Into<String>,
) -> Result<UpDownCounter<i64>> {
let counter = self
.meter
.i64_up_down_counter(name.into())
.with_description(description.into())
.build();
Ok(counter)
}
pub fn create_histogram(
&self,
name: impl Into<String>,
description: impl Into<String>,
) -> Result<Histogram<f64>> {
let name_str = name.into();
let histogram = self
.meter
.f64_histogram(name_str.clone())
.with_description(description.into())
.build();
self.histograms.write().push(name_str);
Ok(histogram)
}
pub fn create_gauge<F>(
&self,
name: impl Into<String>,
description: impl Into<String>,
callback: F,
) -> Result<ObservableGauge<f64>>
where
F: Fn(&dyn opentelemetry::metrics::AsyncInstrument<f64>) + Send + Sync + 'static,
{
let name_str = name.into();
let gauge = self
.meter
.f64_observable_gauge(name_str.clone())
.with_description(description.into())
.with_callback(callback)
.build();
self.gauges.write().push(name_str);
Ok(gauge)
}
pub fn counter_names(&self) -> Vec<String> {
self.counters.read().clone()
}
pub fn histogram_names(&self) -> Vec<String> {
self.histograms.read().clone()
}
pub fn gauge_names(&self) -> Vec<String> {
self.gauges.read().clone()
}
}
pub struct MetricAttributes {
attributes: Vec<KeyValue>,
}
impl MetricAttributes {
pub fn new() -> Self {
Self {
attributes: Vec::new(),
}
}
pub fn add_string(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes
.push(KeyValue::new(key.into(), value.into()));
self
}
pub fn add_int(mut self, key: impl Into<String>, value: i64) -> Self {
self.attributes.push(KeyValue::new(key.into(), value));
self
}
pub fn add_float(mut self, key: impl Into<String>, value: f64) -> Self {
self.attributes.push(KeyValue::new(key.into(), value));
self
}
pub fn add_bool(mut self, key: impl Into<String>, value: bool) -> Self {
self.attributes.push(KeyValue::new(key.into(), value));
self
}
pub fn as_slice(&self) -> &[KeyValue] {
&self.attributes
}
}
impl Default for MetricAttributes {
fn default() -> Self {
Self::new()
}
}
pub struct HistogramBounds;
impl HistogramBounds {
pub fn latency_ms() -> Vec<f64> {
vec![
0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0,
10000.0, 30000.0, 60000.0,
]
}
pub fn size_bytes() -> Vec<f64> {
vec![
1024.0,
10240.0,
102400.0,
1048576.0,
10485760.0,
104857600.0,
1073741824.0,
]
}
pub fn percentage() -> Vec<f64> {
vec![
0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 95.0, 99.0, 100.0,
]
}
pub fn count() -> Vec<f64> {
vec![1.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_attributes() {
let attrs = MetricAttributes::new()
.add_string("service", "oxigdal")
.add_int("version", 1)
.add_float("ratio", 0.5)
.add_bool("enabled", true);
assert_eq!(attrs.as_slice().len(), 4);
}
#[test]
fn test_histogram_bounds() {
let latency = HistogramBounds::latency_ms();
assert!(!latency.is_empty());
assert_eq!(latency[0], 0.1);
let size = HistogramBounds::size_bytes();
assert!(!size.is_empty());
assert_eq!(size[0], 1024.0);
let percentage = HistogramBounds::percentage();
assert!(!percentage.is_empty());
assert_eq!(percentage[0], 0.0);
let count = HistogramBounds::count();
assert!(!count.is_empty());
assert_eq!(count[0], 1.0);
}
}