| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | | use std::io::{BufRead, BufReader, Write}; |
| 7 | | use std::sync::atomic::{AtomicU64, Ordering::Relaxed}; |
| 8 | | use std::time::Instant; |
| 9 | |
|
| 10 | |
|
| 11 | | pub static CLAIMS: AtomicU64 = AtomicU64::new(0); |
| 12 | |
|
| 13 | | pub static DECLINED: AtomicU64 = AtomicU64::new(0); |
| 14 | |
|
| 15 | | pub static PASSED: AtomicU64 = AtomicU64::new(0); |
| 16 | | pub static FAILED: AtomicU64 = AtomicU64::new(0); |
| 17 | | pub static ERRORS: AtomicU64 = AtomicU64::new(0); |
| 18 | | pub static CANCELLED: AtomicU64 = AtomicU64::new(0); |
| 19 | |
|
| 20 | | pub static JOB_MS: AtomicU64 = AtomicU64::new(0); |
| 21 | |
|
| 22 | | pub static RUNNING: AtomicU64 = AtomicU64::new(0); |
| 23 | |
|
| 24 | |
|
| 25 | 1× | pub fn finished(exit_code: Option<i32>, error: Option<&str>, duration_ms: u64, cancelled: bool) { |
| 26 | 1× | let counter = if cancelled { |
| 27 | 0× | &CANCELLED |
| 28 | 1× | } else if error.is_some() { |
| 29 | 0× | &ERRORS |
| 30 | 1× | } else if exit_code == Some(0) { |
| 31 | 1× | &PASSED |
| 32 | | } else { |
| 33 | 0× | &FAILED |
| 34 | | }; |
| 35 | 1× | counter.fetch_add(1, Relaxed); |
| 36 | 1× | JOB_MS.fetch_add(duration_ms, Relaxed); |
| 37 | 1× | } |
| 38 | |
|
| 39 | |
|
| 40 | 1× | pub fn render(name: &str, started: Instant) -> String { |
| 41 | 1× | let n = name.replace('\\', "\\\\").replace('"', "\\\""); |
| 42 | 1× | let mut out = String::new(); |
| 43 | 7× | let mut family = |metric: &str, help: &str, kind: &str, samples: &[(String, u64)]| { |
| 44 | 7× | out.push_str(&format!("# HELP {metric} {help}\n# TYPE {metric} {kind}\n")); |
| 45 | 10× | for (labels, v) in samples { |
| 46 | 10× | let sep = if labels.is_empty() { "" } else { "," }; |
| 47 | 10× | out.push_str(&format!("{metric}{{runner=\"{n}\"{sep}{labels}}} {v}\n")); |
| 48 | | } |
| 49 | 7× | }; |
| 50 | 1× | family("flow_runner_build_info", "This runner's version.", "gauge", &[(format!("version=\"{}\"", env!("CARGO_PKG_VERSION")), 1)]); |
| 51 | 1× | family("flow_runner_uptime_seconds", "Seconds since the runner started.", "gauge", &[(String::new(), started.elapsed().as_secs())]); |
| 52 | 1× | family("flow_runner_claims_total", "Jobs claimed from the hub.", "counter", &[(String::new(), CLAIMS.load(Relaxed))]); |
| 53 | 1× | family("flow_runner_declined_total", "Claimed jobs declined before running.", "counter", &[(String::new(), DECLINED.load(Relaxed))]); |
| 54 | 1× | family( |
| 55 | 1× | "flow_runner_jobs_finished_total", |
| 56 | 1× | "Finished jobs by result.", |
| 57 | 1× | "counter", |
| 58 | 1× | &[ |
| 59 | 1× | ("result=\"passed\"".into(), PASSED.load(Relaxed)), |
| 60 | 1× | ("result=\"failed\"".into(), FAILED.load(Relaxed)), |
| 61 | 1× | ("result=\"error\"".into(), ERRORS.load(Relaxed)), |
| 62 | 1× | ("result=\"cancelled\"".into(), CANCELLED.load(Relaxed)), |
| 63 | 1× | ], |
| 64 | 1× | ); |
| 65 | 1× | family( |
| 66 | 1× | "flow_runner_job_seconds_total", |
| 67 | 1× | "Seconds finished jobs ran, together.", |
| 68 | 1× | "counter", |
| 69 | 1× | &[(String::new(), JOB_MS.load(Relaxed) / 1000)], |
| 70 | 1× | ); |
| 71 | 1× | family("flow_runner_jobs_running", "Jobs running now.", "gauge", &[(String::new(), RUNNING.load(Relaxed))]); |
| 72 | 1× | out |
| 73 | 1× | } |
| 74 | |
|
| 75 | |
|
| 76 | |
|
| 77 | 1× | pub fn serve(addr: &str, token: Option<String>, name: String) { |
| 78 | 1× | let listener = match std::net::TcpListener::bind(addr) { |
| 79 | 1× | Ok(l) => l, |
| 80 | 0× | Err(e) => { |
| 81 | 0× | eprintln!("flow runner {name}: not serving metrics on {addr}: {e}"); |
| 82 | 0× | return; |
| 83 | | } |
| 84 | | }; |
| 85 | 1× | eprintln!("flow runner {name}: metrics on http://{addr}/metrics"); |
| 86 | 1× | let started = Instant::now(); |
| 87 | 1× | std::thread::spawn(move || { |
| 88 | 1× | for stream in listener.incoming().flatten() { |
| 89 | 1× | let _ = answer(stream, token.as_deref(), &name, started); |
| 90 | 1× | } |
| 91 | 1× | }); |
| 92 | 1× | } |
| 93 | |
|
| 94 | 2× | fn answer(mut stream: std::net::TcpStream, token: Option<&str>, name: &str, started: Instant) -> std::io::Result<()> { |
| 95 | 2× | stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?; |
| 96 | 2× | let mut reader = BufReader::new(stream.try_clone()?); |
| 97 | 2× | let mut line = String::new(); |
| 98 | 2× | reader.read_line(&mut line)?; |
| 99 | 2× | let path = line.split_whitespace().nth(1).unwrap_or("").to_string(); |
| 100 | 2× | let mut authorized = token.is_none(); |
| 101 | | loop { |
| 102 | 5× | let mut h = String::new(); |
| 103 | 5× | if reader.read_line(&mut h)? == 0 || h.trim().is_empty() { |
| 104 | 2× | break; |
| 105 | 3× | } |
| 106 | 3× | if let (Some(t), Some((k, v))) = (token, h.split_once(':')) { |
| 107 | 3× | if k.trim().eq_ignore_ascii_case("authorization") && v.trim() == format!("Bearer {t}") { |
| 108 | 1× | authorized = true; |
| 109 | 2× | } |
| 110 | 0× | } |
| 111 | | } |
| 112 | 2× | let (status, body) = match (path.as_str(), authorized) { |
| 113 | 2× | ("/metrics", true) => ("200 OK", render(name, started)), |
| 114 | 1× | ("/metrics", false) => ("401 Unauthorized", "a bearer token is needed\n".to_string()), |
| 115 | 0× | _ => ("404 Not Found", "not found\n".to_string()), |
| 116 | | }; |
| 117 | 2× | write!( |
| 118 | 2× | stream, |
| 119 | | "HTTP/1.1 {status}\r\nContent-Type: text/plain; version=0.0.4\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", |
| 120 | 2× | body.len() |
| 121 | | ) |
| 122 | 2× | } |
| 123 | |
|
| 124 | | #[cfg(test)] |
| 125 | | mod tests { |
| 126 | | use super::*; |
| 127 | | use std::io::Read; |
| 128 | |
|
| 129 | | #[test] |
| 130 | 1× | fn a_scrape_needs_the_token_and_reads_the_counters() { |
| 131 | 1× | let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); |
| 132 | 1× | let addr = l.local_addr().unwrap().to_string(); |
| 133 | 1× | drop(l); |
| 134 | 1× | serve(&addr, Some("t0k".into()), "earth-1".into()); |
| 135 | 1× | CLAIMS.fetch_add(1, Relaxed); |
| 136 | 1× | finished(Some(0), None, 2500, false); |
| 137 | 2× | let get = |auth: &str| { |
| 138 | 2× | let mut s = std::net::TcpStream::connect(&addr).unwrap(); |
| 139 | 2× | write!(s, "GET /metrics HTTP/1.1\r\nHost: x\r\n{auth}\r\n").unwrap(); |
| 140 | 2× | let mut body = String::new(); |
| 141 | 2× | s.read_to_string(&mut body).unwrap(); |
| 142 | 2× | body |
| 143 | 2× | }; |
| 144 | 1× | assert!(get("").starts_with("HTTP/1.1 401")); |
| 145 | 1× | let page = get("Authorization: Bearer t0k\r\n"); |
| 146 | 1× | assert!(page.starts_with("HTTP/1.1 200"), "{page}"); |
| 147 | 1× | assert!(page.contains("flow_runner_claims_total{runner=\"earth-1\"} ") && page.contains("result=\"passed\"}"), "{page}"); |
| 148 | 1× | } |
| 149 | | } |