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)]
251mod tests {
252 use super::*;
253 use tempfile::NamedTempFile;
254
255 #[test]
256 fn test_dashboard_server_creation() {
257 let temp_file = NamedTempFile::new().unwrap();
258 let db_path = temp_file.path();
259
260 let config = things3_core::ThingsConfig::new(db_path, false);
261 let rt = tokio::runtime::Runtime::new().unwrap();
262 let database = Arc::new(
263 rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
264 );
265
266 let observability = Arc::new(
267 things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
268 .unwrap(),
269 );
270 let server = DashboardServer::new(8080, observability, database);
271 assert_eq!(server.port, 8080);
272 }
273
274 #[test]
275 fn test_dashboard_metrics() {
276 let metrics = DashboardMetrics {
277 health: HealthStatus {
278 status: "healthy".to_string(),
279 timestamp: chrono::Utc::now(),
280 uptime: std::time::Duration::from_secs(3600),
281 version: env!("CARGO_PKG_VERSION").to_string(),
282 checks: std::collections::HashMap::new(),
283 },
284 system_metrics: SystemMetrics {
285 memory_usage: 1024.0,
286 cpu_usage: 0.5,
287 uptime: 3600,
288 cache_hit_rate: 0.95,
289 cache_size: 512.0,
290 },
291 application_metrics: ApplicationMetrics {
292 db_operations_total: 1000,
293 tasks_created_total: 50,
294 tasks_updated_total: 25,
295 tasks_deleted_total: 5,
296 tasks_completed_total: 30,
297 search_operations_total: 200,
298 export_operations_total: 10,
299 errors_total: 2,
300 },
301 log_statistics: LogStatistics {
302 total_entries: 1000,
303 level_counts: HashMap::new(),
304 target_counts: HashMap::new(),
305 recent_errors: Vec::new(),
306 },
307 };
308
309 assert!((metrics.system_metrics.memory_usage - 1024.0).abs() < f64::EPSILON);
310 assert_eq!(metrics.application_metrics.db_operations_total, 1000);
311 }
312
313 #[test]
314 fn test_system_metrics_creation() {
315 let system_metrics = SystemMetrics {
316 memory_usage: 2048.0,
317 cpu_usage: 0.75,
318 uptime: 7200,
319 cache_hit_rate: 0.88,
320 cache_size: 1024.0,
321 };
322
323 assert!((system_metrics.memory_usage - 2048.0).abs() < f64::EPSILON);
324 assert!((system_metrics.cpu_usage - 0.75).abs() < f64::EPSILON);
325 assert_eq!(system_metrics.uptime, 7200);
326 assert!((system_metrics.cache_hit_rate - 0.88).abs() < f64::EPSILON);
327 assert!((system_metrics.cache_size - 1024.0).abs() < f64::EPSILON);
328 }
329
330 #[test]
331 fn test_application_metrics_creation() {
332 let app_metrics = ApplicationMetrics {
333 db_operations_total: 5000,
334 tasks_created_total: 100,
335 tasks_updated_total: 50,
336 tasks_deleted_total: 10,
337 tasks_completed_total: 80,
338 search_operations_total: 500,
339 export_operations_total: 25,
340 errors_total: 5,
341 };
342
343 assert_eq!(app_metrics.db_operations_total, 5000);
344 assert_eq!(app_metrics.tasks_created_total, 100);
345 assert_eq!(app_metrics.tasks_updated_total, 50);
346 assert_eq!(app_metrics.tasks_deleted_total, 10);
347 assert_eq!(app_metrics.tasks_completed_total, 80);
348 assert_eq!(app_metrics.search_operations_total, 500);
349 assert_eq!(app_metrics.export_operations_total, 25);
350 assert_eq!(app_metrics.errors_total, 5);
351 }
352
353 #[test]
354 fn test_log_statistics_creation() {
355 let mut level_counts = HashMap::new();
356 level_counts.insert("INFO".to_string(), 100);
357 level_counts.insert("ERROR".to_string(), 5);
358 level_counts.insert("WARN".to_string(), 10);
359
360 let mut target_counts = HashMap::new();
361 target_counts.insert("things3_cli".to_string(), 80);
362 target_counts.insert("things3_cli::database".to_string(), 20);
363
364 let recent_errors = vec![LogEntry {
365 timestamp: "2024-01-01T00:00:00Z".to_string(),
366 level: "ERROR".to_string(),
367 target: "things3_cli".to_string(),
368 message: "Database connection failed".to_string(),
369 }];
370
371 let log_stats = LogStatistics {
372 total_entries: 115,
373 level_counts,
374 target_counts,
375 recent_errors,
376 };
377
378 assert_eq!(log_stats.total_entries, 115);
379 assert_eq!(log_stats.level_counts.get("INFO"), Some(&100));
380 assert_eq!(log_stats.level_counts.get("ERROR"), Some(&5));
381 assert_eq!(log_stats.level_counts.get("WARN"), Some(&10));
382 assert_eq!(log_stats.target_counts.get("things3_cli"), Some(&80));
383 assert_eq!(log_stats.recent_errors.len(), 1);
384 }
385
386 #[test]
387 fn test_log_entry_creation() {
388 let log_entry = LogEntry {
389 timestamp: "2024-01-01T12:00:00Z".to_string(),
390 level: "DEBUG".to_string(),
391 target: "things3_cli::cache".to_string(),
392 message: "Cache miss for key: user_123".to_string(),
393 };
394
395 assert_eq!(log_entry.timestamp, "2024-01-01T12:00:00Z");
396 assert_eq!(log_entry.level, "DEBUG");
397 assert_eq!(log_entry.target, "things3_cli::cache");
398 assert_eq!(log_entry.message, "Cache miss for key: user_123");
399 }
400
401 #[test]
402 fn test_log_search_query_creation() {
403 let search_query = LogSearchQuery {
404 query: "database".to_string(),
405 level: Some("ERROR".to_string()),
406 start_time: Some("2024-01-01T00:00:00Z".to_string()),
407 end_time: Some("2024-01-01T23:59:59Z".to_string()),
408 };
409
410 assert_eq!(search_query.query, "database");
411 assert_eq!(search_query.level, Some("ERROR".to_string()));
412 assert_eq!(
413 search_query.start_time,
414 Some("2024-01-01T00:00:00Z".to_string())
415 );
416 assert_eq!(
417 search_query.end_time,
418 Some("2024-01-01T23:59:59Z".to_string())
419 );
420 }
421
422 #[test]
423 fn test_log_search_query_minimal() {
424 let search_query = LogSearchQuery {
425 query: "test".to_string(),
426 level: None,
427 start_time: None,
428 end_time: None,
429 };
430
431 assert_eq!(search_query.query, "test");
432 assert_eq!(search_query.level, None);
433 assert_eq!(search_query.start_time, None);
434 assert_eq!(search_query.end_time, None);
435 }
436
437 #[test]
438 fn test_system_info_creation() {
439 let system_info = SystemInfo {
440 os: "linux".to_string(),
441 arch: "x86_64".to_string(),
442 version: "1.0.0".to_string(),
443 rust_version: "1.70.0".to_string(),
444 };
445
446 assert_eq!(system_info.os, "linux");
447 assert_eq!(system_info.arch, "x86_64");
448 assert_eq!(system_info.version, "1.0.0");
449 assert_eq!(system_info.rust_version, "1.70.0");
450 }
451
452 #[test]
453 fn test_dashboard_state_creation() {
454 let temp_file = NamedTempFile::new().unwrap();
455 let db_path = temp_file.path();
456
457 let config = things3_core::ThingsConfig::new(db_path, false);
458 let rt = tokio::runtime::Runtime::new().unwrap();
459 let database = Arc::new(
460 rt.block_on(async { ThingsDatabase::new(&config.database_path).await.unwrap() }),
461 );
462
463 let observability = Arc::new(
464 things3_core::ObservabilityManager::new(things3_core::ObservabilityConfig::default())
465 .unwrap(),
466 );
467
468 let state = DashboardState {
469 observability: observability.clone(),
470 database: database.clone(),
471 };
472
473 let cloned_state = state.clone();
475 assert!(Arc::ptr_eq(
476 &cloned_state.observability,
477 &state.observability
478 ));
479 assert!(Arc::ptr_eq(&cloned_state.database, &state.database));
480 }
481
482 #[test]
483 fn test_dashboard_metrics_serialization() {
484 let metrics = DashboardMetrics {
485 health: HealthStatus {
486 status: "healthy".to_string(),
487 timestamp: chrono::Utc::now(),
488 uptime: std::time::Duration::from_secs(3600),
489 version: "1.0.0".to_string(),
490 checks: HashMap::new(),
491 },
492 system_metrics: SystemMetrics {
493 memory_usage: 1024.0,
494 cpu_usage: 0.5,
495 uptime: 3600,
496 cache_hit_rate: 0.95,
497 cache_size: 512.0,
498 },
499 application_metrics: ApplicationMetrics {
500 db_operations_total: 1000,
501 tasks_created_total: 50,
502 tasks_updated_total: 25,
503 tasks_deleted_total: 5,
504 tasks_completed_total: 30,
505 search_operations_total: 200,
506 export_operations_total: 10,
507 errors_total: 2,
508 },
509 log_statistics: LogStatistics {
510 total_entries: 1000,
511 level_counts: HashMap::new(),
512 target_counts: HashMap::new(),
513 recent_errors: Vec::new(),
514 },
515 };
516
517 let json = serde_json::to_string(&metrics).unwrap();
519 assert!(json.contains("healthy"));
520 assert!(json.contains("1024.0"));
521 assert!(json.contains("1000"));
522
523 let deserialized: DashboardMetrics = serde_json::from_str(&json).unwrap();
525 assert_eq!(deserialized.health.status, "healthy");
526 assert!((deserialized.system_metrics.memory_usage - 1024.0).abs() < f64::EPSILON);
527 assert_eq!(deserialized.application_metrics.db_operations_total, 1000);
528 }
529
530 #[test]
531 fn test_system_metrics_serialization() {
532 let system_metrics = SystemMetrics {
533 memory_usage: 2048.0,
534 cpu_usage: 0.75,
535 uptime: 7200,
536 cache_hit_rate: 0.88,
537 cache_size: 1024.0,
538 };
539
540 let json = serde_json::to_string(&system_metrics).unwrap();
541 let deserialized: SystemMetrics = serde_json::from_str(&json).unwrap();
542
543 assert!((deserialized.memory_usage - 2048.0).abs() < f64::EPSILON);
544 assert!((deserialized.cpu_usage - 0.75).abs() < f64::EPSILON);
545 assert_eq!(deserialized.uptime, 7200);
546 assert!((deserialized.cache_hit_rate - 0.88).abs() < f64::EPSILON);
547 assert!((deserialized.cache_size - 1024.0).abs() < f64::EPSILON);
548 }
549
550 #[test]
551 fn test_application_metrics_serialization() {
552 let app_metrics = ApplicationMetrics {
553 db_operations_total: 5000,
554 tasks_created_total: 100,
555 tasks_updated_total: 50,
556 tasks_deleted_total: 10,
557 tasks_completed_total: 80,
558 search_operations_total: 500,
559 export_operations_total: 25,
560 errors_total: 5,
561 };
562
563 let json = serde_json::to_string(&app_metrics).unwrap();
564 let deserialized: ApplicationMetrics = serde_json::from_str(&json).unwrap();
565
566 assert_eq!(deserialized.db_operations_total, 5000);
567 assert_eq!(deserialized.tasks_created_total, 100);
568 assert_eq!(deserialized.tasks_updated_total, 50);
569 assert_eq!(deserialized.tasks_deleted_total, 10);
570 assert_eq!(deserialized.tasks_completed_total, 80);
571 assert_eq!(deserialized.search_operations_total, 500);
572 assert_eq!(deserialized.export_operations_total, 25);
573 assert_eq!(deserialized.errors_total, 5);
574 }
575
576 #[test]
577 fn test_log_entry_serialization() {
578 let log_entry = LogEntry {
579 timestamp: "2024-01-01T12:00:00Z".to_string(),
580 level: "DEBUG".to_string(),
581 target: "things3_cli::cache".to_string(),
582 message: "Cache miss for key: user_123".to_string(),
583 };
584
585 let json = serde_json::to_string(&log_entry).unwrap();
586 let deserialized: LogEntry = serde_json::from_str(&json).unwrap();
587
588 assert_eq!(deserialized.timestamp, "2024-01-01T12:00:00Z");
589 assert_eq!(deserialized.level, "DEBUG");
590 assert_eq!(deserialized.target, "things3_cli::cache");
591 assert_eq!(deserialized.message, "Cache miss for key: user_123");
592 }
593
594 #[test]
595 fn test_log_search_query_serialization() {
596 let search_query = LogSearchQuery {
597 query: "database".to_string(),
598 level: Some("ERROR".to_string()),
599 start_time: Some("2024-01-01T00:00:00Z".to_string()),
600 end_time: Some("2024-01-01T23:59:59Z".to_string()),
601 };
602
603 let json = serde_json::to_string(&search_query).unwrap();
604 let deserialized: LogSearchQuery = serde_json::from_str(&json).unwrap();
605
606 assert_eq!(deserialized.query, "database");
607 assert_eq!(deserialized.level, Some("ERROR".to_string()));
608 assert_eq!(
609 deserialized.start_time,
610 Some("2024-01-01T00:00:00Z".to_string())
611 );
612 assert_eq!(
613 deserialized.end_time,
614 Some("2024-01-01T23:59:59Z".to_string())
615 );
616 }
617
618 #[test]
619 fn test_system_info_serialization() {
620 let system_info = SystemInfo {
621 os: "linux".to_string(),
622 arch: "x86_64".to_string(),
623 version: "1.0.0".to_string(),
624 rust_version: "1.70.0".to_string(),
625 };
626
627 let json = serde_json::to_string(&system_info).unwrap();
628 let deserialized: SystemInfo = serde_json::from_str(&json).unwrap();
629
630 assert_eq!(deserialized.os, "linux");
631 assert_eq!(deserialized.arch, "x86_64");
632 assert_eq!(deserialized.version, "1.0.0");
633 assert_eq!(deserialized.rust_version, "1.70.0");
634 }
635
636 #[test]
637 fn test_dashboard_metrics_debug_formatting() {
638 let metrics = DashboardMetrics {
639 health: HealthStatus {
640 status: "healthy".to_string(),
641 timestamp: chrono::Utc::now(),
642 uptime: std::time::Duration::from_secs(3600),
643 version: "1.0.0".to_string(),
644 checks: HashMap::new(),
645 },
646 system_metrics: SystemMetrics {
647 memory_usage: 1024.0,
648 cpu_usage: 0.5,
649 uptime: 3600,
650 cache_hit_rate: 0.95,
651 cache_size: 512.0,
652 },
653 application_metrics: ApplicationMetrics {
654 db_operations_total: 1000,
655 tasks_created_total: 50,
656 tasks_updated_total: 25,
657 tasks_deleted_total: 5,
658 tasks_completed_total: 30,
659 search_operations_total: 200,
660 export_operations_total: 10,
661 errors_total: 2,
662 },
663 log_statistics: LogStatistics {
664 total_entries: 1000,
665 level_counts: HashMap::new(),
666 target_counts: HashMap::new(),
667 recent_errors: Vec::new(),
668 },
669 };
670
671 let debug_str = format!("{metrics:?}");
672 assert!(debug_str.contains("DashboardMetrics"));
673 assert!(debug_str.contains("SystemMetrics"));
674 assert!(debug_str.contains("ApplicationMetrics"));
675 assert!(debug_str.contains("LogStatistics"));
676 }
677
678 #[test]
679 fn test_dashboard_metrics_clone() {
680 let metrics = DashboardMetrics {
681 health: HealthStatus {
682 status: "healthy".to_string(),
683 timestamp: chrono::Utc::now(),
684 uptime: std::time::Duration::from_secs(3600),
685 version: "1.0.0".to_string(),
686 checks: HashMap::new(),
687 },
688 system_metrics: SystemMetrics {
689 memory_usage: 1024.0,
690 cpu_usage: 0.5,
691 uptime: 3600,
692 cache_hit_rate: 0.95,
693 cache_size: 512.0,
694 },
695 application_metrics: ApplicationMetrics {
696 db_operations_total: 1000,
697 tasks_created_total: 50,
698 tasks_updated_total: 25,
699 tasks_deleted_total: 5,
700 tasks_completed_total: 30,
701 search_operations_total: 200,
702 export_operations_total: 10,
703 errors_total: 2,
704 },
705 log_statistics: LogStatistics {
706 total_entries: 1000,
707 level_counts: HashMap::new(),
708 target_counts: HashMap::new(),
709 recent_errors: Vec::new(),
710 },
711 };
712
713 let cloned_metrics = metrics.clone();
714 assert_eq!(cloned_metrics.health.status, metrics.health.status);
715 assert!(
716 (cloned_metrics.system_metrics.memory_usage - metrics.system_metrics.memory_usage)
717 .abs()
718 < f64::EPSILON
719 );
720 assert_eq!(
721 cloned_metrics.application_metrics.db_operations_total,
722 metrics.application_metrics.db_operations_total
723 );
724 assert_eq!(
725 cloned_metrics.log_statistics.total_entries,
726 metrics.log_statistics.total_entries
727 );
728 }
729}