1use std::sync::{Arc, Mutex};
7
8use tokio::sync::watch;
9
10use crate::{
11 docker::DockerClient,
12 metrics::MetricsCollector,
13 storage::{TelemetrySnapshot, TelemetryStore},
14};
15
16#[derive(Debug, Clone)]
18pub struct CollectorConfig {
19 pub snapshot_interval_secs: u64,
21 pub metrics_interval_secs: u64,
23}
24
25impl Default for CollectorConfig {
26 fn default() -> Self {
27 Self {
28 snapshot_interval_secs: crate::DEFAULT_SNAPSHOT_INTERVAL,
29 metrics_interval_secs: crate::DEFAULT_METRICS_INTERVAL,
30 }
31 }
32}
33
34#[derive(Debug, Default)]
36pub struct CollectionStats {
37 pub snapshots_written: u64,
38 pub metrics_written: u64,
39 pub errors: Vec<String>,
40}
41
42#[allow(clippy::needless_pass_by_value)]
47pub fn spawn_metrics_collector<S, C, M>(
48 store: Arc<Mutex<S>>,
49 docker: C,
50 metrics_collector: M,
51 config: CollectorConfig,
52 mut shutdown: watch::Receiver<bool>,
53) -> tokio::task::JoinHandle<()>
54where
55 S: TelemetryStore + Send + 'static,
56 C: DockerClient + Send + Sync + 'static,
57 M: MetricsCollector + Send + Sync + 'static,
58{
59 tokio::spawn(async move {
60 let mut metrics_interval =
61 tokio::time::interval(std::time::Duration::from_secs(config.metrics_interval_secs));
62 let mut snapshot_interval = tokio::time::interval(std::time::Duration::from_secs(
63 config.snapshot_interval_secs,
64 ));
65
66 metrics_interval.tick().await;
68 snapshot_interval.tick().await;
69
70 collect_snapshot(&store, &docker).await;
72
73 loop {
74 tokio::select! {
75 _ = shutdown.changed(), if *shutdown.borrow() => {
76 break;
77 }
78 _ = metrics_interval.tick() => {
79 collect_metrics(&store, &metrics_collector).await;
80 }
81 _ = snapshot_interval.tick() => {
82 collect_snapshot(&store, &docker).await;
83 }
84 }
85 }
86 })
87}
88
89async fn collect_metrics<S, M>(store: &Arc<Mutex<S>>, metrics_collector: &M)
90where
91 S: TelemetryStore,
92 M: MetricsCollector + ?Sized,
93{
94 match metrics_collector.collect().await {
95 Ok(samples) => {
96 if let Ok(mut store) = store.lock() {
97 for sample in samples {
98 if let Err(e) = store.write_metric(sample) {
99 eprintln!("[collector] metric write error: {e}");
100 }
101 }
102 }
103 }
104 Err(e) => {
105 eprintln!("[collector] metrics collection error: {e}");
106 }
107 }
108}
109
110async fn collect_snapshot<S, C>(store: &Arc<Mutex<S>>, docker: &C)
111where
112 S: TelemetryStore,
113 C: DockerClient,
114{
115 let timestamp = chrono::Utc::now().to_rfc3339();
116 let containers = docker.list_containers().await.unwrap_or_default();
117 let images = docker.list_images().await.unwrap_or_default();
118 let networks = docker.list_networks().await.unwrap_or_default();
119 let volumes = docker.list_volumes().await.unwrap_or_default();
120
121 let snapshot = TelemetrySnapshot {
122 timestamp,
123 containers,
124 images,
125 networks,
126 volumes,
127 };
128
129 if let Ok(mut store) = store.lock()
130 && let Err(e) = store.write_snapshot(snapshot)
131 {
132 eprintln!("[collector] snapshot write error: {e}");
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::{
140 docker::MockDockerClient, metrics::MockMetricsCollector, storage::InMemoryTelemetryStore,
141 };
142
143 const COLLECTOR_WAIT_DURATION: std::time::Duration = std::time::Duration::from_millis(1500);
145
146 #[tokio::test]
147 async fn collector_writes_snapshot_and_metrics() {
148 let store = Arc::new(Mutex::new(InMemoryTelemetryStore::default()));
149 let docker = MockDockerClient {
150 containers: vec![crate::docker::Container {
151 id: "abc".to_owned(),
152 name: "api".to_owned(),
153 image: "api:latest".to_owned(),
154 status: "Up".to_owned(),
155 state: "running".to_owned(),
156 ports: Vec::new(),
157 labels: Vec::new(),
158 created_at: None,
159 started_at: None,
160 finished_at: None,
161 restart_count: Some(0),
162 health: None,
163 }],
164 ..Default::default()
165 };
166 let metrics = MockMetricsCollector {
167 samples: vec![crate::docker::MetricSample {
168 container_id: "abc".to_owned(),
169 container_name: "api".to_owned(),
170 timestamp: "2026-01-01T12:00:00Z".to_owned(),
171 cpu_percent: Some(50.0),
172 memory_usage_bytes: Some(128),
173 memory_limit_bytes: Some(1024),
174 network_rx_bytes: None,
175 network_tx_bytes: None,
176 disk_read_bytes: None,
177 disk_write_bytes: None,
178 }],
179 };
180
181 let (shutdown_tx, shutdown_rx) = watch::channel(false);
182 let config = CollectorConfig {
183 snapshot_interval_secs: 1,
184 metrics_interval_secs: 1,
185 };
186
187 let handle =
188 spawn_metrics_collector(Arc::clone(&store), docker, metrics, config, shutdown_rx);
189
190 tokio::time::sleep(COLLECTOR_WAIT_DURATION).await;
192
193 shutdown_tx.send(true).unwrap();
195 handle.await.unwrap();
196
197 let store = store.lock().unwrap();
199 let latest = store.latest_metrics().unwrap();
200 assert!(!latest.is_empty());
201 }
202}