Skip to main content

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)]
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        // Test that the state can be cloned
475        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        // Test serialization
519        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        // Test deserialization
525        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}