diff --git a/databuild/service/handlers.rs b/databuild/service/handlers.rs index 6760bae..ec6bcd6 100644 --- a/databuild/service/handlers.rs +++ b/databuild/service/handlers.rs @@ -822,17 +822,27 @@ pub async fn list_jobs( // Original query but let's see all statuses let query = " + WITH job_durations AS ( + SELECT + je.job_label, + be.build_request_id, + (MAX(be.timestamp) - MIN(be.timestamp)) / 1000000 as duration_ms + FROM job_events je + JOIN build_events be ON je.event_id = be.event_id + GROUP BY je.job_label, be.build_request_id + HAVING MAX(CASE WHEN je.status IN ('3', '4', '5', '6') THEN 1 ELSE 0 END) = 1 + ) SELECT je.job_label, COUNT(CASE WHEN je.status IN ('3', '6') THEN 1 END) as completed_count, - COUNT(CASE WHEN je.status = '4' THEN 1 END) as failed_count, - COUNT(*) as total_count, - -- For now, skip duration calculation since we need start/end times - NULL as avg_duration_ms, + COUNT(CASE WHEN je.status IN ('4', '5') THEN 1 END) as failed_count, + COUNT(CASE WHEN je.status IN ('3', '4', '5', '6') THEN 1 END) as total_count, + COALESCE(AVG(jd.duration_ms), 0) as avg_duration_ms, MAX(be.timestamp) as last_run, GROUP_CONCAT(DISTINCT je.status) as all_statuses FROM job_events je JOIN build_events be ON je.event_id = be.event_id + LEFT JOIN job_durations jd ON je.job_label = jd.job_label WHERE je.job_label != '' GROUP BY je.job_label ORDER BY last_run DESC"; @@ -855,7 +865,7 @@ pub async fn list_jobs( let completed_count: u32 = row[1].parse().unwrap_or(0); let failed_count: u32 = row[2].parse().unwrap_or(0); let total_count: u32 = row[3].parse().unwrap_or(0); - let avg_duration_ms: Option = row[4].parse().ok(); + let avg_duration_ms: Option = row[4].parse::().ok().map(|f| f as i64); let last_run: Option = row[5].parse().ok(); let all_statuses = &row[6]; @@ -919,21 +929,31 @@ pub async fn get_job_metrics( // Get overall job metrics let metrics_query = " + WITH job_run_durations AS ( + SELECT + be.build_request_id, + (MAX(be.timestamp) - MIN(be.timestamp)) / 1000000 as duration_ms + FROM job_events je + JOIN build_events be ON je.event_id = be.event_id + WHERE je.job_label = ? + GROUP BY be.build_request_id + HAVING MAX(CASE WHEN je.status IN ('3', '4', '5', '6') THEN 1 ELSE 0 END) = 1 + ) SELECT COUNT(CASE WHEN je.status IN ('3', '6') THEN 1 END) as completed_count, - COUNT(*) as total_count, - -- Skip duration calculation for now - NULL as avg_duration_ms + COUNT(CASE WHEN je.status IN ('3', '4', '5', '6') THEN 1 END) as total_count, + COALESCE(AVG(jrd.duration_ms), 0) as avg_duration_ms FROM job_events je JOIN build_events be ON je.event_id = be.event_id + LEFT JOIN job_run_durations jrd ON be.build_request_id = jrd.build_request_id WHERE je.job_label = ?"; - let (success_rate, total_runs, avg_duration_ms) = match service.event_log.execute_query(&metrics_query.replace("?", &format!("'{}'", decoded_label))).await { + let (success_rate, total_runs, avg_duration_ms) = match service.event_log.execute_query(&metrics_query.replace("?", &format!("'{}'", decoded_label)).replace("?", &format!("'{}'", decoded_label))).await { Ok(result) if !result.rows.is_empty() => { let row = &result.rows[0]; let completed_count: u32 = row[0].parse().unwrap_or(0); let total_count: u32 = row[1].parse().unwrap_or(0); - let avg_duration: Option = row[2].parse().ok(); + let avg_duration: Option = row[2].parse::().ok().map(|f| f as i64); let success_rate = if total_count > 0 { completed_count as f64 / total_count as f64 @@ -946,28 +966,43 @@ pub async fn get_job_metrics( _ => (0.0, 0, None), }; - // Get recent runs + // Get recent runs - consolidated by build request to show final status per job run let recent_runs_query = " - SELECT DISTINCT + SELECT be.build_request_id, je.target_partitions, je.status, - be.timestamp, - (julianday('now') - julianday(be.timestamp/1000000000, 'unixepoch')) * 24 * 60 * 60 * 1000 as duration_ms + MIN(be.timestamp) as started_at, + MAX(be.timestamp) as completed_at FROM job_events je JOIN build_events be ON je.event_id = be.event_id WHERE je.job_label = ? - ORDER BY be.timestamp DESC + GROUP BY be.build_request_id, je.target_partitions + HAVING je.status = ( + SELECT je2.status + FROM job_events je2 + JOIN build_events be2 ON je2.event_id = be2.event_id + WHERE je2.job_label = ? + AND be2.build_request_id = be.build_request_id + ORDER BY be2.timestamp DESC + LIMIT 1 + ) + ORDER BY started_at DESC LIMIT 50"; - let recent_runs = match service.event_log.execute_query(&recent_runs_query.replace("?", &format!("'{}'", decoded_label))).await { + let recent_runs = match service.event_log.execute_query(&recent_runs_query.replace("?", &format!("'{}'", decoded_label)).replace("?", &format!("'{}'", decoded_label))).await { Ok(result) => { result.rows.into_iter().map(|row| { let build_request_id = row[0].clone(); let partitions_json: String = row[1].clone(); let status_code: String = row[2].clone(); let started_at: i64 = row[3].parse().unwrap_or(0); - let duration_ms: Option = row[4].parse().ok(); + let completed_at: i64 = row[4].parse().unwrap_or(started_at); + let duration_ms: Option = if completed_at > started_at { + Some((completed_at - started_at) / 1_000_000) // Convert nanoseconds to milliseconds + } else { + None + }; let partitions: Vec = serde_json::from_str::>(&partitions_json) .unwrap_or_default() @@ -983,6 +1018,7 @@ pub async fn get_job_metrics( "3" => "completed", "4" => "failed", "5" => "cancelled", + "6" => "skipped", _ => "unknown", }; @@ -1000,26 +1036,38 @@ pub async fn get_job_metrics( // Get daily stats (simplified - just recent days) let daily_stats_query = " + WITH daily_job_durations AS ( + SELECT + date(be.timestamp/1000000000, 'unixepoch') as date, + be.build_request_id, + (MAX(be.timestamp) - MIN(be.timestamp)) / 1000000 as duration_ms + FROM job_events je + JOIN build_events be ON je.event_id = be.event_id + WHERE je.job_label = ? + AND be.timestamp > (strftime('%s', 'now', '-30 days') * 1000000000) + GROUP BY date(be.timestamp/1000000000, 'unixepoch'), be.build_request_id + HAVING MAX(CASE WHEN je.status IN ('3', '4', '5', '6') THEN 1 ELSE 0 END) = 1 + ) SELECT date(be.timestamp/1000000000, 'unixepoch') as date, COUNT(CASE WHEN je.status IN ('3', '6') THEN 1 END) as completed_count, - COUNT(*) as total_count, - -- Skip duration calculation for now - NULL as avg_duration_ms + COUNT(CASE WHEN je.status IN ('3', '4', '5', '6') THEN 1 END) as total_count, + COALESCE(AVG(djd.duration_ms), 0) as avg_duration_ms FROM job_events je JOIN build_events be ON je.event_id = be.event_id + LEFT JOIN daily_job_durations djd ON date(be.timestamp/1000000000, 'unixepoch') = djd.date WHERE je.job_label = ? AND be.timestamp > (strftime('%s', 'now', '-30 days') * 1000000000) GROUP BY date(be.timestamp/1000000000, 'unixepoch') ORDER BY date DESC"; - let daily_stats = match service.event_log.execute_query(&daily_stats_query.replace("?", &format!("'{}'", decoded_label))).await { + let daily_stats = match service.event_log.execute_query(&daily_stats_query.replace("?", &format!("'{}'", decoded_label)).replace("?", &format!("'{}'", decoded_label))).await { Ok(result) => { result.rows.into_iter().map(|row| { let date = row[0].clone(); let completed_count: u32 = row[1].parse().unwrap_or(0); let total_count: u32 = row[2].parse().unwrap_or(0); - let avg_duration: Option = row[3].parse().ok(); + let avg_duration: Option = row[3].parse::().ok().map(|f| f as i64); let success_rate = if total_count > 0 { completed_count as f64 / total_count as f64