/metrics: database pool, pushes and backups (refs #212) a6d824c5ebf5
Adds to /metrics: - the database pool's connections, idle and in use; - reference updates pushed in the last 24 hours, accepted and refused (one query, working on SQLite and Postgres); - for each owner's latest backup run, the seconds since it started and whether it succeeded. Test extended, on SQLite and Postgres. Hub spec §13.5.5. Refs #212
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 1b1a8b9d4684
- When
- · 2026-10-18 03:50 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
a6d824c5ebf565d6c29e00acb4f646d80640a7810e3eda0c6953099ec4adb9a0- Depends on
- 42fa95540468 8a48dd830178 93be9ce4653e b9b74d997cec
- Depended on by
- 9e8cbfc3ad00 85ec74fd82d8 2c47eecfeab6
- In bookmarks
- api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
- In tags
- v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 6 atoms, 0 tree operations, 3 files
Browse the tree at this change
modified crates/hub/src/metrics.rs +49 −0
| … | 223 unchanged lines |
||
| 224 | 224 | ], |
|
| 225 | 225 | ); |
|
| 226 | 226 | family(&mut out, "oxyhub_tlog_entries", "Entries in the transparency log.", "gauge", &[(String::new(), app.tlog.size() as f64)]); |
|
| 227 | + | family( |
|
| 228 | + | &mut out, |
|
| 229 | + | "oxyhub_db_connections", |
|
| 230 | + | "The database pool's connections, by whether they are idle.", |
|
| 231 | + | "gauge", |
|
| 232 | + | &[ |
|
| 233 | + | ("idle=\"true\"".into(), app.db.num_idle() as f64), |
|
| 234 | + | ("idle=\"false\"".into(), (app.db.size() as f64 - app.db.num_idle() as f64).max(0.0)), |
|
| 235 | + | ], |
|
| 236 | + | ); |
|
| 237 | + | let since = db::now_ms() - 86_400_000; |
|
| 238 | + | let (ok, refused): (Option<i64>, Option<i64>) = |
|
| 239 | + | sqlx::query_as("SELECT SUM(CASE WHEN ok THEN 1 ELSE 0 END), SUM(CASE WHEN ok THEN 0 ELSE 1 END) FROM pushes WHERE at_ms > $1") |
|
| 240 | + | .bind(since) |
|
| 241 | + | .fetch_one(&app.db) |
|
| 242 | + | .await |
|
| 243 | + | .unwrap_or((None, None)); |
|
| 244 | + | family( |
|
| 245 | + | &mut out, |
|
| 246 | + | "oxyhub_pushes_last_day", |
|
| 247 | + | "Reference updates pushed in the last 24 hours, by whether they were accepted.", |
|
| 248 | + | "gauge", |
|
| 249 | + | &[("result=\"accepted\"".into(), ok.unwrap_or(0) as f64), ("result=\"refused\"".into(), refused.unwrap_or(0) as f64)], |
|
| 250 | + | ); |
|
| 251 | + | let backups: Vec<(String, String, i64, Option<i64>)> = sqlx::query_as( |
|
| 252 | + | "SELECT owner, status, started_ms, finished_ms FROM backup_runs WHERE id IN (SELECT MAX(id) FROM backup_runs GROUP BY owner) ORDER BY owner", |
|
| 253 | + | ) |
|
| 254 | + | .fetch_all(&app.db) |
|
| 255 | + | .await |
|
| 256 | + | .unwrap_or_default(); |
|
| 257 | + | let now = db::now_ms(); |
|
| 258 | + | family( |
|
| 259 | + | &mut out, |
|
| 260 | + | "oxyhub_backup_last_run_age_seconds", |
|
| 261 | + | "Seconds since each owner's latest backup run started.", |
|
| 262 | + | "gauge", |
|
| 263 | + | &backups.iter().map(|(o, _, s, _)| (format!("owner=\"{}\"", label(o)), (now - s) as f64 / 1000.0)).collect::<Vec<_>>(), |
|
| 264 | + | ); |
|
| 265 | + | family( |
|
| 266 | + | &mut out, |
|
| 267 | + | "oxyhub_backup_last_run_ok", |
|
| 268 | + | "Whether each owner's latest finished backup run succeeded (1), was partial or failed (0); running ones are left out.", |
|
| 269 | + | "gauge", |
|
| 270 | + | &backups |
|
| 271 | + | .iter() |
|
| 272 | + | .filter(|(_, st, _, f)| f.is_some() && st != "running") |
|
| 273 | + | .map(|(o, st, _, _)| (format!("owner=\"{}\"", label(o)), if st == "ok" { 1.0 } else { 0.0 })) |
|
| 274 | + | .collect::<Vec<_>>(), |
|
| 275 | + | ); |
|
| 227 | 276 | http_families(&mut out); |
|
| 228 | 277 | out |
|
| 229 | 278 | } |
|
| … | 34 unchanged lines |
modified crates/hub/tests/flow.rs +4 −0
| … | 5303 unchanged lines |
||
| 5304 | 5304 | "oxyhub_flow_jobs{state=\"queued\"} 0", |
|
| 5305 | 5305 | "oxyhub_bucket_requests_total{kind=\"list\"}", |
|
| 5306 | 5306 | "oxyhub_tlog_entries", |
|
| 5307 | + | "oxyhub_db_connections{idle=\"true\"}", |
|
| 5308 | + | "oxyhub_pushes_last_day{result=\"refused\"} 0", |
|
| 5309 | + | "# TYPE oxyhub_backup_last_run_ok gauge", |
|
| 5307 | 5310 | ] { |
|
| 5308 | 5311 | assert!(text.contains(want), "missing {want}:\n{text}"); |
|
| 5309 | 5312 | } |
|
| … | 8 unchanged lines |
||
| 5318 | 5321 | assert_eq!(runs_of(&h.app, "andy", "demo").await.len(), 1); |
|
| 5319 | 5322 | let text = oxygenhub::metrics::render(&h.app).await; |
|
| 5320 | 5323 | assert!(!text.contains("oxyhub_flow_jobs{state=\"queued\"} 0"), "the run's jobs are queued:\n{text}"); |
|
| 5324 | + | assert!(text.contains("oxyhub_pushes_last_day{result=\"accepted\"}"), "{text}"); |
|
| 5321 | 5325 | // Requests through the router are counted by route class. |
|
| 5322 | 5326 | call(&h.app, "GET", "/andy/demo", None, None).await; |
|
| 5323 | 5327 | let text = oxygenhub::metrics::render(&h.app).await; |
|
| … | 188 unchanged lines |
modified docs/spec.md +4 −1
| … | 643 unchanged lines |
||
| 644 | 644 | - requests to bucket stores by kind (with pack listings sent and kept apart), and the time spent in them; |
|
| 645 | 645 | - OxygenFlow jobs queued, waiting for approval and running, and runners online and not; |
|
| 646 | 646 | - the transparency log's entries; |
|
| 647 | + | - the database pool's connections (idle and in use); |
|
| 648 | + | - reference updates pushed in the last 24 hours, accepted and refused; |
|
| 649 | + | - each owner's latest backup: seconds since it started, and whether it succeeded; |
|
| 647 | 650 | - HTTP requests by route class (`page:<tab>`, `api:<area>`, `protocol:<endpoint>`, `static` and so on, never a user's or repository's name; at most 300 classes), method and status class, with latency histograms (5 ms to 30 s). |
|
| 648 | 651 | ||
| 649 | - | Pushes and fetches, backups, Diffusion, mail and the database pool are the next slices; the runner's own metrics too. |
|
| 652 | + | Push and fetch sizes and durations, Diffusion, mail and query latency are the next slices; the runner's own metrics too. |
|
| 650 | 653 | ||
| 651 | 654 | ### 13.6 Coverage reports (oxygenhub#113, done 2026-10-06) |
|
| 652 | 655 | ||
| … | 168 unchanged lines |