1async fn dashboard_home(State(_state): State<DashboardState>) -> Html<&'static str> {
2 Html(include_str!("dashboard.html"))
3}
4
5async fn get_metrics(
6 State(state): State<DashboardState>,
7) -> Result<Json<DashboardMetrics>, StatusCode> {
8 let health = state.observability.health_status();
9 let system_metrics = SystemMetrics {
10 memory_usage: 1024.0,
11 cpu_usage: 0.5,
12 uptime: 3600,
13 cache_hit_rate: 0.95,
14 cache_size: 512.0,
15 };
16 let application_metrics = ApplicationMetrics {
17 db_operations_total: 1000,
18 tasks_created_total: 50,
19 tasks_updated_total: 25,
20 tasks_deleted_total: 5,
21 tasks_completed_total: 30,
22 search_operations_total: 200,
23 export_operations_total: 10,
24 errors_total: 2,
25 };
26 let log_statistics = LogStatistics {
27 total_entries: 1000,
28 level_counts: HashMap::new(),
29 target_counts: HashMap::new(),
30 recent_errors: Vec::new(),
31 };
32 let metrics = DashboardMetrics {
33 health,
34 system_metrics,
35 application_metrics,
36 log_statistics,
37 };
38 Ok(Json(metrics))
39}
40
41async fn get_health(State(state): State<DashboardState>) -> Result<Json<HealthStatus>, StatusCode> {
42 let health = state.observability.health_status();
43 Ok(Json(health))
44}
45
46async fn get_logs(State(_state): State<DashboardState>) -> Result<Json<Vec<LogEntry>>, StatusCode> {
47 let logs = vec![
49 LogEntry {
50 timestamp: "2024-01-01T00:00:00Z".to_string(),
51 level: "INFO".to_string(),
52 target: "things3_cli".to_string(),
53 message: "Application started".to_string(),
54 },
55 LogEntry {
56 timestamp: "2024-01-01T00:01:00Z".to_string(),
57 level: "DEBUG".to_string(),
58 target: "things3_cli::database".to_string(),
59 message: "Database connection established".to_string(),
60 },
61 LogEntry {
62 timestamp: "2024-01-01T00:02:00Z".to_string(),
63 level: "WARN".to_string(),
64 target: "things3_cli::metrics".to_string(),
65 message: "High memory usage detected".to_string(),
66 },
67 ];
68 Ok(Json(logs))
69}
70
71async fn search_logs(
72 State(_state): State<DashboardState>,
73 Json(_query): Json<LogSearchQuery>,
74) -> Result<Json<Vec<LogEntry>>, StatusCode> {
75 let logs = vec![LogEntry {
77 timestamp: "2024-01-01T00:00:00Z".to_string(),
78 level: "INFO".to_string(),
79 target: "things3_cli".to_string(),
80 message: "Application started".to_string(),
81 }];
82 Ok(Json(logs))
83}
84
85async fn get_system_info(
86 State(_state): State<DashboardState>,
87) -> Result<Json<SystemInfo>, StatusCode> {
88 let system_info = SystemInfo {
90 os: std::env::consts::OS.to_string(),
91 arch: std::env::consts::ARCH.to_string(),
92 version: env!("CARGO_PKG_VERSION").to_string(),
93 rust_version: std::env::var("RUSTC_SEMVER").unwrap_or_else(|_| "unknown".to_string()),
94 };
95
96 Ok(Json(system_info))
97}
98
99use axum::{
100 extract::State,
101 http::StatusCode,
102 response::{Html, Json},
103 routing::{get, post},
104 Router,
105};
106use serde::{Deserialize, Serialize};
107use std::collections::HashMap;
108use std::sync::Arc;
109use things3_core::{HealthStatus, ObservabilityManager, ThingsDatabase};
110use tokio::net::TcpListener;
111use tower_http::cors::CorsLayer;
112use tracing::{info, instrument};
113
114#[derive(Clone)]
117pub struct DashboardState {
118 pub observability: Arc<ObservabilityManager>,
119 pub database: Arc<ThingsDatabase>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct DashboardMetrics {
125 pub health: HealthStatus,
126 pub system_metrics: SystemMetrics,
127 pub application_metrics: ApplicationMetrics,
128 pub log_statistics: LogStatistics,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct SystemMetrics {
133 pub memory_usage: f64,
134 pub cpu_usage: f64,
135 pub uptime: u64,
136 pub cache_hit_rate: f64,
137 pub cache_size: f64,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ApplicationMetrics {
142 pub db_operations_total: u64,
143 pub tasks_created_total: u64,
144 pub tasks_updated_total: u64,
145 pub tasks_deleted_total: u64,
146 pub tasks_completed_total: u64,
147 pub search_operations_total: u64,
148 pub export_operations_total: u64,
149 pub errors_total: u64,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct LogStatistics {
154 pub total_entries: u64,
155 pub level_counts: HashMap<String, u64>,
156 pub target_counts: HashMap<String, u64>,
157 pub recent_errors: Vec<LogEntry>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct LogEntry {
162 pub timestamp: String,
163 pub level: String,
164 pub target: String,
165 pub message: String,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct LogSearchQuery {
170 pub query: String,
171 pub level: Option<String>,
172 pub start_time: Option<String>,
173 pub end_time: Option<String>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct SystemInfo {
179 pub os: String,
180 pub arch: String,
181 pub version: String,
182 pub rust_version: String,
183}
184
185impl DashboardServer {
186 #[must_use]
188 pub fn new(
189 port: u16,
190 observability: Arc<ObservabilityManager>,
191 database: Arc<ThingsDatabase>,
192 ) -> Self {
193 Self {
194 port,
195 observability,
196 database,
197 }
198 }
199
200 #[instrument(skip(self))]
205 pub async fn start(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
206 let state = DashboardState {
207 observability: self.observability,
208 database: self.database,
209 };
210
211 let app = Router::new()
212 .route("/", get(dashboard_home))
213 .route("/metrics", get(get_metrics))
214 .route("/health", get(get_health))
215 .route("/logs", get(get_logs))
216 .route("/logs/search", post(search_logs))
217 .route("/system", get(get_system_info))
218 .layer(CorsLayer::permissive())
219 .with_state(state);
220
221 let listener = TcpListener::bind(format!("0.0.0.0:{}", self.port)).await?;
222 info!("Dashboard server running on port {}", self.port);
223
224 axum::serve(listener, app).await?;
225 Ok(())
226 }
227}
228
229pub struct DashboardServer {
231 port: u16,
232 observability: Arc<ObservabilityManager>,
233 database: Arc<ThingsDatabase>,
234}
235
236#[instrument(skip(observability, database))]
241pub async fn start_dashboard_server(
242 port: u16,
243 observability: Arc<ObservabilityManager>,
244 database: Arc<ThingsDatabase>,
245) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
246 let server = DashboardServer::new(port, observability, database);
247 server.start().await
248}
249
250#[cfg(test)]
251#[allow(deprecated)]
252mod tests {
253 use super::*;
254 use tempfile::NamedTempFile;
255
256 #[test]
257 fn test_dashboard_server_creation() {
258 let temp_file = NamedTempFile::new().unwrap();
259 let db_path = temp_file.path();
260
261 let config = things3_core::ThingsConfig::new(db_path, false);
262 let rt = tokio::runtime::Runtime::new().unwrap();
263 let database = Arc::new(
264 rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
265 );
266
267 let observability = Arc::new(
268 things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
269 .unwrap(),
270 );
271 let server = DashboardServer::new(8080, observability, database);
272 assert_eq!(server.port, 8080);
273 }
274
275 #[test]
276 fn test_dashboard_metrics() {
277 let metrics = DashboardMetrics {
278 health: HealthStatus {
279 status: "healthy".to_string(),
280 timestamp: chrono::Utc::now(),
281 uptime: std::time::Duration::from_secs(3600),
282 version: env!("CARGO_PKG_VERSION").to_string(),
283 checks: std::collections::HashMap::new(),
284 },
285 system_metrics: SystemMetrics {
286 memory_usage: 1024.0,
287 cpu_usage: 0.5,
288 uptime: 3600,
289 cache_hit_rate: 0.95,
290 cache_size: 512.0,
291 },
292 application_metrics: ApplicationMetrics {
293 db_operations_total: 1000,
294 tasks_created_total: 50,
295 tasks_updated_total: 25,
296 tasks_deleted_total: 5,
297 tasks_completed_total: 30,
298 search_operations_total: 200,
299 export_operations_total: 10,
300 errors_total: 2,
301 },
302 log_statistics: LogStatistics {
303 total_entries: 1000,
304 level_counts: HashMap::new(),
305 target_counts: HashMap::new(),
306 recent_errors: Vec::new(),
307 },
308 };
309
310 assert!((metrics.system_metrics.memory_usage - 1024.0).abs() < f64::EPSILON);
311 assert_eq!(metrics.application_metrics.db_operations_total, 1000);
312 }
313
314 #[test]
315 fn test_system_metrics_creation() {
316 let system_metrics = SystemMetrics {
317 memory_usage: 2048.0,
318 cpu_usage: 0.75,
319 uptime: 7200,
320 cache_hit_rate: 0.88,
321 cache_size: 1024.0,
322 };
323
324 assert!((system_metrics.memory_usage - 2048.0).abs() < f64::EPSILON);
325 assert!((system_metrics.cpu_usage - 0.75).abs() < f64::EPSILON);
326 assert_eq!(system_metrics.uptime, 7200);
327 assert!((system_metrics.cache_hit_rate - 0.88).abs() < f64::EPSILON);
328 assert!((system_metrics.cache_size - 1024.0).abs() < f64::EPSILON);
329 }
330
331 #[test]
332 fn test_application_metrics_creation() {
333 let app_metrics = ApplicationMetrics {
334 db_operations_total: 5000,
335 tasks_created_total: 100,
336 tasks_updated_total: 50,
337 tasks_deleted_total: 10,
338 tasks_completed_total: 80,
339 search_operations_total: 500,
340 export_operations_total: 25,
341 errors_total: 5,
342 };
343
344 assert_eq!(app_metrics.db_operations_total, 5000);
345 assert_eq!(app_metrics.tasks_created_total, 100);
346 assert_eq!(app_metrics.tasks_updated_total, 50);
347 assert_eq!(app_metrics.tasks_deleted_total, 10);
348 assert_eq!(app_metrics.tasks_completed_total, 80);
349 assert_eq!(app_metrics.search_operations_total, 500);
350 assert_eq!(app_metrics.export_operations_total, 25);
351 assert_eq!(app_metrics.errors_total, 5);
352 }
353
354 #[test]
355 fn test_log_statistics_creation() {
356 let mut level_counts = HashMap::new();
357 level_counts.insert("INFO".to_string(), 100);
358 level_counts.insert("ERROR".to_string(), 5);
359 level_counts.insert("WARN".to_string(), 10);
360
361 let mut target_counts = HashMap::new();
362 target_counts.insert("things3_cli".to_string(), 80);
363 target_counts.insert("things3_cli::database".to_string(), 20);
364
365 let recent_errors = vec![LogEntry {
366 timestamp: "2024-01-01T00:00:00Z".to_string(),
367 level: "ERROR".to_string(),
368 target: "things3_cli".to_string(),
369 message: "Database connection failed".to_string(),
370 }];
371
372 let log_stats = LogStatistics {
373 total_entries: 115,
374 level_counts,
375 target_counts,
376 recent_errors,
377 };
378
379 assert_eq!(log_stats.total_entries, 115);
380 assert_eq!(log_stats.level_counts.get("INFO"), Some(&100));
381 assert_eq!(log_stats.level_counts.get("ERROR"), Some(&5));
382 assert_eq!(log_stats.level_counts.get("WARN"), Some(&10));
383 assert_eq!(log_stats.target_counts.get("things3_cli"), Some(&80));
384 assert_eq!(log_stats.recent_errors.len(), 1);
385 }
386
387 #[test]
388 fn test_log_entry_creation() {
389 let log_entry = LogEntry {
390 timestamp: "2024-01-01T12:00:00Z".to_string(),
391 level: "DEBUG".to_string(),
392 target: "things3_cli::cache".to_string(),
393 message: "Cache miss for key: user_123".to_string(),
394 };
395
396 assert_eq!(log_entry.timestamp, "2024-01-01T12:00:00Z");
397 assert_eq!(log_entry.level, "DEBUG");
398 assert_eq!(log_entry.target, "things3_cli::cache");
399 assert_eq!(log_entry.message, "Cache miss for key: user_123");
400 }
401
402 #[test]
403 fn test_log_search_query_creation() {
404 let search_query = LogSearchQuery {
405 query: "database".to_string(),
406 level: Some("ERROR".to_string()),
407 start_time: Some("2024-01-01T00:00:00Z".to_string()),
408 end_time: Some("2024-01-01T23:59:59Z".to_string()),
409 };
410
411 assert_eq!(search_query.query, "database");
412 assert_eq!(search_query.level, Some("ERROR".to_string()));
413 assert_eq!(
414 search_query.start_time,
415 Some("2024-01-01T00:00:00Z".to_string())
416 );
417 assert_eq!(
418 search_query.end_time,
419 Some("2024-01-01T23:59:59Z".to_string())
420 );
421 }
422
423 #[test]
424 fn test_log_search_query_minimal() {
425 let search_query = LogSearchQuery {
426 query: "test".to_string(),
427 level: None,
428 start_time: None,
429 end_time: None,
430 };
431
432 assert_eq!(search_query.query, "test");
433 assert_eq!(search_query.level, None);
434 assert_eq!(search_query.start_time, None);
435 assert_eq!(search_query.end_time, None);
436 }
437
438 #[test]
439 fn test_system_info_creation() {
440 let system_info = SystemInfo {
441 os: "linux".to_string(),
442 arch: "x86_64".to_string(),
443 version: "1.0.0".to_string(),
444 rust_version: "1.70.0".to_string(),
445 };
446
447 assert_eq!(system_info.os, "linux");
448 assert_eq!(system_info.arch, "x86_64");
449 assert_eq!(system_info.version, "1.0.0");
450 assert_eq!(system_info.rust_version, "1.70.0");
451 }
452
453 #[test]
454 fn test_dashboard_state_creation() {
455 let temp_file = NamedTempFile::new().unwrap();
456 let db_path = temp_file.path();
457
458 let config = things3_core::ThingsConfig::new(db_path, false);
459 let rt = tokio::runtime::Runtime::new().unwrap();
460 let database = Arc::new(
461 rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
462 );
463
464 let observability = Arc::new(
465 things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
466 .unwrap(),
467 );
468
469 let state = DashboardState {
470 observability: observability.clone(),
471 database: database.clone(),
472 };
473
474 let cloned_state = state.clone();
476 assert!(Arc::ptr_eq(
477 &cloned_state.observability,
478 &state.observability
479 ));
480 assert!(Arc::ptr_eq(&cloned_state.database, &state.database));
481 }
482
483 #[test]
484 fn test_dashboard_metrics_serialization() {
485 let metrics = DashboardMetrics {
486 health: HealthStatus {
487 status: "healthy".to_string(),
488 timestamp: chrono::Utc::now(),
489 uptime: std::time::Duration::from_secs(3600),
490 version: "1.0.0".to_string(),
491 checks: HashMap::new(),
492 },
493 system_metrics: SystemMetrics {
494 memory_usage: 1024.0,
495 cpu_usage: 0.5,
496 uptime: 3600,
497 cache_hit_rate: 0.95,
498 cache_size: 512.0,
499 },
500 application_metrics: ApplicationMetrics {
501 db_operations_total: 1000,
502 tasks_created_total: 50,
503 tasks_updated_total: 25,
504 tasks_deleted_total: 5,
505 tasks_completed_total: 30,
506 search_operations_total: 200,
507 export_operations_total: 10,
508 errors_total: 2,
509 },
510 log_statistics: LogStatistics {
511 total_entries: 1000,
512 level_counts: HashMap::new(),
513 target_counts: HashMap::new(),
514 recent_errors: Vec::new(),
515 },
516 };
517
518 let json = serde_json::to_string(&metrics).unwrap();
520 assert!(json.contains("healthy"));
521 assert!(json.contains("1024.0"));
522 assert!(json.contains("1000"));
523
524 let deserialized: DashboardMetrics = serde_json::from_str(&json).unwrap();
526 assert_eq!(deserialized.health.status, "healthy");
527 assert!((deserialized.system_metrics.memory_usage - 1024.0).abs() < f64::EPSILON);
528 assert_eq!(deserialized.application_metrics.db_operations_total, 1000);
529 }
530
531 #[test]
532 fn test_system_metrics_serialization() {
533 let system_metrics = SystemMetrics {
534 memory_usage: 2048.0,
535 cpu_usage: 0.75,
536 uptime: 7200,
537 cache_hit_rate: 0.88,
538 cache_size: 1024.0,
539 };
540
541 let json = serde_json::to_string(&system_metrics).unwrap();
542 let deserialized: SystemMetrics = serde_json::from_str(&json).unwrap();
543
544 assert!((deserialized.memory_usage - 2048.0).abs() < f64::EPSILON);
545 assert!((deserialized.cpu_usage - 0.75).abs() < f64::EPSILON);
546 assert_eq!(deserialized.uptime, 7200);
547 assert!((deserialized.cache_hit_rate - 0.88).abs() < f64::EPSILON);
548 assert!((deserialized.cache_size - 1024.0).abs() < f64::EPSILON);
549 }
550
551 #[test]
552 fn test_application_metrics_serialization() {
553 let app_metrics = ApplicationMetrics {
554 db_operations_total: 5000,
555 tasks_created_total: 100,
556 tasks_updated_total: 50,
557 tasks_deleted_total: 10,
558 tasks_completed_total: 80,
559 search_operations_total: 500,
560 export_operations_total: 25,
561 errors_total: 5,
562 };
563
564 let json = serde_json::to_string(&app_metrics).unwrap();
565 let deserialized: ApplicationMetrics = serde_json::from_str(&json).unwrap();
566
567 assert_eq!(deserialized.db_operations_total, 5000);
568 assert_eq!(deserialized.tasks_created_total, 100);
569 assert_eq!(deserialized.tasks_updated_total, 50);
570 assert_eq!(deserialized.tasks_deleted_total, 10);
571 assert_eq!(deserialized.tasks_completed_total, 80);
572 assert_eq!(deserialized.search_operations_total, 500);
573 assert_eq!(deserialized.export_operations_total, 25);
574 assert_eq!(deserialized.errors_total, 5);
575 }
576
577 #[test]
578 fn test_log_entry_serialization() {
579 let log_entry = LogEntry {
580 timestamp: "2024-01-01T12:00:00Z".to_string(),
581 level: "DEBUG".to_string(),
582 target: "things3_cli::cache".to_string(),
583 message: "Cache miss for key: user_123".to_string(),
584 };
585
586 let json = serde_json::to_string(&log_entry).unwrap();
587 let deserialized: LogEntry = serde_json::from_str(&json).unwrap();
588
589 assert_eq!(deserialized.timestamp, "2024-01-01T12:00:00Z");
590 assert_eq!(deserialized.level, "DEBUG");
591 assert_eq!(deserialized.target, "things3_cli::cache");
592 assert_eq!(deserialized.message, "Cache miss for key: user_123");
593 }
594
595 #[test]
596 fn test_log_search_query_serialization() {
597 let search_query = LogSearchQuery {
598 query: "database".to_string(),
599 level: Some("ERROR".to_string()),
600 start_time: Some("2024-01-01T00:00:00Z".to_string()),
601 end_time: Some("2024-01-01T23:59:59Z".to_string()),
602 };
603
604 let json = serde_json::to_string(&search_query).unwrap();
605 let deserialized: LogSearchQuery = serde_json::from_str(&json).unwrap();
606
607 assert_eq!(deserialized.query, "database");
608 assert_eq!(deserialized.level, Some("ERROR".to_string()));
609 assert_eq!(
610 deserialized.start_time,
611 Some("2024-01-01T00:00:00Z".to_string())
612 );
613 assert_eq!(
614 deserialized.end_time,
615 Some("2024-01-01T23:59:59Z".to_string())
616 );
617 }
618
619 #[test]
620 fn test_system_info_serialization() {
621 let system_info = SystemInfo {
622 os: "linux".to_string(),
623 arch: "x86_64".to_string(),
624 version: "1.0.0".to_string(),
625 rust_version: "1.70.0".to_string(),
626 };
627
628 let json = serde_json::to_string(&system_info).unwrap();
629 let deserialized: SystemInfo = serde_json::from_str(&json).unwrap();
630
631 assert_eq!(deserialized.os, "linux");
632 assert_eq!(deserialized.arch, "x86_64");
633 assert_eq!(deserialized.version, "1.0.0");
634 assert_eq!(deserialized.rust_version, "1.70.0");
635 }
636
637 #[test]
638 fn test_dashboard_metrics_debug_formatting() {
639 let metrics = DashboardMetrics {
640 health: HealthStatus {
641 status: "healthy".to_string(),
642 timestamp: chrono::Utc::now(),
643 uptime: std::time::Duration::from_secs(3600),
644 version: "1.0.0".to_string(),
645 checks: HashMap::new(),
646 },
647 system_metrics: SystemMetrics {
648 memory_usage: 1024.0,
649 cpu_usage: 0.5,
650 uptime: 3600,
651 cache_hit_rate: 0.95,
652 cache_size: 512.0,
653 },
654 application_metrics: ApplicationMetrics {
655 db_operations_total: 1000,
656 tasks_created_total: 50,
657 tasks_updated_total: 25,
658 tasks_deleted_total: 5,
659 tasks_completed_total: 30,
660 search_operations_total: 200,
661 export_operations_total: 10,
662 errors_total: 2,
663 },
664 log_statistics: LogStatistics {
665 total_entries: 1000,
666 level_counts: HashMap::new(),
667 target_counts: HashMap::new(),
668 recent_errors: Vec::new(),
669 },
670 };
671
672 let debug_str = format!("{metrics:?}");
673 assert!(debug_str.contains("DashboardMetrics"));
674 assert!(debug_str.contains("SystemMetrics"));
675 assert!(debug_str.contains("ApplicationMetrics"));
676 assert!(debug_str.contains("LogStatistics"));
677 }
678
679 #[test]
680 fn test_dashboard_metrics_clone() {
681 let metrics = DashboardMetrics {
682 health: HealthStatus {
683 status: "healthy".to_string(),
684 timestamp: chrono::Utc::now(),
685 uptime: std::time::Duration::from_secs(3600),
686 version: "1.0.0".to_string(),
687 checks: HashMap::new(),
688 },
689 system_metrics: SystemMetrics {
690 memory_usage: 1024.0,
691 cpu_usage: 0.5,
692 uptime: 3600,
693 cache_hit_rate: 0.95,
694 cache_size: 512.0,
695 },
696 application_metrics: ApplicationMetrics {
697 db_operations_total: 1000,
698 tasks_created_total: 50,
699 tasks_updated_total: 25,
700 tasks_deleted_total: 5,
701 tasks_completed_total: 30,
702 search_operations_total: 200,
703 export_operations_total: 10,
704 errors_total: 2,
705 },
706 log_statistics: LogStatistics {
707 total_entries: 1000,
708 level_counts: HashMap::new(),
709 target_counts: HashMap::new(),
710 recent_errors: Vec::new(),
711 },
712 };
713
714 let cloned_metrics = metrics.clone();
715 assert_eq!(cloned_metrics.health.status, metrics.health.status);
716 assert!(
717 (cloned_metrics.system_metrics.memory_usage - metrics.system_metrics.memory_usage)
718 .abs()
719 < f64::EPSILON
720 );
721 assert_eq!(
722 cloned_metrics.application_metrics.db_operations_total,
723 metrics.application_metrics.db_operations_total
724 );
725 assert_eq!(
726 cloned_metrics.log_statistics.total_entries,
727 metrics.log_statistics.total_entries
728 );
729 }
730}