use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::WyrdError;
use crate::vala::ids::{SpanId, TraceId};
use crate::vala::trace::{InstrumentationScope, Resource};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum MetricType {
Gauge,
Sum,
Histogram,
ExponentialHistogram,
Summary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum AggregationTemporality {
Delta,
Cumulative,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct ExponentialBuckets {
pub offset: i32,
pub bucket_counts: Vec<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct QuantileValue {
pub quantile: f64,
pub value: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MetricExemplar {
pub time: DateTime<Utc>,
pub value: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace_id: Option<TraceId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span_id: Option<SpanId>,
#[serde(default)]
pub filtered_attributes: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MetricRecord {
pub time: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_time: Option<DateTime<Utc>>,
pub metric_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
pub metric_type: MetricType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temporality: Option<AggregationTemporality>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_monotonic: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flags: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub count: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sum: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bucket_counts: Option<Vec<u64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub explicit_bounds: Option<Vec<f64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scale: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub zero_count: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub zero_threshold: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positive_buckets: Option<ExponentialBuckets>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub negative_buckets: Option<ExponentialBuckets>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quantile_values: Option<Vec<QuantileValue>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exemplars: Vec<MetricExemplar>,
pub resource: Resource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<InstrumentationScope>,
#[serde(default)]
pub attributes: serde_json::Map<String, serde_json::Value>,
}
impl MetricRecord {
pub fn validate(&self) -> Result<(), WyrdError> {
let err = |msg: &str| {
Err(WyrdError::Validation {
message: msg.to_string(),
details: serde_json::Value::Null,
})
};
for ex in &self.exemplars {
if ex.span_id.is_some() && ex.trace_id.is_none() {
return err("exemplar span_id requires trace_id");
}
}
match self.metric_type {
MetricType::Gauge | MetricType::Sum => {
if self.value.is_none() {
return err("Gauge/Sum requires value");
}
}
MetricType::Histogram => {
if self.count.is_none() {
return err("Histogram requires count");
}
if let (Some(bc), Some(eb)) = (&self.bucket_counts, &self.explicit_bounds)
&& bc.len() != eb.len() + 1
{
return err(
"Histogram: bucket_counts.len() must equal explicit_bounds.len() + 1",
);
}
if self.scale.is_some()
|| self.positive_buckets.is_some()
|| self.negative_buckets.is_some()
{
return err("Histogram must not carry exponential-histogram fields");
}
if self.quantile_values.is_some() {
return err("Histogram must not carry quantile_values");
}
}
MetricType::ExponentialHistogram => {
if self.count.is_none() {
return err("ExponentialHistogram requires count");
}
if self.scale.is_none() {
return err("ExponentialHistogram requires scale");
}
if self.bucket_counts.is_some() || self.explicit_bounds.is_some() {
return err(
"ExponentialHistogram must not carry explicit-boundary histogram fields",
);
}
if self.quantile_values.is_some() {
return err("ExponentialHistogram must not carry quantile_values");
}
}
MetricType::Summary => {
if self.count.is_none() {
return err("Summary requires count");
}
let qv = self.quantile_values.as_deref().unwrap_or(&[]);
if qv.is_empty() {
return err("Summary requires quantile_values");
}
for q in qv {
if !(0.0..=1.0).contains(&q.quantile) {
return err("Summary quantile must be in [0, 1]");
}
}
if self.bucket_counts.is_some() || self.explicit_bounds.is_some() {
return err("Summary must not carry explicit-boundary histogram fields");
}
}
}
Ok(())
}
}