1use crate::cache::LruCache;
4use crate::cached::{cache_handler, CacheState};
5use crate::config::DashboardConfig;
6use crate::pricing_cache::PricingCache;
7use crate::static_files;
8use axum::{
9 routing::{get, post},
10 Router,
11};
12use otelite_core::storage::StorageBackend;
13use serde::Serialize;
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16use tower_http::trace::TraceLayer;
17use tracing::info;
18use utoipa::OpenApi;
19
20#[derive(OpenApi)]
22#[openapi(
23 paths(
24 crate::api::health::health_check,
25 crate::api::stats::get_stats,
26 crate::api::admin::purge_all,
27 crate::api::help::api_help,
28 crate::api::logs::list_logs,
29 crate::api::logs::get_log,
30 crate::api::logs::export_logs,
31 crate::api::traces::list_traces,
32 crate::api::traces::get_trace,
33 crate::api::traces::get_trace_logs,
34 crate::api::traces::export_traces,
35 crate::api::metrics::list_metrics,
36 crate::api::metrics::list_metric_names,
37 crate::api::metrics::aggregate_metrics,
38 crate::api::metrics::get_metric_timeseries,
39 crate::api::metrics::export_metrics,
40 crate::api::genai::get_token_usage,
41 crate::api::genai::get_cost_series,
42 crate::api::genai::get_top_spans,
43 crate::api::genai::get_recent_retries,
44 crate::api::genai::get_top_sessions,
45 crate::api::genai::get_context_composition,
46 crate::api::genai::get_top_conversations,
47 crate::api::genai::get_finish_reasons,
48 crate::api::genai::get_distributions,
49 crate::api::genai::get_latency_stats,
50 crate::api::genai::get_latency_percentiles,
51 crate::api::genai::get_genai_capabilities,
52 crate::api::genai::get_model_performance,
53 crate::api::genai::get_error_rate,
54 crate::api::genai::get_tool_usage,
55 crate::api::genai::get_retry_stats,
56 crate::api::genai::get_retrieval_stats,
57 crate::api::genai::get_pricing_metadata,
58 crate::api::genai::get_agent_framework_defs,
59 crate::api::genai::get_truncation_rate,
60 crate::api::genai::get_cache_hit_rate,
61 crate::api::genai::get_agent_roles,
62 crate::api::genai::get_reasoning_share,
63 crate::api::genai::get_agents,
64 crate::api::genai::get_projects,
65 crate::api::genai::get_provider_mix,
66 crate::api::genai::get_request_param_profile,
67 crate::api::genai::get_conversation_depth,
68 crate::api::genai::get_latency_series,
69 crate::api::genai::get_calls_series,
70 crate::api::genai::get_latency_by_context,
71 crate::api::genai::get_error_types,
72 crate::api::genai::get_model_drift,
73 crate::api::genai::get_tool_approvals,
74 crate::api::genai::get_stop_reasons,
75 crate::api::genai::get_context_type_split,
76 crate::api::genai::get_tool_errors,
77 crate::api::genai::get_hour_of_day,
78 crate::api::genai::get_effort_breakdown,
79 crate::api::genai::get_efficiency_stats,
80 crate::api::genai::get_codex_ttft,
81 crate::api::genai::get_agent_project_rollup,
82 crate::api::genai::get_mcp_health,
83 crate::api::genai::get_guardian_stats,
84 crate::api::genai::get_multi_agent_stats,
85 crate::api::genai::get_codex_turn_breakdown,
86 crate::api::genai::get_codex_idle_ratio,
87 crate::api::genai::get_session_duration,
88 crate::api::genai::get_loc_efficiency,
89 crate::api::genai::get_rare_tool_sessions,
90 crate::api::genai::get_human_response_latency,
91 crate::api::genai::get_session_model_breakdown,
92 crate::api::genai::get_speed_distribution,
93 crate::api::genai::get_cross_tool_ttft,
94 crate::api::genai::get_hook_overhead,
95 crate::api::genai::get_bob_hook_overhead,
96 crate::api::genai::get_tool_switch_overhead,
97 crate::api::genai::get_tool_failure_rates,
98 crate::api::genai::get_daily_tool_mix,
99 crate::api::genai::get_productivity,
100 crate::api::genai::get_cost_projection,
101 crate::api::genai::get_cost_by_project,
102 crate::api::genai::get_session_depth_cost,
103 crate::api::genai::get_time_in_tool,
104 crate::api::genai::get_skill_activity,
105 crate::api::genai::get_skill_outcomes,
106 crate::api::genai::get_model_selection_heatmap,
107 crate::api::genai::get_session_quality_summary,
108 crate::api::genai::get_recent_errors,
109 crate::api::sessions::get_session_costs,
110 crate::api::sessions::get_session_cost_distribution,
111 crate::api::sessions::get_session_context,
112 crate::api::sessions::get_session_chains,
113 ),
114 components(
115 schemas(
116 otelite_core::api::ErrorResponse,
117 otelite_core::api::LogsResponse,
118 otelite_core::api::LogEntry,
119 otelite_core::api::Resource,
120 otelite_core::api::TracesResponse,
121 otelite_core::api::TraceEntry,
122 otelite_core::api::TraceDetail,
123 otelite_core::api::SpanEntry,
124 otelite_core::api::SpanStatus,
125 otelite_core::api::SpanEvent,
126 otelite_core::api::MetricResponse,
127 otelite_core::api::TokenUsageResponse,
128 otelite_core::api::TokenUsageSummary,
129 otelite_core::api::ModelUsage,
130 otelite_core::api::SystemUsage,
131 otelite_core::api::CostSeriesPoint,
132 otelite_core::api::TopSpan,
133 otelite_core::api::FinishReasonCount,
134 otelite_core::api::LatencyStats,
135 otelite_core::api::LatencyPercentilePoint,
136 otelite_core::api::LatencyPercentileSeries,
137 otelite_core::api::LatencyPercentilesResponse,
138 otelite_core::api::DistributionBucket,
139 otelite_core::api::DistributionStats,
140 otelite_core::api::DistributionResponse,
141 otelite_core::api::GenAiMetricCapability,
142 otelite_core::api::GenAiCorrelationProvenance,
143 otelite_core::api::GenAiCapabilityReport,
144 otelite_core::api::GenAiCapabilityResponse,
145 otelite_core::api::ModelPerformanceWindow,
146 otelite_core::api::ModelPerformanceQuery,
147 otelite_core::api::ModelPerformanceResponse,
148 otelite_core::api::ModelPerformanceIdentity,
149 otelite_core::api::ModelPerformanceMetric,
150 otelite_core::api::ModelPerformanceSample,
151 otelite_core::api::ModelPerformancePercentile,
152 otelite_core::api::ModelPerformanceDelta,
153 otelite_core::api::ModelPerformanceCounts,
154 otelite_core::api::ModelPerformanceErrorRate,
155 otelite_core::api::ModelPerformanceErrorValue,
156 otelite_core::api::ModelPerformanceDiagnosis,
157 otelite_core::model_performance::ModelPerformanceAssessment,
158 otelite_core::model_performance::ModelPerformanceMetricAssessment,
159 otelite_core::model_performance::PerformanceMetricKind,
160 otelite_core::model_performance::PerformanceChangeClass,
161 otelite_core::model_performance::PerformanceConfidence,
162 otelite_core::model_performance::TtftTrust,
163 otelite_core::model_performance::WorkloadShiftEvidence,
164 crate::api::genai::ModelPerformanceQueryParams,
165 otelite_core::api::ErrorRateByModel,
166 otelite_core::api::ToolUsage,
167 otelite_core::api::RetryStats,
168 otelite_core::api::RetryIncident,
169 otelite_core::api::RetrievalStats,
170 otelite_core::api::TopRetrievalQuery,
171 crate::api::health::HealthResponse,
172 crate::api::stats::StatsResponse,
173 crate::api::admin::PurgeAllResponse,
174 crate::api::metrics::AggregateResponse,
175 crate::api::metrics::TimeBucket,
176 crate::api::metrics::TimeseriesQuery,
177 crate::api::genai::TokenUsageQuery,
178 crate::api::genai::CostSeriesQuery,
179 crate::api::genai::TopSpansQuery,
180 crate::api::genai::TopGroupQuery,
181 crate::api::genai::FinishReasonsQuery,
182 crate::api::genai::LatencyQuery,
183 crate::api::genai::ErrorRateQuery,
184 crate::api::genai::ToolUsageQuery,
185 crate::api::genai::RetryStatsQuery,
186 crate::api::genai::RecentRetriesQuery,
187 crate::api::genai::RetrievalStatsQuery,
188 crate::api::genai::PricingMetadata,
189 otelite_core::agent_frameworks::AgentFrameworkRecognizer,
190 otelite_core::api::ToolApprovalStats,
191 otelite_core::api::ToolApprovalEntry,
192 otelite_core::api::StopReasonCount,
193 otelite_core::api::ContextTypeSplit,
194 otelite_core::api::ToolErrorEntry,
195 otelite_core::api::HourOfDayBucket,
196 otelite_core::api::ReasoningShareResponse,
197 otelite_core::api::ReasoningShareByModel,
198 otelite_core::api::ReasoningEffortEntry,
199 otelite_core::api::SessionCost,
200 otelite_core::api::SessionCostResponse,
201 otelite_core::api::CostBucket,
202 otelite_core::api::CostDistributionResponse,
203 otelite_core::api::ProjectRollup,
204 otelite_core::api::ProjectTopModel,
205 otelite_core::api::ProjectRollupResponse,
206 otelite_core::api::SessionContextResponse,
207 otelite_core::api::SessionContextSession,
208 otelite_core::api::SessionContextSpan,
209 otelite_core::api::SessionContextLog,
210 otelite_core::api::SessionContextMetric,
211 otelite_core::api::SessionContextTimelineEvent,
212 crate::api::sessions::SessionContextQuery,
213 otelite_core::api::EffortBreakdownRow,
214 otelite_core::api::EffortBreakdownResponse,
215 otelite_core::api::EfficiencyStats,
216 otelite_core::api::AgentEfficiencyRow,
217 otelite_core::api::CodexTtftByModel,
218 otelite_core::api::CodexTtftResponse,
219 otelite_core::api::ProjectRollupEntry,
220 otelite_core::api::AgentProjectRollupResponse,
221 otelite_core::api::McpHealthEntry,
222 otelite_core::api::McpHealthResponse,
223 otelite_core::api::GuardianRiskLevelEntry,
224 otelite_core::api::GuardianActionEntry,
225 otelite_core::api::GuardianStatsResponse,
226 otelite_core::api::MultiAgentRoleEntry,
227 otelite_core::api::MultiAgentStatsResponse,
228 otelite_core::api::CodexTurnBreakdownRow,
229 otelite_core::api::CodexTurnBreakdownResponse,
230 otelite_core::api::SessionModelRow,
231 otelite_core::api::SessionModelBreakdown,
232 otelite_core::api::SpeedBucket,
233 otelite_core::api::SpeedDistribution,
234 otelite_core::api::CrossToolTtftRow,
235 otelite_core::api::CrossToolTtftResponse,
236 otelite_core::api::HookOverheadRow,
237 otelite_core::api::HookOverheadResponse,
238 otelite_core::api::ToolFailureRow,
239 otelite_core::api::ToolFailureRatesResponse,
240 otelite_core::api::DailyToolMixRow,
241 otelite_core::api::DailyToolMixResponse,
242 otelite_core::api::SkillActivityRow,
243 otelite_core::api::SkillActivityResponse,
244 otelite_core::api::SkillOutcomeRow,
245 otelite_core::api::SkillOutcomesResponse,
246 otelite_core::api::ModelSelectionRow,
247 otelite_core::api::ModelSelectionHeatmapResponse,
248 otelite_core::api::SessionQuality,
249 otelite_core::api::SessionQualitySummary,
250 otelite_core::api::RecentErrorRow,
251 otelite_core::api::RecentErrorsResponse,
252 crate::api::genai::RecentErrorsQuery,
253 )
254 ),
255 tags(
256 (name = "health", description = "Health check endpoints"),
257 (name = "stats", description = "Storage statistics endpoints"),
258 (name = "help", description = "API documentation and help"),
259 (name = "logs", description = "Log query and export endpoints"),
260 (name = "traces", description = "Trace query and export endpoints"),
261 (name = "metrics", description = "Metric query and aggregation endpoints"),
262 (name = "genai", description = "GenAI/LLM token usage and analytics endpoints"),
263 (name = "admin", description = "Administrative endpoints for data management")
264 ),
265 info(
266 title = "Otelite API",
267 version = "1.0.0",
268 description = "OpenTelemetry data query and visualization API",
269 contact(
270 name = "Otelite",
271 url = "https://github.com/yourusername/otelite"
272 )
273 )
274)]
275struct ApiDoc;
276
277#[derive(Clone)]
279pub struct AppState {
280 pub storage: Arc<dyn StorageBackend>,
281 pub cache: QueryCache,
282 pub cache_state: Arc<CacheState>,
284 pub pricing: PricingCache,
286 pub start_time: Arc<Instant>,
288 pub otlp_grpc_port: u16,
290 pub otlp_http_port: u16,
291}
292
293#[derive(Clone)]
295pub struct QueryCache {
296 pub logs: LruCache<String, String>,
298 pub traces: LruCache<String, String>,
300 pub metrics: LruCache<String, String>,
302}
303
304impl QueryCache {
305 pub fn new() -> Self {
307 let max_size = 100;
309 let ttl = Duration::from_secs(300);
310
311 Self {
312 logs: LruCache::new(max_size, ttl),
313 traces: LruCache::new(max_size, ttl),
314 metrics: LruCache::new(max_size, ttl),
315 }
316 }
317
318 pub fn make_key<T: Serialize>(params: &T) -> String {
320 serde_json::to_string(params).unwrap_or_default()
322 }
323
324 pub fn clear_all(&self) {
327 self.logs.clear();
328 self.traces.clear();
329 self.metrics.clear();
330 }
331}
332
333impl Default for QueryCache {
334 fn default() -> Self {
335 Self::new()
336 }
337}
338
339#[derive(Clone)]
343pub struct DashboardShutdown(Arc<tokio::sync::Notify>);
344
345impl DashboardShutdown {
346 pub fn trigger(&self) {
348 self.0.notify_waiters();
349 }
350}
351
352pub struct DashboardServer {
353 config: Arc<DashboardConfig>,
354 state: AppState,
355 shutdown_notify: Arc<tokio::sync::Notify>,
356}
357
358impl DashboardServer {
359 pub fn new(config: DashboardConfig, storage: Arc<dyn StorageBackend>) -> Self {
362 Self::with_pricing_cache(config, storage, PricingCache::new().spawn_refresher())
363 }
364
365 pub fn with_pricing_cache(
372 config: DashboardConfig,
373 storage: Arc<dyn StorageBackend>,
374 pricing: PricingCache,
375 ) -> Self {
376 let state = AppState {
377 storage,
378 cache: QueryCache::new(),
379 cache_state: CacheState::new(),
380 pricing,
381 start_time: Arc::new(Instant::now()),
382 otlp_grpc_port: config.otlp_grpc_port,
383 otlp_http_port: config.otlp_http_port,
384 };
385
386 Self {
387 config: Arc::new(config),
388 state,
389 shutdown_notify: Arc::new(tokio::sync::Notify::new()),
390 }
391 }
392
393 pub fn shutdown_handle(&self) -> DashboardShutdown {
395 DashboardShutdown(Arc::clone(&self.shutdown_notify))
396 }
397
398 pub fn build_router(&self) -> Router {
400 let api = Router::new()
401 .route("/api/health", get(crate::api::health_check))
403 .route("/api/help", get(crate::api::api_help))
405 .route("/api/logs", get(crate::api::logs::list_logs))
407 .route("/api/logs/export", get(crate::api::logs::export_logs))
408 .route("/api/logs/{timestamp}", get(crate::api::logs::get_log))
409 .route("/api/traces", get(crate::api::traces::list_traces))
411 .route("/api/traces/export", get(crate::api::traces::export_traces))
412 .route("/api/traces/{trace_id}", get(crate::api::traces::get_trace))
413 .route("/api/traces/{trace_id}/logs", get(crate::api::traces::get_trace_logs))
414 .route("/api/metrics", get(crate::api::metrics::list_metrics))
416 .route("/api/metrics/names", get(crate::api::metrics::list_metric_names))
417 .route("/api/metrics/aggregate", get(crate::api::metrics::aggregate_metrics))
418 .route("/api/metrics/{name}/timeseries", get(crate::api::metrics::get_metric_timeseries))
419 .route("/api/metrics/export", get(crate::api::metrics::export_metrics))
420 .route("/api/resource-keys", get(crate::api::resource_keys::get_resource_keys))
422 .route("/api/stats", get(crate::api::stats::get_stats))
424 .route("/api/admin/purge", post(crate::api::admin::purge_all))
426 .route("/api/genai/usage", get(crate::api::get_token_usage))
428 .route("/api/genai/cost_series", get(crate::api::genai::get_cost_series))
429 .route("/api/genai/top_spans", get(crate::api::genai::get_top_spans))
430 .route("/api/genai/retries", get(crate::api::genai::get_recent_retries))
431 .route("/api/genai/top_sessions", get(crate::api::genai::get_top_sessions))
432 .route(
433 "/api/genai/context_composition",
434 get(crate::api::genai::get_context_composition),
435 )
436 .route("/api/genai/top_conversations", get(crate::api::genai::get_top_conversations))
437 .route("/api/genai/finish_reasons", get(crate::api::genai::get_finish_reasons))
438 .route("/api/genai/latency_stats", get(crate::api::genai::get_latency_stats))
439 .route(
440 "/api/genai/latency_percentiles",
441 get(crate::api::genai::get_latency_percentiles),
442 )
443 .route("/api/genai/distributions", get(crate::api::genai::get_distributions))
444 .route("/api/genai/capabilities", get(crate::api::genai::get_genai_capabilities))
445 .route(
446 "/api/genai/model-performance",
447 get(crate::api::genai::get_model_performance),
448 )
449 .route("/api/genai/error_rate", get(crate::api::genai::get_error_rate))
450 .route("/api/genai/tool_usage", get(crate::api::genai::get_tool_usage))
451 .route("/api/genai/retry_stats", get(crate::api::genai::get_retry_stats))
452 .route("/api/genai/retrieval_stats", get(crate::api::genai::get_retrieval_stats))
453 .route("/api/genai/pricing_metadata", get(crate::api::genai::get_pricing_metadata))
454 .route("/api/genai/agent_framework_defs", get(crate::api::genai::get_agent_framework_defs))
455 .route("/api/genai/truncation_rate", get(crate::api::genai::get_truncation_rate))
456 .route("/api/genai/cache_hit_rate", get(crate::api::genai::get_cache_hit_rate))
457 .route("/api/genai/agent_roles", get(crate::api::genai::get_agent_roles))
458 .route(
459 "/api/genai/reasoning_share",
460 get(crate::api::genai::get_reasoning_share),
461 )
462 .route("/api/genai/agents", get(crate::api::genai::get_agents))
463 .route("/api/genai/projects", get(crate::api::genai::get_projects))
464 .route("/api/genai/provider_mix", get(crate::api::genai::get_provider_mix))
465 .route("/api/genai/request_param_profile", get(crate::api::genai::get_request_param_profile))
466 .route("/api/genai/conversation_depth", get(crate::api::genai::get_conversation_depth))
467 .route("/api/genai/latency_series", get(crate::api::genai::get_latency_series))
468 .route("/api/genai/calls_series", get(crate::api::genai::get_calls_series))
469 .route("/api/genai/latency_by_context", get(crate::api::genai::get_latency_by_context))
470 .route("/api/genai/error_types", get(crate::api::genai::get_error_types))
471 .route("/api/genai/model_drift", get(crate::api::genai::get_model_drift))
472 .route("/api/genai/tool_approvals", get(crate::api::genai::get_tool_approvals))
473 .route("/api/genai/stop_reasons", get(crate::api::genai::get_stop_reasons))
474 .route("/api/genai/context_type_split", get(crate::api::genai::get_context_type_split))
475 .route("/api/genai/tool_errors", get(crate::api::genai::get_tool_errors))
476 .route("/api/genai/hour_of_day", get(crate::api::genai::get_hour_of_day))
477 .route("/api/genai/effort_breakdown", get(crate::api::genai::get_effort_breakdown))
478 .route("/api/genai/efficiency_stats", get(crate::api::genai::get_efficiency_stats))
479 .route("/api/genai/codex_ttft", get(crate::api::genai::get_codex_ttft))
480 .route("/api/genai/project_rollup", get(crate::api::genai::get_agent_project_rollup))
481 .route("/api/genai/mcp_health", get(crate::api::genai::get_mcp_health))
482 .route("/api/genai/guardian_stats", get(crate::api::genai::get_guardian_stats))
483 .route("/api/genai/multi_agent_stats", get(crate::api::genai::get_multi_agent_stats))
484 .route("/api/genai/codex_turn_breakdown", get(crate::api::genai::get_codex_turn_breakdown))
485 .route("/api/genai/codex_idle_ratio", get(crate::api::genai::get_codex_idle_ratio))
486 .route("/api/genai/session_duration", get(crate::api::genai::get_session_duration))
487 .route("/api/genai/loc_efficiency", get(crate::api::genai::get_loc_efficiency))
488 .route(
489 "/api/genai/rare_tool_sessions",
490 get(crate::api::genai::get_rare_tool_sessions),
491 )
492 .route(
493 "/api/genai/human_response_latency",
494 get(crate::api::genai::get_human_response_latency),
495 )
496 .route("/api/genai/session_model_breakdown", get(crate::api::genai::get_session_model_breakdown))
497 .route("/api/genai/speed_distribution", get(crate::api::genai::get_speed_distribution))
498 .route("/api/genai/cross_tool_ttft", get(crate::api::genai::get_cross_tool_ttft))
499 .route("/api/genai/hook_overhead", get(crate::api::genai::get_hook_overhead))
500 .route("/api/genai/bob_hook_overhead", get(crate::api::genai::get_bob_hook_overhead))
501 .route("/api/genai/tool_switch_overhead", get(crate::api::genai::get_tool_switch_overhead))
502 .route("/api/genai/tool_failure_rates", get(crate::api::genai::get_tool_failure_rates))
503 .route("/api/genai/daily_tool_mix", get(crate::api::genai::get_daily_tool_mix))
504 .route("/api/genai/productivity", get(crate::api::genai::get_productivity))
505 .route("/api/genai/cost_projection", get(crate::api::genai::get_cost_projection))
506 .route("/api/genai/cost_by_project", get(crate::api::genai::get_cost_by_project))
507 .route("/api/genai/session_depth_cost", get(crate::api::genai::get_session_depth_cost))
508 .route("/api/genai/time_in_tool", get(crate::api::genai::get_time_in_tool))
509 .route("/api/genai/skill_activity", get(crate::api::genai::get_skill_activity))
510 .route("/api/genai/skill_outcomes", get(crate::api::genai::get_skill_outcomes))
511 .route("/api/genai/model_selection_heatmap", get(crate::api::genai::get_model_selection_heatmap))
512 .route("/api/genai/session_quality_summary", get(crate::api::genai::get_session_quality_summary))
513 .route("/api/genai/recent_errors", get(crate::api::genai::get_recent_errors))
514 .route("/api/sessions", get(crate::api::sessions::list_sessions))
516 .route("/api/sessions/costs", get(crate::api::sessions::get_session_costs))
517 .route(
518 "/api/sessions/cost-distribution",
519 get(crate::api::sessions::get_session_cost_distribution),
520 )
521 .route("/api/sessions/{session_id}/diagnose", get(crate::api::sessions::get_session_diagnose))
522 .route(
523 "/api/sessions/{session_id}/context",
524 get(crate::api::sessions::get_session_context),
525 )
526 .route("/api/sessions/chains", get(crate::api::sessions::get_session_chains))
527 .route("/api/openapi.json", get(|| async {
529 axum::Json(ApiDoc::openapi())
530 }))
531 .with_state(self.state.clone());
533
534 api
535 .layer(axum::middleware::from_fn_with_state(
539 Arc::clone(&self.state.cache_state),
540 cache_handler,
541 ))
542 .fallback(static_files::serve_static_file)
544 .layer(TraceLayer::new_for_http())
546 }
547
548 pub async fn start(self) -> Result<(), Box<dyn std::error::Error>> {
551 let addr = self.config.bind_address;
552 let router = self.build_router();
553
554 info!("Starting dashboard server on {}", addr);
555
556 let listener = tokio::net::TcpListener::bind(addr).await?;
557 let shutdown_notify = Arc::clone(&self.shutdown_notify);
558 axum::serve(listener, router)
559 .with_graceful_shutdown(async move {
560 shutdown_notify.notified().await;
561 info!("Shutting down dashboard server");
562 })
563 .await?;
564
565 Ok(())
566 }
567}
568
569#[cfg(test)]
570mod server_shutdown_tests {
571 use super::*;
572 use otelite_storage::sqlite::SqliteBackend;
573 use otelite_storage::StorageConfig;
574 use tempfile::TempDir;
575
576 #[tokio::test]
580 async fn test_start_returns_on_shutdown_trigger() {
581 let temp = TempDir::new().expect("temp dir");
582 let config = StorageConfig::default().with_data_dir(temp.path().join("otelite.db"));
583 let mut storage = SqliteBackend::new(config);
584 storage.initialize().await.expect("storage initializes");
585 let storage: Arc<dyn StorageBackend> = Arc::new(storage);
586
587 let port = {
588 let l = tokio::net::TcpListener::bind("127.0.0.1:0")
589 .await
590 .expect("bind free port");
591 l.local_addr().expect("local addr").port()
592 };
593 let dashboard = DashboardServer::new(
594 DashboardConfig::default()
595 .with_bind_address(format!("127.0.0.1:{port}").parse().unwrap()),
596 storage,
597 );
598 let handle = dashboard.shutdown_handle();
599 let serve = tokio::spawn(async move { dashboard.start().await.map_err(|e| e.to_string()) });
600
601 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
603 handle.trigger();
604
605 let result = tokio::time::timeout(std::time::Duration::from_secs(5), serve)
606 .await
607 .expect("start() must return after the shutdown trigger")
608 .expect("serve task must not panic");
609 result.expect("a clean shutdown is not a server error");
610 }
611}