1use std::fmt::Write;
4
5use chrono::Utc;
6use sqlx::SqlitePool;
7
8use crate::config::Config;
9use crate::models::CameraStatus;
10use crate::services::storage;
11
12fn esc(s: &str) -> String {
14 s.replace('\\', "\\\\")
15 .replace('"', "\\\"")
16 .replace('\n', " ")
17}
18
19fn metric(out: &mut String, name: &str, help: &str, kind: &str, value: f64) {
20 let _ = writeln!(out, "# HELP {name} {help}");
21 let _ = writeln!(out, "# TYPE {name} {kind}");
22 let _ = writeln!(out, "{name} {value}");
23}
24
25pub async fn render(pool: &SqlitePool, cfg: &Config) -> sqlx::Result<String> {
27 let mut out = String::new();
28
29 let _ = writeln!(out, "# HELP heldar_build_info Build information");
30 let _ = writeln!(out, "# TYPE heldar_build_info gauge");
31 let _ = writeln!(
32 out,
33 "heldar_build_info{{version=\"{}\"}} 1",
34 env!("CARGO_PKG_VERSION")
35 );
36
37 let cameras_total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM cameras")
38 .fetch_one(pool)
39 .await?;
40 let recording: i64 =
41 sqlx::query_scalar("SELECT COUNT(*) FROM camera_status WHERE state = 'recording'")
42 .fetch_one(pool)
43 .await?;
44 let segments_total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM segments")
45 .fetch_one(pool)
46 .await?;
47 let recordings_bytes: i64 =
48 sqlx::query_scalar("SELECT COALESCE(SUM(size_bytes), 0) FROM segments")
49 .fetch_one(pool)
50 .await?;
51
52 metric(
53 &mut out,
54 "heldar_cameras_total",
55 "Registered cameras",
56 "gauge",
57 cameras_total as f64,
58 );
59 metric(
60 &mut out,
61 "heldar_cameras_recording",
62 "Cameras currently recording",
63 "gauge",
64 recording as f64,
65 );
66 metric(
67 &mut out,
68 "heldar_segments_total",
69 "Indexed recording segments",
70 "gauge",
71 segments_total as f64,
72 );
73 metric(
74 &mut out,
75 "heldar_recordings_bytes",
76 "Total bytes of recorded segments",
77 "gauge",
78 recordings_bytes as f64,
79 );
80
81 let ai_tasks_enabled: i64 =
82 sqlx::query_scalar("SELECT COUNT(*) FROM ai_tasks WHERE enabled = 1")
83 .fetch_one(pool)
84 .await?;
85 let detections_total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM detections")
86 .fetch_one(pool)
87 .await?;
88 metric(
89 &mut out,
90 "heldar_ai_tasks_enabled",
91 "Enabled AI tasks",
92 "gauge",
93 ai_tasks_enabled as f64,
94 );
95 metric(
97 &mut out,
98 "heldar_detections_stored",
99 "AI detections currently stored",
100 "gauge",
101 detections_total as f64,
102 );
103
104 if let Some(d) = storage::disk_stats_async(cfg.recordings_dir.clone()).await {
105 metric(
106 &mut out,
107 "heldar_disk_total_bytes",
108 "Total bytes on recordings filesystem",
109 "gauge",
110 d.total_bytes as f64,
111 );
112 metric(
113 &mut out,
114 "heldar_disk_free_bytes",
115 "Free bytes on recordings filesystem",
116 "gauge",
117 d.free_bytes as f64,
118 );
119 metric(
120 &mut out,
121 "heldar_disk_used_percent",
122 "Used percent of recordings filesystem",
123 "gauge",
124 d.used_percent,
125 );
126 }
127
128 let rows = sqlx::query_as::<_, CameraStatus>("SELECT * FROM camera_status")
129 .fetch_all(pool)
130 .await?;
131 let now = Utc::now();
132
133 let _ = writeln!(
134 out,
135 "# HELP heldar_camera_up Camera recording state (1 = recording)"
136 );
137 let _ = writeln!(out, "# TYPE heldar_camera_up gauge");
138 for r in &rows {
139 let up = i32::from(r.state == "recording");
140 let _ = writeln!(
141 out,
142 "heldar_camera_up{{camera=\"{}\",state=\"{}\"}} {up}",
143 esc(&r.camera_id),
144 esc(&r.state)
145 );
146 }
147
148 let _ = writeln!(
149 out,
150 "# HELP heldar_camera_reconnects_total Recorder reconnect count"
151 );
152 let _ = writeln!(out, "# TYPE heldar_camera_reconnects_total counter");
153 for r in &rows {
154 let _ = writeln!(
155 out,
156 "heldar_camera_reconnects_total{{camera=\"{}\"}} {}",
157 esc(&r.camera_id),
158 r.reconnect_count
159 );
160 }
161
162 let _ = writeln!(
163 out,
164 "# HELP heldar_camera_segments_written_total Segments written by the recorder"
165 );
166 let _ = writeln!(out, "# TYPE heldar_camera_segments_written_total counter");
167 for r in &rows {
168 let _ = writeln!(
169 out,
170 "heldar_camera_segments_written_total{{camera=\"{}\"}} {}",
171 esc(&r.camera_id),
172 r.segments_written
173 );
174 }
175
176 let _ = writeln!(
177 out,
178 "# HELP heldar_camera_bitrate_kbps Observed stream bitrate (kbps)"
179 );
180 let _ = writeln!(out, "# TYPE heldar_camera_bitrate_kbps gauge");
181 for r in &rows {
182 if let Some(b) = r.bitrate_kbps {
183 let _ = writeln!(
184 out,
185 "heldar_camera_bitrate_kbps{{camera=\"{}\"}} {b}",
186 esc(&r.camera_id)
187 );
188 }
189 }
190
191 let _ = writeln!(
192 out,
193 "# HELP heldar_camera_last_segment_age_seconds Seconds since the last indexed segment"
194 );
195 let _ = writeln!(out, "# TYPE heldar_camera_last_segment_age_seconds gauge");
196 for r in &rows {
197 if let Some(t) = r.last_segment_at {
198 let age = (now - t).num_seconds().max(0);
199 let _ = writeln!(
200 out,
201 "heldar_camera_last_segment_age_seconds{{camera=\"{}\"}} {age}",
202 esc(&r.camera_id)
203 );
204 }
205 }
206
207 Ok(out)
208}