|
|
… |
339 unchanged lines |
| 340 |
340 |
|
.map(|(o, st, _, _)| (format!("owner=\"{}\"", label(o)), if st == "ok" { 1.0 } else { 0.0 })) |
| 341 |
341 |
|
.collect::<Vec<_>>(), |
| 342 |
342 |
|
); |
|
343 |
+ |
flow_timings(app, &mut out, now).await; |
|
344 |
+ |
if let Ok((waiting, dead)) = db::queue::counts(&app.db).await { |
|
345 |
+ |
family( |
|
346 |
+ |
&mut out, |
|
347 |
+ |
"oxyhub_queue_items", |
|
348 |
+ |
"Items in the durable work queue (#276): waiting (due, backing off or claimed) and dead (failed every attempt).", |
|
349 |
+ |
"gauge", |
|
350 |
+ |
&[("state=\"waiting\"".into(), waiting as f64), ("state=\"dead\"".into(), dead as f64)], |
|
351 |
+ |
); |
|
352 |
+ |
} |
| 343 |
353 |
|
http_families(&mut out); |
| 344 |
354 |
|
out |
| 345 |
355 |
|
} |
|
356 |
+ |
|
|
357 |
+ |
/// A finished job's runner, result, note, and when it was ready, started |
|
358 |
+ |
/// and finished. |
|
359 |
+ |
type JobTimes = (String, String, String, Option<i64>, Option<i64>, Option<i64>); |
| 346 |
360 |
|
|
|
361 |
+ |
/// Bucket bounds for job times, in seconds. |
|
362 |
+ |
const JOB_SECONDS: [f64; 11] = [1.0, 5.0, 15.0, 30.0, 60.0, 120.0, 300.0, 600.0, 1200.0, 1800.0, 3600.0]; |
|
363 |
+ |
|
|
364 |
+ |
/// Flow's job times over the last day (oxygenflow#35), by runner: how long |
|
365 |
+ |
/// jobs waited from ready to started (queue time) and ran (run time), and |
|
366 |
+ |
/// how many finished, by result, and were reused (#265). |
|
367 |
+ |
async fn flow_timings(app: &Shared, out: &mut String, now: i64) { |
|
368 |
+ |
let rows: Vec<JobTimes> = sqlx::query_as( |
|
369 |
+ |
"SELECT runner, status, error, COALESCE(ready_ms, queued_ms), started_ms, finished_ms FROM flow_jobs \ |
|
370 |
+ |
WHERE finished_ms >= $1 AND status IN ('passed', 'failed', 'error', 'cancelled')", |
|
371 |
+ |
) |
|
372 |
+ |
.bind(now - 86_400_000) |
|
373 |
+ |
.fetch_all(&app.db) |
|
374 |
+ |
.await |
|
375 |
+ |
.unwrap_or_default(); |
|
376 |
+ |
let mut queue: std::collections::BTreeMap<String, Vec<f64>> = Default::default(); |
|
377 |
+ |
let mut run: std::collections::BTreeMap<String, Vec<f64>> = Default::default(); |
|
378 |
+ |
let mut finished: std::collections::BTreeMap<(String, String), f64> = Default::default(); |
|
379 |
+ |
let mut reused = 0.0; |
|
380 |
+ |
for (runner, status, error, ready, started, finished_ms) in &rows { |
|
381 |
+ |
if error.starts_with("reused from run #") { |
|
382 |
+ |
reused += 1.0; |
|
383 |
+ |
continue; |
|
384 |
+ |
} |
|
385 |
+ |
let r = if runner.is_empty() { "none".to_string() } else { runner.clone() }; |
|
386 |
+ |
*finished.entry((r.clone(), status.clone())).or_default() += 1.0; |
|
387 |
+ |
if let (Some(ready), Some(started)) = (ready, started) { |
|
388 |
+ |
queue.entry(r.clone()).or_default().push((started - ready).max(0) as f64 / 1000.0); |
|
389 |
+ |
} |
|
390 |
+ |
if let (Some(started), Some(f)) = (started, finished_ms) { |
|
391 |
+ |
run.entry(r).or_default().push((f - started).max(0) as f64 / 1000.0); |
|
392 |
+ |
} |
|
393 |
+ |
} |
|
394 |
+ |
histogram( |
|
395 |
+ |
out, |
|
396 |
+ |
"oxyhub_flow_job_queue_seconds", |
|
397 |
+ |
"How long finished jobs of the last day waited from ready to started, by runner.", |
|
398 |
+ |
&queue, |
|
399 |
+ |
); |
|
400 |
+ |
histogram(out, "oxyhub_flow_job_run_seconds", "How long finished jobs of the last day ran, by runner.", &run); |
|
401 |
+ |
family( |
|
402 |
+ |
out, |
|
403 |
+ |
"oxyhub_flow_jobs_finished_last_day", |
|
404 |
+ |
"Jobs that finished in the last day, by runner and result (none: never placed).", |
|
405 |
+ |
"gauge", |
|
406 |
+ |
&finished.iter().map(|((r, st), n)| (format!("runner=\"{}\",status=\"{}\"", label(r), label(st)), *n)).collect::<Vec<_>>(), |
|
407 |
+ |
); |
|
408 |
+ |
family( |
|
409 |
+ |
out, |
|
410 |
+ |
"oxyhub_flow_jobs_reused_last_day", |
|
411 |
+ |
"Jobs of the last day that reused a pass on the same state instead of running (#265).", |
|
412 |
+ |
"gauge", |
|
413 |
+ |
&[(String::new(), reused)], |
|
414 |
+ |
); |
|
415 |
+ |
} |
|
416 |
+ |
|
|
417 |
+ |
/// A histogram family from samples (seconds) per runner. |
|
418 |
+ |
fn histogram(out: &mut String, name: &str, help: &str, by_runner: &std::collections::BTreeMap<String, Vec<f64>>) { |
|
419 |
+ |
let _ = writeln!(out, "# HELP {name} {help}"); |
|
420 |
+ |
let _ = writeln!(out, "# TYPE {name} histogram"); |
|
421 |
+ |
for (runner, samples) in by_runner { |
|
422 |
+ |
let l = format!("runner=\"{}\"", label(runner)); |
|
423 |
+ |
for b in JOB_SECONDS { |
|
424 |
+ |
let n = samples.iter().filter(|s| **s <= b).count(); |
|
425 |
+ |
let _ = writeln!(out, "{name}_bucket{{{l},le=\"{b}\"}} {n}"); |
|
426 |
+ |
} |
|
427 |
+ |
let _ = writeln!(out, "{name}_bucket{{{l},le=\"+Inf\"}} {}", samples.len()); |
|
428 |
+ |
let _ = writeln!(out, "{name}_sum{{{l}}} {}", samples.iter().sum::<f64>()); |
|
429 |
+ |
let _ = writeln!(out, "{name}_count{{{l}}} {}", samples.len()); |
|
430 |
+ |
} |
|
431 |
+ |
} |
|
432 |
+ |
|
| 347 |
433 |
|
/// `GET /metrics`. |
| 348 |
434 |
|
pub async fn metrics(State(app): State<Shared>, headers: HeaderMap) -> Response { |
| 349 |
435 |
|
let want = &app.config.server.metrics_token; |
|
|
… |
10 unchanged lines |
| 360 |
446 |
|
|
| 361 |
447 |
|
#[cfg(test)] |
| 362 |
448 |
|
mod tests { |
|
449 |
+ |
|
|
450 |
+ |
#[test] |
|
451 |
+ |
fn job_times_are_cumulative_buckets_per_runner() { |
|
452 |
+ |
let mut by: std::collections::BTreeMap<String, Vec<f64>> = Default::default(); |
|
453 |
+ |
by.insert("earth-1".into(), vec![0.5, 20.0, 4000.0]); |
|
454 |
+ |
let mut out = String::new(); |
|
455 |
+ |
super::histogram(&mut out, "x_seconds", "help", &by); |
|
456 |
+ |
assert!(out.contains("x_seconds_bucket{runner=\"earth-1\",le=\"1\"} 1"), "{out}"); |
|
457 |
+ |
assert!(out.contains("x_seconds_bucket{runner=\"earth-1\",le=\"30\"} 2"), "{out}"); |
|
458 |
+ |
assert!(out.contains("x_seconds_bucket{runner=\"earth-1\",le=\"3600\"} 2"), "{out}"); |
|
459 |
+ |
assert!( |
|
460 |
+ |
out.contains("x_seconds_bucket{runner=\"earth-1\",le=\"+Inf\"} 3") && out.contains("x_seconds_count{runner=\"earth-1\"} 3"), |
|
461 |
+ |
"{out}" |
|
462 |
+ |
); |
|
463 |
+ |
} |
|
464 |
+ |
|
| 363 |
465 |
|
#[test] |
| 364 |
466 |
|
fn route_classes_hold_no_names() { |
| 365 |
467 |
|
for (path, class) in [ |
|
|
… |
14 unchanged lines |