use crate::cache::LruCache;
use crate::config::DashboardConfig;
use crate::static_files;
use axum::{
routing::{get, post},
Router,
};
use otelite_core::storage::StorageBackend;
use serde::Serialize;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tower_http::trace::TraceLayer;
use tracing::info;
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(
crate::api::health::health_check,
crate::api::stats::get_stats,
crate::api::admin::purge_all,
crate::api::help::api_help,
crate::api::logs::list_logs,
crate::api::logs::get_log,
crate::api::logs::export_logs,
crate::api::traces::list_traces,
crate::api::traces::get_trace,
crate::api::traces::export_traces,
crate::api::metrics::list_metrics,
crate::api::metrics::list_metric_names,
crate::api::metrics::aggregate_metrics,
crate::api::metrics::get_metric_timeseries,
crate::api::metrics::export_metrics,
crate::api::genai::get_token_usage,
),
components(
schemas(
otelite_core::api::ErrorResponse,
otelite_core::api::LogsResponse,
otelite_core::api::LogEntry,
otelite_core::api::Resource,
otelite_core::api::TracesResponse,
otelite_core::api::TraceEntry,
otelite_core::api::TraceDetail,
otelite_core::api::SpanEntry,
otelite_core::api::SpanStatus,
otelite_core::api::SpanEvent,
otelite_core::api::MetricResponse,
otelite_core::api::TokenUsageResponse,
otelite_core::api::TokenUsageSummary,
otelite_core::api::ModelUsage,
otelite_core::api::SystemUsage,
crate::api::health::HealthResponse,
crate::api::stats::StatsResponse,
crate::api::admin::PurgeAllResponse,
crate::api::metrics::AggregateResponse,
crate::api::metrics::TimeBucket,
crate::api::metrics::TimeseriesQuery,
crate::api::genai::TokenUsageQuery,
)
),
tags(
(name = "health", description = "Health check endpoints"),
(name = "stats", description = "Storage statistics endpoints"),
(name = "help", description = "API documentation and help"),
(name = "logs", description = "Log query and export endpoints"),
(name = "traces", description = "Trace query and export endpoints"),
(name = "metrics", description = "Metric query and aggregation endpoints"),
(name = "genai", description = "GenAI/LLM token usage and analytics endpoints"),
(name = "admin", description = "Administrative endpoints for data management")
),
info(
title = "Otelite API",
version = "1.0.0",
description = "OpenTelemetry data query and visualization API",
contact(
name = "Otelite",
url = "https://github.com/yourusername/otelite"
)
)
)]
struct ApiDoc;
#[derive(Clone)]
pub struct AppState {
pub storage: Arc<dyn StorageBackend>,
pub cache: QueryCache,
pub start_time: Arc<Instant>,
}
#[derive(Clone)]
pub struct QueryCache {
pub logs: LruCache<String, String>,
pub traces: LruCache<String, String>,
pub metrics: LruCache<String, String>,
}
impl QueryCache {
pub fn new() -> Self {
let max_size = 100;
let ttl = Duration::from_secs(300);
Self {
logs: LruCache::new(max_size, ttl),
traces: LruCache::new(max_size, ttl),
metrics: LruCache::new(max_size, ttl),
}
}
pub fn make_key<T: Serialize>(params: &T) -> String {
serde_json::to_string(params).unwrap_or_default()
}
}
impl Default for QueryCache {
fn default() -> Self {
Self::new()
}
}
pub struct DashboardServer {
config: Arc<DashboardConfig>,
state: AppState,
}
impl DashboardServer {
pub fn new(config: DashboardConfig, storage: Arc<dyn StorageBackend>) -> Self {
let state = AppState {
storage,
cache: QueryCache::new(),
start_time: Arc::new(Instant::now()),
};
Self {
config: Arc::new(config),
state,
}
}
pub fn build_router(&self) -> Router {
Router::new()
.route("/api/health", get(crate::api::health_check))
.route("/api/help", get(crate::api::api_help))
.route("/api/logs", get(crate::api::logs::list_logs))
.route("/api/logs/export", get(crate::api::logs::export_logs))
.route("/api/logs/{timestamp}", get(crate::api::logs::get_log))
.route("/api/traces", get(crate::api::traces::list_traces))
.route("/api/traces/export", get(crate::api::traces::export_traces))
.route("/api/traces/{trace_id}", get(crate::api::traces::get_trace))
.route("/api/metrics", get(crate::api::metrics::list_metrics))
.route("/api/metrics/names", get(crate::api::metrics::list_metric_names))
.route("/api/metrics/aggregate", get(crate::api::metrics::aggregate_metrics))
.route("/api/metrics/{name}/timeseries", get(crate::api::metrics::get_metric_timeseries))
.route("/api/metrics/export", get(crate::api::metrics::export_metrics))
.route("/api/resource-keys", get(crate::api::resource_keys::get_resource_keys))
.route("/api/stats", get(crate::api::stats::get_stats))
.route("/api/admin/purge", post(crate::api::admin::purge_all))
.route("/api/genai/usage", get(crate::api::get_token_usage))
.route("/api/openapi.json", get(|| async {
axum::Json(ApiDoc::openapi())
}))
.fallback(static_files::serve_static_file)
.with_state(self.state.clone())
.layer(TraceLayer::new_for_http())
}
pub async fn start(self) -> Result<(), Box<dyn std::error::Error>> {
let addr = self.config.bind_address;
let router = self.build_router();
info!("Starting dashboard server on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, router).await?;
Ok(())
}
}