use crate::telemetry::resource::ResourceBuilder;
use opentelemetry::{global, metrics::MeterProvider as _, KeyValue};
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::{
metrics::{
reader::{DefaultAggregationSelector, DefaultTemporalitySelector, TemporalitySelector},
PeriodicReader, SdkMeterProvider,
},
runtime,
Resource,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Temporality {
Delta,
Cumulative,
}
impl Default for Temporality {
fn default() -> Self {
Self::Cumulative
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportConfig {
#[serde(with = "humantime_serde", default = "default_export_interval")]
pub export_interval: Duration,
#[serde(with = "humantime_serde", default = "default_export_timeout")]
pub export_timeout: Duration,
}
fn default_export_interval() -> Duration {
Duration::from_secs(60)
}
fn default_export_timeout() -> Duration {
Duration::from_secs(30)
}
impl Default for ExportConfig {
fn default() -> Self {
Self {
export_interval: default_export_interval(),
export_timeout: default_export_timeout(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
pub service_name: String,
#[serde(default = "default_service_version")]
pub service_version: String,
#[serde(default = "default_otlp_endpoint")]
pub otlp_endpoint: String,
#[serde(default)]
pub temporality: Temporality,
#[serde(default)]
pub export_config: ExportConfig,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub environment: Option<String>,
}
fn default_enabled() -> bool {
true
}
fn default_service_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
fn default_otlp_endpoint() -> String {
"http://localhost:4317".to_string()
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
enabled: default_enabled(),
service_name: "llm-optimizer-processor".to_string(),
service_version: default_service_version(),
otlp_endpoint: default_otlp_endpoint(),
temporality: Temporality::default(),
export_config: ExportConfig::default(),
headers: HashMap::new(),
environment: None,
}
}
}
pub fn init_meter_provider(
config: MetricsConfig,
) -> Result<SdkMeterProvider, Box<dyn std::error::Error>> {
if !config.enabled {
tracing::info!("OpenTelemetry metrics export is disabled");
return Err("metrics disabled".into());
}
let resource = ResourceBuilder::new()
.with_service_name(config.service_name.clone())
.with_service_version(config.service_version.clone())
.with_attribute("service.component", "stream-processor");
let resource = if let Some(env) = config.environment.clone() {
resource.with_deployment_environment(env)
} else {
resource
};
let resource = resource.build();
let exporter_builder = opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint(&config.otlp_endpoint);
let exporter_builder = if !config.headers.is_empty() {
let mut metadata = tonic::metadata::MetadataMap::new();
for (key, value) in &config.headers {
if let (Ok(k), Ok(v)) = (
key.parse::<tonic::metadata::MetadataKey<_>>(),
value.parse::<tonic::metadata::MetadataValue<_>>(),
) {
metadata.insert(k, v);
}
}
exporter_builder.with_metadata(metadata)
} else {
exporter_builder
};
let exporter = opentelemetry_otlp::new_pipeline()
.metrics(runtime::Tokio)
.with_exporter(exporter_builder)
.with_resource(resource)
.with_period(config.export_config.export_interval)
.with_timeout(config.export_config.export_timeout)
.build()?;
Ok(exporter)
}
pub fn shutdown_meter_provider() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[derive(Debug, Clone)]
pub struct MetricsRecorder {
common_attributes: Vec<KeyValue>,
}
impl MetricsRecorder {
pub fn new() -> Self {
Self {
common_attributes: Vec::new(),
}
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.common_attributes
.push(KeyValue::new(key.into(), value.into()));
self
}
pub fn with_attributes(mut self, attributes: Vec<KeyValue>) -> Self {
self.common_attributes.extend(attributes);
self
}
pub fn record_counter(
&self,
meter_name: &str,
counter_name: &str,
value: u64,
attributes: Vec<KeyValue>,
) {
let meter = global::meter(meter_name.to_string());
let counter = meter.u64_counter(counter_name.to_string()).init();
let mut all_attrs = self.common_attributes.clone();
all_attrs.extend(attributes);
counter.add(value, &all_attrs);
}
pub fn record_histogram(
&self,
meter_name: &str,
histogram_name: &str,
value: f64,
attributes: Vec<KeyValue>,
) {
let meter = global::meter(meter_name.to_string());
let histogram = meter.f64_histogram(histogram_name.to_string()).init();
let mut all_attrs = self.common_attributes.clone();
all_attrs.extend(attributes);
histogram.record(value, &all_attrs);
}
pub fn record_gauge(
&self,
meter_name: &str,
gauge_name: &str,
value: i64,
attributes: Vec<KeyValue>,
) {
let meter = global::meter(meter_name.to_string());
let gauge = meter.i64_up_down_counter(gauge_name.to_string()).init();
let mut all_attrs = self.common_attributes.clone();
all_attrs.extend(attributes);
gauge.add(value, &all_attrs);
}
}
impl Default for MetricsRecorder {
fn default() -> Self {
Self::new()
}
}
pub mod metric_names {
pub const EVENTS_PROCESSED: &str = "processor.events.processed";
pub const EVENTS_DROPPED: &str = "processor.events.dropped";
pub const PROCESSING_DURATION: &str = "processor.processing.duration";
pub const WINDOW_TRIGGER_COUNT: &str = "processor.window.triggers";
pub const AGGREGATION_DURATION: &str = "processor.aggregation.duration";
pub const STATE_OPERATION_DURATION: &str = "processor.state.operation.duration";
pub const DEDUPLICATION_COUNT: &str = "processor.deduplication.count";
pub const WATERMARK_LAG: &str = "processor.watermark.lag";
}
pub mod metric_attributes {
pub const EVENT_TYPE: &str = "event.type";
pub const WINDOW_TYPE: &str = "window.type";
pub const AGGREGATION_TYPE: &str = "aggregation.type";
pub const STATE_BACKEND: &str = "state.backend";
pub const OPERATION_TYPE: &str = "operation.type";
pub const ERROR_TYPE: &str = "error.type";
pub const PARTITION_ID: &str = "partition.id";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_temporality_default() {
let temporality = Temporality::default();
assert_eq!(temporality, Temporality::Cumulative);
}
#[test]
fn test_export_config_default() {
let config = ExportConfig::default();
assert_eq!(config.export_interval, Duration::from_secs(60));
assert_eq!(config.export_timeout, Duration::from_secs(30));
}
#[test]
fn test_metrics_config_default() {
let config = MetricsConfig::default();
assert!(config.enabled);
assert_eq!(config.service_name, "llm-optimizer-processor");
assert_eq!(config.otlp_endpoint, "http://localhost:4317");
assert_eq!(config.temporality, Temporality::Cumulative);
}
#[test]
fn test_metrics_recorder_new() {
let recorder = MetricsRecorder::new();
assert!(recorder.common_attributes.is_empty());
}
#[test]
fn test_metrics_recorder_with_attributes() {
let recorder = MetricsRecorder::new()
.with_attribute("service", "test")
.with_attribute("env", "dev");
assert_eq!(recorder.common_attributes.len(), 2);
}
#[test]
fn test_temporality_serialization() {
let delta = Temporality::Delta;
let cumulative = Temporality::Cumulative;
let delta_json = serde_json::to_string(&delta).unwrap();
let cumulative_json = serde_json::to_string(&cumulative).unwrap();
assert_eq!(delta_json, "\"delta\"");
assert_eq!(cumulative_json, "\"cumulative\"");
}
}