OXYGENHUB

/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