use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ErrorResponse {
pub error: String,
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
}
impl ErrorResponse {
pub fn new(code: impl Into<String>, error: impl Into<String>) -> Self {
Self {
error: error.into(),
code: code.into(),
details: None,
}
}
pub fn with_details(
code: impl Into<String>,
error: impl Into<String>,
details: impl Into<String>,
) -> Self {
Self {
error: error.into(),
code: code.into(),
details: Some(details.into()),
}
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new("BAD_REQUEST", message)
}
pub fn not_found(resource: impl Into<String>) -> Self {
Self::new("NOT_FOUND", format!("{} not found", resource.into()))
}
pub fn internal_error(message: impl Into<String>) -> Self {
Self::new("INTERNAL_ERROR", message)
}
pub fn storage_error(operation: impl Into<String>) -> Self {
Self::with_details(
"STORAGE_ERROR",
format!("Storage operation failed: {}", operation.into()),
"Check storage configuration and disk space",
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct LogsResponse {
pub logs: Vec<LogEntry>,
pub total: usize,
pub limit: usize,
pub offset: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct LogEntry {
pub timestamp: i64,
pub severity: String,
pub severity_text: Option<String>,
pub body: String,
#[serde(default)]
pub attributes: HashMap<String, String>,
pub resource: Option<Resource>,
pub trace_id: Option<String>,
pub span_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct Resource {
pub attributes: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TracesResponse {
pub traces: Vec<TraceEntry>,
pub total: usize,
pub limit: usize,
pub offset: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TraceEntry {
pub trace_id: String,
pub root_span_name: String,
pub start_time: i64,
pub duration: i64,
pub span_count: usize,
pub service_names: Vec<String>,
pub has_errors: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TraceDetail {
pub trace_id: String,
pub spans: Vec<SpanEntry>,
pub start_time: i64,
pub end_time: i64,
pub duration: i64,
pub span_count: usize,
pub service_names: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SpanEntry {
pub span_id: String,
pub trace_id: String,
pub parent_span_id: Option<String>,
pub name: String,
pub kind: String,
pub start_time: i64,
pub end_time: i64,
pub duration: i64,
#[serde(default)]
pub attributes: HashMap<String, String>,
pub resource: Option<Resource>,
pub status: SpanStatus,
pub events: Vec<SpanEvent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SpanStatus {
pub code: String,
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SpanEvent {
pub name: String,
pub timestamp: i64,
#[serde(default)]
pub attributes: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct MetricResponse {
pub name: String,
pub description: Option<String>,
pub unit: Option<String>,
pub metric_type: String,
pub value: MetricValue,
pub timestamp: i64,
#[serde(default)]
pub attributes: HashMap<String, String>,
pub resource: Option<Resource>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MetricValue {
Gauge(f64),
Counter(i64),
Histogram(HistogramValue),
Summary(SummaryValue),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistogramValue {
pub sum: f64,
pub count: u64,
pub buckets: Vec<HistogramBucket>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistogramBucket {
pub upper_bound: f64,
pub count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SummaryValue {
pub sum: f64,
pub count: u64,
pub quantiles: Vec<Quantile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Quantile {
pub quantile: f64,
pub value: f64,
}
impl From<crate::telemetry::LogRecord> for LogEntry {
fn from(log: crate::telemetry::LogRecord) -> Self {
Self {
timestamp: log.timestamp,
severity: log.severity.as_str().to_string(),
severity_text: Some(log.severity.as_str().to_string()),
body: log.body,
attributes: log.attributes,
resource: log.resource.map(Resource::from),
trace_id: log.trace_id,
span_id: log.span_id,
}
}
}
impl From<crate::telemetry::Resource> for Resource {
fn from(resource: crate::telemetry::Resource) -> Self {
Self {
attributes: resource.attributes,
}
}
}
impl From<crate::telemetry::Span> for SpanEntry {
fn from(span: crate::telemetry::Span) -> Self {
use crate::telemetry::trace::{SpanKind, StatusCode};
let kind_str = match span.kind {
SpanKind::Internal => "Internal",
SpanKind::Server => "Server",
SpanKind::Client => "Client",
SpanKind::Producer => "Producer",
SpanKind::Consumer => "Consumer",
};
let status_code_str = match span.status.code {
StatusCode::Unset => "Unset",
StatusCode::Ok => "Ok",
StatusCode::Error => "Error",
};
Self {
span_id: span.span_id,
trace_id: span.trace_id,
parent_span_id: span.parent_span_id,
name: span.name,
kind: kind_str.to_string(),
start_time: span.start_time,
end_time: span.end_time,
duration: span.end_time - span.start_time,
attributes: span.attributes,
resource: span.resource.map(Resource::from),
status: SpanStatus {
code: status_code_str.to_string(),
message: span.status.message,
},
events: span
.events
.into_iter()
.map(|e| SpanEvent {
name: e.name,
timestamp: e.timestamp,
attributes: e.attributes,
})
.collect(),
}
}
}
impl From<crate::telemetry::Metric> for MetricResponse {
fn from(metric: crate::telemetry::Metric) -> Self {
use crate::telemetry::metric::MetricType;
let (metric_type_str, value) = match metric.metric_type {
MetricType::Gauge(v) => ("gauge", MetricValue::Gauge(v)),
MetricType::Counter(v) => ("counter", MetricValue::Counter(v as i64)),
MetricType::Histogram {
count,
sum,
buckets,
} => (
"histogram",
MetricValue::Histogram(HistogramValue {
sum,
count,
buckets: buckets
.into_iter()
.map(|b| HistogramBucket {
upper_bound: b.upper_bound,
count: b.count,
})
.collect(),
}),
),
MetricType::Summary {
count,
sum,
quantiles,
} => (
"summary",
MetricValue::Summary(SummaryValue {
sum,
count,
quantiles: quantiles
.into_iter()
.map(|q| Quantile {
quantile: q.quantile,
value: q.value,
})
.collect(),
}),
),
};
Self {
name: metric.name,
description: metric.description,
unit: metric.unit,
metric_type: metric_type_str.to_string(),
value,
timestamp: metric.timestamp,
attributes: metric.attributes,
resource: metric.resource.map(Resource::from),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_response_new() {
let err = ErrorResponse::new("TEST_ERROR", "Test error message");
assert_eq!(err.code, "TEST_ERROR");
assert_eq!(err.error, "Test error message");
assert!(err.details.is_none());
}
#[test]
fn test_error_response_with_details() {
let err =
ErrorResponse::with_details("TEST_ERROR", "Test error message", "Additional details");
assert_eq!(err.code, "TEST_ERROR");
assert_eq!(err.error, "Test error message");
assert_eq!(err.details, Some("Additional details".to_string()));
}
#[test]
fn test_error_response_bad_request() {
let err = ErrorResponse::bad_request("Invalid parameter");
assert_eq!(err.code, "BAD_REQUEST");
assert_eq!(err.error, "Invalid parameter");
}
#[test]
fn test_error_response_not_found() {
let err = ErrorResponse::not_found("Log entry");
assert_eq!(err.code, "NOT_FOUND");
assert_eq!(err.error, "Log entry not found");
}
#[test]
fn test_error_response_internal_error() {
let err = ErrorResponse::internal_error("Database connection failed");
assert_eq!(err.code, "INTERNAL_ERROR");
assert_eq!(err.error, "Database connection failed");
}
#[test]
fn test_error_response_storage_error() {
let err = ErrorResponse::storage_error("write");
assert_eq!(err.code, "STORAGE_ERROR");
assert!(err.error.contains("write"));
assert!(err.details.is_some());
}
#[test]
fn test_error_response_serialization() {
let err = ErrorResponse::with_details("TEST", "message", "details");
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("\"code\":\"TEST\""));
assert!(json.contains("\"error\":\"message\""));
assert!(json.contains("\"details\":\"details\""));
}
#[test]
fn test_error_response_deserialization() {
let json = r#"{"error":"test message","code":"TEST_CODE","details":"test details"}"#;
let err: ErrorResponse = serde_json::from_str(json).unwrap();
assert_eq!(err.code, "TEST_CODE");
assert_eq!(err.error, "test message");
assert_eq!(err.details, Some("test details".to_string()));
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TokenUsageResponse {
pub summary: TokenUsageSummary,
pub by_model: Vec<ModelUsage>,
pub by_system: Vec<SystemUsage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TokenUsageSummary {
pub total_input_tokens: u64,
pub total_output_tokens: u64,
pub total_requests: usize,
#[serde(default)]
pub total_cache_creation_tokens: u64,
#[serde(default)]
pub total_cache_read_tokens: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ModelUsage {
pub model: String,
pub input_tokens: u64,
pub output_tokens: u64,
pub requests: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SystemUsage {
pub system: String,
pub input_tokens: u64,
pub output_tokens: u64,
pub requests: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CostSeriesPoint {
pub timestamp: i64,
pub model: Option<String>,
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_creation_tokens: u64,
pub cache_read_tokens: u64,
pub requests: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_source: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum TopSpanSort {
#[default]
TotalTokens,
Duration,
OutputInputRatio,
CacheEfficiency,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TopSpan {
pub trace_id: String,
pub span_id: String,
pub start_time: i64,
pub duration: i64,
pub model: Option<String>,
pub system: Option<String>,
pub session_id: Option<String>,
pub prompt_id: Option<String>,
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_creation_tokens: u64,
pub cache_read_tokens: u64,
pub total_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub derived_output_tokens_per_sec: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SessionCostRow {
pub session_id: String,
pub request_count: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ConversationCostRow {
pub conversation_id: String,
pub request_count: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct FinishReasonCount {
pub reason: String,
pub count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct LatencyStats {
pub model: Option<String>,
pub count: usize,
pub avg_ms: f64,
pub p50_ms: i64,
pub p95_ms: i64,
pub p99_ms: i64,
pub ttft_count: usize,
pub ttft_p50_ms: Option<i64>,
pub ttft_p95_ms: Option<i64>,
pub ttft_p99_ms: Option<i64>,
pub derived_tokens_per_sec_p50: Option<f64>,
pub derived_tokens_per_sec_p95: Option<f64>,
pub derived_tokens_per_sec_p99: Option<f64>,
pub input_tokens_p50: Option<i64>,
pub input_tokens_p95: Option<i64>,
pub input_tokens_p99: Option<i64>,
pub output_input_ratio_p50: Option<f64>,
pub output_input_ratio_p95: Option<f64>,
pub output_input_ratio_p99: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ErrorRateByModel {
pub model: Option<String>,
pub total: usize,
pub errors: usize,
pub error_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ToolUsage {
pub tool_name: String,
pub count: usize,
pub success_count: usize,
pub error_count: usize,
pub avg_duration_ms: f64,
pub total_duration_ms: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RetryStats {
pub total_llm_calls: usize,
pub retried_calls: usize,
pub extra_attempts: usize,
pub retry_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RetrievalStats {
pub total_retrievals: usize,
pub avg_documents_per_query: f64,
pub avg_top_document_score: Option<f64>,
pub top_queries: Vec<TopRetrievalQuery>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TopRetrievalQuery {
pub query: String,
pub count: usize,
pub avg_documents: f64,
pub avg_top_score: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TruncationRateByModel {
pub model: Option<String>,
pub total: usize,
pub truncated: usize,
pub rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CacheHitRateByModel {
pub model: Option<String>,
pub total_input_tokens: u64,
pub total_cache_read_tokens: u64,
pub total_cache_creation_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hit_rate: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TemperatureBucket {
pub temperature: Option<f64>,
pub count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct MaxTokensBucket {
pub max_tokens: Option<i64>,
pub count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RequestParamProfile {
pub temperature_buckets: Vec<TemperatureBucket>,
pub max_tokens_buckets: Vec<MaxTokensBucket>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ConversationDepthStats {
pub total_conversations: usize,
pub avg_turns: f64,
pub p50_turns: i64,
pub p95_turns: i64,
pub p99_turns: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CallsSeriesPoint {
pub timestamp: i64,
pub model: Option<String>,
pub requests: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ErrorTypeBreakdown {
pub model: Option<String>,
pub error_type: String,
pub bucket: String,
pub count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ModelDriftPair {
pub request_model: Option<String>,
pub response_model: Option<String>,
pub count: usize,
pub differs: bool,
}