use crate::server::AppState;
use axum::{
extract::{Query, State},
http::StatusCode,
response::Json,
};
use otelite_core::api::{
CostSeriesPoint, ErrorRateByModel, ErrorResponse, FinishReasonCount, LatencyStats,
RetrievalStats, RetryStats, TokenUsageResponse, ToolUsage, TopSpan,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct TokenUsageQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/genai/usage",
params(TokenUsageQuery),
responses(
(status = 200, description = "Token usage summary", body = TokenUsageResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_token_usage(
State(state): State<AppState>,
Query(query): Query<TokenUsageQuery>,
) -> Result<Json<TokenUsageResponse>, (StatusCode, Json<ErrorResponse>)> {
let (summary, by_model, by_system) = state
.storage
.query_token_usage(query.start_time, query.end_time)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query token usage: {}",
e
))),
)
})?;
Ok(Json(TokenUsageResponse {
summary,
by_model,
by_system,
}))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct CostSeriesQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
pub bucket: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/genai/cost_series",
params(CostSeriesQuery),
responses(
(status = 200, description = "Cost series points", body = Vec<CostSeriesPoint>),
(status = 400, description = "Invalid bucket parameter", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_cost_series(
State(state): State<AppState>,
Query(query): Query<CostSeriesQuery>,
) -> Result<Json<Vec<CostSeriesPoint>>, (StatusCode, Json<ErrorResponse>)> {
let bucket_seconds = query.bucket.unwrap_or(3600);
if bucket_seconds <= 0 {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request(
"bucket must be a positive number of seconds",
)),
));
}
let bucket_ns = bucket_seconds.saturating_mul(1_000_000_000);
let series = state
.storage
.query_cost_series(query.start_time, query.end_time, bucket_ns)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query cost series: {}",
e
))),
)
})?;
Ok(Json(series))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct TopSpansQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
pub limit: Option<usize>,
}
#[utoipa::path(
get,
path = "/api/genai/top_spans",
params(TopSpansQuery),
responses(
(status = 200, description = "Top expensive spans", body = Vec<TopSpan>),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_top_spans(
State(state): State<AppState>,
Query(query): Query<TopSpansQuery>,
) -> Result<Json<Vec<TopSpan>>, (StatusCode, Json<ErrorResponse>)> {
let limit = query.limit.unwrap_or(20).clamp(1, 100);
let spans = state
.storage
.query_top_spans(query.start_time, query.end_time, limit)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query top spans: {}",
e
))),
)
})?;
Ok(Json(spans))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct FinishReasonsQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/genai/finish_reasons",
params(FinishReasonsQuery),
responses(
(status = 200, description = "Finish reason counts", body = Vec<FinishReasonCount>),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_finish_reasons(
State(state): State<AppState>,
Query(query): Query<FinishReasonsQuery>,
) -> Result<Json<Vec<FinishReasonCount>>, (StatusCode, Json<ErrorResponse>)> {
let rows = state
.storage
.query_finish_reasons(query.start_time, query.end_time)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query finish reasons: {}",
e
))),
)
})?;
Ok(Json(rows))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct LatencyQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/genai/latency_stats",
params(LatencyQuery),
responses(
(status = 200, description = "Latency statistics per model", body = Vec<LatencyStats>),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_latency_stats(
State(state): State<AppState>,
Query(query): Query<LatencyQuery>,
) -> Result<Json<Vec<LatencyStats>>, (StatusCode, Json<ErrorResponse>)> {
let rows = state
.storage
.query_latency_stats(query.start_time, query.end_time)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query latency stats: {}",
e
))),
)
})?;
Ok(Json(rows))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct ErrorRateQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/genai/error_rate",
params(ErrorRateQuery),
responses(
(status = 200, description = "Error rate per model", body = Vec<ErrorRateByModel>),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_error_rate(
State(state): State<AppState>,
Query(query): Query<ErrorRateQuery>,
) -> Result<Json<Vec<ErrorRateByModel>>, (StatusCode, Json<ErrorResponse>)> {
let rows = state
.storage
.query_error_rate(query.start_time, query.end_time)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query error rate: {}",
e
))),
)
})?;
Ok(Json(rows))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct ToolUsageQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
pub limit: Option<usize>,
}
#[utoipa::path(
get,
path = "/api/genai/tool_usage",
params(ToolUsageQuery),
responses(
(status = 200, description = "Tool usage aggregates", body = Vec<ToolUsage>),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_tool_usage(
State(state): State<AppState>,
Query(query): Query<ToolUsageQuery>,
) -> Result<Json<Vec<ToolUsage>>, (StatusCode, Json<ErrorResponse>)> {
let limit = query.limit.unwrap_or(20).clamp(1, 100);
let rows = state
.storage
.query_tool_usage(query.start_time, query.end_time, limit)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query tool usage: {}",
e
))),
)
})?;
Ok(Json(rows))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct RetryStatsQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/genai/retry_stats",
params(RetryStatsQuery),
responses(
(status = 200, description = "Retry statistics", body = RetryStats),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_retry_stats(
State(state): State<AppState>,
Query(query): Query<RetryStatsQuery>,
) -> Result<Json<RetryStats>, (StatusCode, Json<ErrorResponse>)> {
let stats = state
.storage
.query_retry_stats(query.start_time, query.end_time)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query retry stats: {}",
e
))),
)
})?;
Ok(Json(stats))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct RetrievalStatsQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
pub limit: Option<usize>,
}
#[utoipa::path(
get,
path = "/api/genai/retrieval_stats",
params(RetrievalStatsQuery),
responses(
(status = 200, description = "Retrieval statistics", body = RetrievalStats),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_retrieval_stats(
State(state): State<AppState>,
Query(query): Query<RetrievalStatsQuery>,
) -> Result<Json<RetrievalStats>, (StatusCode, Json<ErrorResponse>)> {
let limit = query.limit.unwrap_or(5).clamp(1, 20);
let stats = state
.storage
.query_retrieval_stats(query.start_time, query.end_time, limit)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query retrieval stats: {}",
e
))),
)
})?;
Ok(Json(stats))
}