things3_cli/
dashboard.rs

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    // Mock log entries - in a real implementation, these would come from log files
48    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    // Mock search results - in a real implementation, this would search through log files
76    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    // Mock system info - in a real implementation, this would come from system APIs
89    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// Struct definitions - must come after all functions to avoid items_after_statements
115/// Dashboard state
116#[derive(Clone)]
117pub struct DashboardState {
118    pub observability: Arc<ObservabilityManager>,
119    pub database: Arc<ThingsDatabase>,
120}
121
122/// Dashboard metrics
123#[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/// System information
177#[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    /// Create a new dashboard server
187    #[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    /// Start the dashboard server
201    ///
202    /// # Errors
203    /// Returns an error if the server fails to start or bind to the port
204    #[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
229/// Dashboard server
230pub struct DashboardServer {
231    port: u16,
232    observability: Arc<ObservabilityManager>,
233    database: Arc<ThingsDatabase>,
234}
235
236/// Start the dashboard server
237///
238/// # Errors
239/// Returns an error if the server fails to start or bind to the port
240#[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        // Test that the state can be cloned
474        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        // Test serialization
518        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        // Test deserialization
524        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}