pub mod cloudwatch;
pub mod influxdb;
pub mod json;
pub mod prometheus;
pub mod statsd;
use crate::error::{ObservabilityError, Result};
use chrono::{DateTime, Utc};
use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
pub use cloudwatch::CloudWatchExporter;
pub use influxdb::InfluxDbExporter;
pub use json::JsonFileExporter;
pub use prometheus::PrometheusExporter;
pub use statsd::StatsdExporter;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metric {
pub name: String,
pub value: MetricValue,
pub labels: HashMap<String, String>,
pub timestamp: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<MetricUnit>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MetricValue {
Counter(u64),
Gauge(f64),
Histogram(HistogramValue),
Summary(SummaryValue),
Distribution(DistributionValue),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistogramValue {
pub buckets: Vec<f64>,
pub bucket_counts: Vec<u64>,
pub sum: f64,
pub count: u64,
pub min: Option<f64>,
pub max: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SummaryValue {
pub quantiles: Vec<(f64, f64)>,
pub sum: f64,
pub count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributionValue {
pub values: Vec<f64>,
pub mean: f64,
pub std_dev: f64,
pub min: f64,
pub max: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum MetricUnit {
Bytes,
Kilobytes,
Megabytes,
Gigabytes,
Nanoseconds,
Microseconds,
Milliseconds,
Seconds,
Count,
Percent,
Ratio,
RequestsPerSecond,
Custom,
}
impl Metric {
pub fn counter(name: impl Into<String>, value: u64) -> Self {
Self {
name: name.into(),
value: MetricValue::Counter(value),
labels: HashMap::new(),
timestamp: Utc::now(),
description: None,
unit: Some(MetricUnit::Count),
}
}
pub fn gauge(name: impl Into<String>, value: f64) -> Self {
Self {
name: name.into(),
value: MetricValue::Gauge(value),
labels: HashMap::new(),
timestamp: Utc::now(),
description: None,
unit: None,
}
}
pub fn histogram(name: impl Into<String>, values: Vec<f64>) -> Self {
let histogram = HistogramValue::from_values(&values, &default_histogram_buckets());
Self {
name: name.into(),
value: MetricValue::Histogram(histogram),
labels: HashMap::new(),
timestamp: Utc::now(),
description: None,
unit: None,
}
}
pub fn histogram_with_buckets(
name: impl Into<String>,
values: Vec<f64>,
buckets: &[f64],
) -> Self {
let histogram = HistogramValue::from_values(&values, buckets);
Self {
name: name.into(),
value: MetricValue::Histogram(histogram),
labels: HashMap::new(),
timestamp: Utc::now(),
description: None,
unit: None,
}
}
pub fn summary(name: impl Into<String>, values: Vec<f64>) -> Self {
let summary = SummaryValue::from_values(&values);
Self {
name: name.into(),
value: MetricValue::Summary(summary),
labels: HashMap::new(),
timestamp: Utc::now(),
description: None,
unit: None,
}
}
pub fn distribution(name: impl Into<String>, values: Vec<f64>) -> Self {
let distribution = DistributionValue::from_values(&values);
Self {
name: name.into(),
value: MetricValue::Distribution(distribution),
labels: HashMap::new(),
timestamp: Utc::now(),
description: None,
unit: None,
}
}
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
pub fn with_labels(mut self, labels: HashMap<String, String>) -> Self {
self.labels.extend(labels);
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_unit(mut self, unit: MetricUnit) -> Self {
self.unit = Some(unit);
self
}
pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = timestamp;
self
}
pub fn metric_type(&self) -> &'static str {
match &self.value {
MetricValue::Counter(_) => "counter",
MetricValue::Gauge(_) => "gauge",
MetricValue::Histogram(_) => "histogram",
MetricValue::Summary(_) => "summary",
MetricValue::Distribution(_) => "distribution",
}
}
}
impl HistogramValue {
pub fn from_values(values: &[f64], buckets: &[f64]) -> Self {
let mut bucket_counts = vec![0u64; buckets.len() + 1];
let mut sum = 0.0;
let mut min: Option<f64> = None;
let mut max: Option<f64> = None;
for &value in values {
sum += value;
min = Some(min.map_or(value, |m| m.min(value)));
max = Some(max.map_or(value, |m| m.max(value)));
let mut placed = false;
for (i, &boundary) in buckets.iter().enumerate() {
if value <= boundary {
bucket_counts[i] += 1;
placed = true;
break;
}
}
if !placed {
bucket_counts[buckets.len()] += 1;
}
}
Self {
buckets: buckets.to_vec(),
bucket_counts,
sum,
count: values.len() as u64,
min,
max,
}
}
}
impl SummaryValue {
pub fn from_values(values: &[f64]) -> Self {
Self::from_values_with_quantiles(values, &[0.5, 0.9, 0.95, 0.99])
}
pub fn from_values_with_quantiles(values: &[f64], quantile_points: &[f64]) -> Self {
if values.is_empty() {
return Self {
quantiles: quantile_points.iter().map(|&q| (q, 0.0)).collect(),
sum: 0.0,
count: 0,
};
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let sum: f64 = values.iter().sum();
let count = values.len();
let quantiles: Vec<(f64, f64)> = quantile_points
.iter()
.map(|&q| {
let idx = ((q * (count as f64 - 1.0)).floor() as usize).min(count - 1);
(q, sorted[idx])
})
.collect();
Self {
quantiles,
sum,
count: count as u64,
}
}
}
impl DistributionValue {
pub fn from_values(values: &[f64]) -> Self {
if values.is_empty() {
return Self {
values: Vec::new(),
mean: 0.0,
std_dev: 0.0,
min: 0.0,
max: 0.0,
};
}
let sum: f64 = values.iter().sum();
let mean = sum / values.len() as f64;
let variance: f64 =
values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
let std_dev = variance.sqrt();
let min = values.iter().copied().fold(f64::INFINITY, |a, b| a.min(b));
let max = values
.iter()
.copied()
.fold(f64::NEG_INFINITY, |a, b| a.max(b));
Self {
values: values.to_vec(),
mean,
std_dev,
min,
max,
}
}
}
pub fn default_histogram_buckets() -> Vec<f64> {
vec![
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
]
}
pub fn latency_buckets() -> Vec<f64> {
vec![
0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0,
]
}
pub fn size_buckets() -> Vec<f64> {
vec![
1024.0, 10240.0, 102400.0, 1048576.0, 10485760.0, 104857600.0, 1073741824.0, 10737418240.0, ]
}
pub trait MetricExporter: Send + Sync {
fn export(&self, metrics: &[Metric]) -> Result<()>;
fn flush(&self) -> Result<()>;
fn name(&self) -> &str;
fn is_healthy(&self) -> bool {
true
}
}
#[allow(async_fn_in_trait)]
pub trait AsyncMetricExporter: Send + Sync {
async fn export_async(&self, metrics: &[Metric]) -> Result<()>;
async fn flush_async(&self) -> Result<()>;
fn name(&self) -> &str;
async fn is_healthy(&self) -> bool {
true
}
}
pub trait CustomExporter: Send + Sync {
fn export_custom(&self, metrics: &[Metric], context: &ExportContext) -> Result<()>;
fn metadata(&self) -> ExporterMetadata;
fn validate_metric(&self, metric: &Metric) -> Result<()> {
let _ = metric;
Ok(())
}
fn transform_metrics(&self, metrics: &[Metric]) -> Vec<Metric> {
metrics.to_vec()
}
}
#[derive(Debug, Clone)]
pub struct ExportContext {
pub batch_number: u64,
pub total_metrics: usize,
pub export_start: DateTime<Utc>,
pub metadata: HashMap<String, String>,
}
impl Default for ExportContext {
fn default() -> Self {
Self {
batch_number: 0,
total_metrics: 0,
export_start: Utc::now(),
metadata: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct ExporterMetadata {
pub name: String,
pub version: String,
pub supported_types: Vec<String>,
pub max_batch_size: usize,
pub supports_async: bool,
}
#[derive(Debug, Clone)]
pub struct BatchConfig {
pub max_batch_size: usize,
pub max_batch_delay: Duration,
pub retry_on_failure: bool,
pub max_retries: u32,
pub retry_delay: Duration,
pub drop_on_overflow: bool,
pub max_queue_size: usize,
}
impl Default for BatchConfig {
fn default() -> Self {
Self {
max_batch_size: 1000,
max_batch_delay: Duration::from_secs(10),
retry_on_failure: true,
max_retries: 3,
retry_delay: Duration::from_millis(100),
drop_on_overflow: false,
max_queue_size: 10000,
}
}
}
pub struct BatchExporter<E: MetricExporter> {
inner: E,
config: BatchConfig,
buffer: Mutex<Vec<Metric>>,
stats: BatchExportStats,
last_flush: Mutex<DateTime<Utc>>,
}
#[derive(Debug, Default)]
pub struct BatchExportStats {
pub metrics_received: AtomicU64,
pub metrics_exported: AtomicU64,
pub metrics_dropped: AtomicU64,
pub batches_exported: AtomicU64,
pub export_failures: AtomicU64,
pub retries: AtomicU64,
}
impl<E: MetricExporter> BatchExporter<E> {
pub fn new(exporter: E) -> Self {
Self::with_config(exporter, BatchConfig::default())
}
pub fn with_config(exporter: E, config: BatchConfig) -> Self {
Self {
inner: exporter,
config,
buffer: Mutex::new(Vec::new()),
stats: BatchExportStats::default(),
last_flush: Mutex::new(Utc::now()),
}
}
pub fn add_metrics(&self, metrics: &[Metric]) -> Result<()> {
self.stats
.metrics_received
.fetch_add(metrics.len() as u64, Ordering::Relaxed);
let mut buffer = self.buffer.lock();
if self.config.max_queue_size > 0
&& buffer.len() + metrics.len() > self.config.max_queue_size
{
if self.config.drop_on_overflow {
let to_drop = (buffer.len() + metrics.len()) - self.config.max_queue_size;
self.stats
.metrics_dropped
.fetch_add(to_drop as u64, Ordering::Relaxed);
let can_add = self.config.max_queue_size.saturating_sub(buffer.len());
buffer.extend_from_slice(&metrics[..can_add.min(metrics.len())]);
} else {
return Err(ObservabilityError::MetricsExportFailed(
"Queue overflow".to_string(),
));
}
} else {
buffer.extend_from_slice(metrics);
}
if buffer.len() >= self.config.max_batch_size {
drop(buffer);
self.flush_internal()?;
}
Ok(())
}
pub fn force_flush(&self) -> Result<()> {
self.flush_internal()
}
pub fn should_flush(&self) -> bool {
let last_flush = self.last_flush.lock();
let elapsed = Utc::now().signed_duration_since(*last_flush);
elapsed.to_std().unwrap_or(Duration::ZERO) >= self.config.max_batch_delay
}
pub fn buffer_size(&self) -> usize {
self.buffer.lock().len()
}
pub fn stats(&self) -> &BatchExportStats {
&self.stats
}
fn flush_internal(&self) -> Result<()> {
let metrics = {
let mut buffer = self.buffer.lock();
std::mem::take(&mut *buffer)
};
if metrics.is_empty() {
return Ok(());
}
for chunk in metrics.chunks(self.config.max_batch_size) {
let mut attempts = 0;
let mut last_error = None;
while attempts <= self.config.max_retries {
match self.inner.export(chunk) {
Ok(()) => {
self.stats
.metrics_exported
.fetch_add(chunk.len() as u64, Ordering::Relaxed);
self.stats.batches_exported.fetch_add(1, Ordering::Relaxed);
break;
}
Err(e) => {
last_error = Some(e);
attempts += 1;
if attempts <= self.config.max_retries && self.config.retry_on_failure {
self.stats.retries.fetch_add(1, Ordering::Relaxed);
std::thread::sleep(self.config.retry_delay);
}
}
}
}
if attempts > self.config.max_retries {
self.stats.export_failures.fetch_add(1, Ordering::Relaxed);
self.stats
.metrics_dropped
.fetch_add(chunk.len() as u64, Ordering::Relaxed);
if let Some(e) = last_error {
return Err(e);
}
}
}
*self.last_flush.lock() = Utc::now();
Ok(())
}
}
impl<E: MetricExporter> MetricExporter for BatchExporter<E> {
fn export(&self, metrics: &[Metric]) -> Result<()> {
self.add_metrics(metrics)
}
fn flush(&self) -> Result<()> {
self.force_flush()
}
fn name(&self) -> &str {
self.inner.name()
}
fn is_healthy(&self) -> bool {
self.inner.is_healthy()
}
}
#[derive(Debug, Clone)]
pub struct ScheduleConfig {
pub interval: Duration,
pub export_on_start: bool,
pub export_on_shutdown: bool,
pub max_metrics_per_export: usize,
pub skip_empty_exports: bool,
}
impl Default for ScheduleConfig {
fn default() -> Self {
Self {
interval: Duration::from_secs(60),
export_on_start: false,
export_on_shutdown: true,
max_metrics_per_export: 10000,
skip_empty_exports: true,
}
}
}
pub struct ScheduledExporter<E: MetricExporter + 'static> {
exporter: Arc<BatchExporter<E>>,
config: ScheduleConfig,
running: Arc<AtomicBool>,
metrics_collector: Arc<RwLock<Vec<Metric>>>,
}
impl<E: MetricExporter + 'static> ScheduledExporter<E> {
pub fn new(exporter: E, config: ScheduleConfig) -> Self {
Self {
exporter: Arc::new(BatchExporter::new(exporter)),
config,
running: Arc::new(AtomicBool::new(false)),
metrics_collector: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn with_batch_config(
exporter: E,
schedule_config: ScheduleConfig,
batch_config: BatchConfig,
) -> Self {
Self {
exporter: Arc::new(BatchExporter::with_config(exporter, batch_config)),
config: schedule_config,
running: Arc::new(AtomicBool::new(false)),
metrics_collector: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn record(&self, metric: Metric) {
let mut collector = self.metrics_collector.write();
if collector.len() < self.config.max_metrics_per_export {
collector.push(metric);
}
}
pub fn record_many(&self, metrics: &[Metric]) {
let mut collector = self.metrics_collector.write();
let available = self
.config
.max_metrics_per_export
.saturating_sub(collector.len());
collector.extend_from_slice(&metrics[..available.min(metrics.len())]);
}
pub fn start(&self) -> ExportHandle {
self.running.store(true, Ordering::SeqCst);
if self.config.export_on_start {
let _ = self.export_now();
}
let running = Arc::clone(&self.running);
let exporter = Arc::clone(&self.exporter);
let collector = Arc::clone(&self.metrics_collector);
let interval = self.config.interval;
let skip_empty = self.config.skip_empty_exports;
let handle = std::thread::spawn(move || {
while running.load(Ordering::SeqCst) {
std::thread::sleep(interval);
if !running.load(Ordering::SeqCst) {
break;
}
let metrics = {
let mut c = collector.write();
std::mem::take(&mut *c)
};
if metrics.is_empty() && skip_empty {
continue;
}
if let Err(e) = exporter.add_metrics(&metrics) {
tracing::warn!("Scheduled export failed: {}", e);
}
if let Err(e) = exporter.force_flush() {
tracing::warn!("Scheduled flush failed: {}", e);
}
}
});
ExportHandle {
running: Arc::clone(&self.running),
handle: Some(handle),
}
}
pub fn export_now(&self) -> Result<()> {
let metrics = {
let mut collector = self.metrics_collector.write();
std::mem::take(&mut *collector)
};
if metrics.is_empty() && self.config.skip_empty_exports {
return Ok(());
}
self.exporter.add_metrics(&metrics)?;
self.exporter.force_flush()
}
pub fn stop(&self) {
self.running.store(false, Ordering::SeqCst);
}
pub fn inner(&self) -> &BatchExporter<E> {
&self.exporter
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
}
impl<E: MetricExporter + 'static> Drop for ScheduledExporter<E> {
fn drop(&mut self) {
if self.config.export_on_shutdown {
let _ = self.export_now();
}
self.stop();
}
}
pub struct ExportHandle {
running: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl ExportHandle {
pub fn stop(&mut self) {
self.running.store(false, Ordering::SeqCst);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
}
impl Drop for ExportHandle {
fn drop(&mut self) {
self.stop();
}
}
pub struct MultiExporter {
exporters: Vec<Box<dyn MetricExporter>>,
fail_fast: bool,
}
impl MultiExporter {
pub fn new() -> Self {
Self {
exporters: Vec::new(),
fail_fast: false,
}
}
pub fn add_exporter<E: MetricExporter + 'static>(mut self, exporter: E) -> Self {
self.exporters.push(Box::new(exporter));
self
}
pub fn fail_fast(mut self, fail_fast: bool) -> Self {
self.fail_fast = fail_fast;
self
}
pub fn exporter_count(&self) -> usize {
self.exporters.len()
}
}
impl Default for MultiExporter {
fn default() -> Self {
Self::new()
}
}
impl MetricExporter for MultiExporter {
fn export(&self, metrics: &[Metric]) -> Result<()> {
let mut errors = Vec::new();
for exporter in &self.exporters {
if let Err(e) = exporter.export(metrics) {
if self.fail_fast {
return Err(e);
}
errors.push(format!("{}: {}", exporter.name(), e));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(ObservabilityError::MetricsExportFailed(errors.join("; ")))
}
}
fn flush(&self) -> Result<()> {
let mut errors = Vec::new();
for exporter in &self.exporters {
if let Err(e) = exporter.flush() {
if self.fail_fast {
return Err(e);
}
errors.push(format!("{}: {}", exporter.name(), e));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(ObservabilityError::MetricsExportFailed(errors.join("; ")))
}
}
fn name(&self) -> &str {
"multi-exporter"
}
fn is_healthy(&self) -> bool {
self.exporters.iter().all(|e| e.is_healthy())
}
}
pub trait MetricFilter: Send + Sync {
fn should_export(&self, metric: &Metric) -> bool;
}
pub struct PrefixFilter {
prefixes: Vec<String>,
allow: bool,
}
impl PrefixFilter {
pub fn allow(prefixes: Vec<String>) -> Self {
Self {
prefixes,
allow: true,
}
}
pub fn deny(prefixes: Vec<String>) -> Self {
Self {
prefixes,
allow: false,
}
}
}
impl MetricFilter for PrefixFilter {
fn should_export(&self, metric: &Metric) -> bool {
let matches = self.prefixes.iter().any(|p| metric.name.starts_with(p));
if self.allow { matches } else { !matches }
}
}
pub struct LabelFilter {
required_labels: Vec<String>,
}
impl LabelFilter {
pub fn requiring(labels: Vec<String>) -> Self {
Self {
required_labels: labels,
}
}
}
impl MetricFilter for LabelFilter {
fn should_export(&self, metric: &Metric) -> bool {
self.required_labels
.iter()
.all(|l| metric.labels.contains_key(l))
}
}
pub struct FilteredExporter<E: MetricExporter> {
inner: E,
filters: Vec<Box<dyn MetricFilter>>,
}
impl<E: MetricExporter> FilteredExporter<E> {
pub fn new(exporter: E) -> Self {
Self {
inner: exporter,
filters: Vec::new(),
}
}
pub fn with_filter<F: MetricFilter + 'static>(mut self, filter: F) -> Self {
self.filters.push(Box::new(filter));
self
}
}
impl<E: MetricExporter> MetricExporter for FilteredExporter<E> {
fn export(&self, metrics: &[Metric]) -> Result<()> {
let filtered: Vec<Metric> = metrics
.iter()
.filter(|m| self.filters.iter().all(|f| f.should_export(m)))
.cloned()
.collect();
if filtered.is_empty() {
return Ok(());
}
self.inner.export(&filtered)
}
fn flush(&self) -> Result<()> {
self.inner.flush()
}
fn name(&self) -> &str {
self.inner.name()
}
fn is_healthy(&self) -> bool {
self.inner.is_healthy()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestExporter {
name: String,
exported: Arc<Mutex<Vec<Metric>>>,
}
impl TestExporter {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
exported: Arc::new(Mutex::new(Vec::new())),
}
}
#[allow(dead_code)]
fn get_exported(&self) -> Vec<Metric> {
self.exported.lock().clone()
}
}
impl MetricExporter for TestExporter {
fn export(&self, metrics: &[Metric]) -> Result<()> {
self.exported.lock().extend_from_slice(metrics);
Ok(())
}
fn flush(&self) -> Result<()> {
Ok(())
}
fn name(&self) -> &str {
&self.name
}
}
#[test]
fn test_metric_creation() {
let counter = Metric::counter("requests_total", 42);
assert_eq!(counter.name, "requests_total");
assert!(matches!(counter.value, MetricValue::Counter(42)));
let gauge = Metric::gauge("temperature", 23.5);
assert_eq!(gauge.name, "temperature");
assert!(matches!(gauge.value, MetricValue::Gauge(v) if (v - 23.5).abs() < f64::EPSILON));
}
#[test]
fn test_metric_with_labels() {
let metric = Metric::counter("requests", 100)
.with_label("method", "GET")
.with_label("status", "200");
assert_eq!(metric.labels.get("method"), Some(&"GET".to_string()));
assert_eq!(metric.labels.get("status"), Some(&"200".to_string()));
}
#[test]
fn test_histogram_value() {
let values = vec![0.1, 0.5, 1.0, 2.0, 5.0];
let buckets = vec![0.5, 1.0, 2.5, 5.0, 10.0];
let histogram = HistogramValue::from_values(&values, &buckets);
assert_eq!(histogram.count, 5);
assert!((histogram.sum - 8.6).abs() < 0.001);
assert_eq!(histogram.min, Some(0.1));
assert_eq!(histogram.max, Some(5.0));
}
#[test]
fn test_summary_value() {
let values: Vec<f64> = (1..=100).map(|i| i as f64).collect();
let summary = SummaryValue::from_values(&values);
assert_eq!(summary.count, 100);
assert!((summary.sum - 5050.0).abs() < 0.001);
}
#[test]
fn test_distribution_value() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let dist = DistributionValue::from_values(&values);
assert!((dist.mean - 3.0).abs() < 0.001);
assert_eq!(dist.min, 1.0);
assert_eq!(dist.max, 5.0);
}
#[test]
fn test_batch_exporter() {
let test_exporter = TestExporter::new("test");
let exported = Arc::clone(&test_exporter.exported);
let batch = BatchExporter::with_config(
test_exporter,
BatchConfig {
max_batch_size: 5,
..Default::default()
},
);
let metrics: Vec<Metric> = (0..3)
.map(|i| Metric::counter(format!("metric_{}", i), i as u64))
.collect();
batch.add_metrics(&metrics).expect("add should succeed");
assert_eq!(batch.buffer_size(), 3);
assert_eq!(exported.lock().len(), 0);
let more_metrics: Vec<Metric> = (3..8)
.map(|i| Metric::counter(format!("metric_{}", i), i as u64))
.collect();
batch
.add_metrics(&more_metrics)
.expect("add should succeed");
assert!(!exported.lock().is_empty());
}
#[test]
fn test_multi_exporter() {
let exporter1 = TestExporter::new("exporter1");
let exporter2 = TestExporter::new("exporter2");
let exported1 = Arc::clone(&exporter1.exported);
let exported2 = Arc::clone(&exporter2.exported);
let multi = MultiExporter::new()
.add_exporter(exporter1)
.add_exporter(exporter2);
let metrics = vec![Metric::counter("test", 1)];
multi.export(&metrics).expect("export should succeed");
assert_eq!(exported1.lock().len(), 1);
assert_eq!(exported2.lock().len(), 1);
}
#[test]
fn test_prefix_filter() {
let allow_filter = PrefixFilter::allow(vec!["http_".to_string(), "grpc_".to_string()]);
let http_metric = Metric::counter("http_requests", 100);
let db_metric = Metric::counter("db_queries", 50);
assert!(allow_filter.should_export(&http_metric));
assert!(!allow_filter.should_export(&db_metric));
}
#[test]
fn test_filtered_exporter() {
let test_exporter = TestExporter::new("filtered");
let exported = Arc::clone(&test_exporter.exported);
let filtered = FilteredExporter::new(test_exporter)
.with_filter(PrefixFilter::allow(vec!["allowed_".to_string()]));
let metrics = vec![
Metric::counter("allowed_metric", 1),
Metric::counter("denied_metric", 2),
];
filtered.export(&metrics).expect("export should succeed");
let result = exported.lock();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "allowed_metric");
}
}