From 3641dffeccc4d6eb14bdc146146a853f9de95f11 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 14 Mar 2026 09:29:14 +0100 Subject: [PATCH] Add comparison stats: failure rate %, vs-yesterday change, today total Stats endpoint now returns current + previous period (24h shift) values plus today's total count. UI shows: - Total Matches: "of 12.3K today" - Avg Duration: arrow + % vs yesterday - Failure Rate: percentage of errors vs total, arrow + % vs yesterday - P99 Latency: arrow + % vs yesterday - In-Flight: unchanged (running executions) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/search/ClickHouseSearchEngine.java | 38 +++++++++++++++++-- .../server/core/search/ExecutionStats.java | 20 ++++++---- ui/src/api/openapi.json | 24 ++++++++++++ ui/src/api/schema.d.ts | 6 +++ ui/src/pages/executions/ExecutionExplorer.tsx | 33 ++++++++++++++-- 5 files changed, 106 insertions(+), 15 deletions(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java index fe89222e..148ba81c 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java @@ -89,18 +89,48 @@ public class ClickHouseSearchEngine implements SearchEngine { @Override public ExecutionStats stats(Instant from, Instant to) { - return jdbcTemplate.queryForObject( - "SELECT countIf(status = 'FAILED') AS failed_count, " + + String aggregateSql = "SELECT count() AS total_count, " + + "countIf(status = 'FAILED') AS failed_count, " + "toInt64(avg(duration_ms)) AS avg_duration_ms, " + "toInt64(quantile(0.99)(duration_ms)) AS p99_duration_ms, " + "countIf(status = 'RUNNING') AS active_count " + - "FROM route_executions WHERE start_time >= ? AND start_time <= ?", - (rs, rowNum) -> new ExecutionStats( + "FROM route_executions WHERE start_time >= ? AND start_time <= ?"; + + // Current period + record PeriodStats(long totalCount, long failedCount, long avgDurationMs, long p99LatencyMs, long activeCount) {} + PeriodStats current = jdbcTemplate.queryForObject(aggregateSql, + (rs, rowNum) -> new PeriodStats( + rs.getLong("total_count"), rs.getLong("failed_count"), rs.getLong("avg_duration_ms"), rs.getLong("p99_duration_ms"), rs.getLong("active_count")), Timestamp.from(from), Timestamp.from(to)); + + // Previous period (same window shifted back 24h) + Duration window = Duration.between(from, to); + Instant prevFrom = from.minus(Duration.ofHours(24)); + Instant prevTo = prevFrom.plus(window); + PeriodStats prev = jdbcTemplate.queryForObject(aggregateSql, + (rs, rowNum) -> new PeriodStats( + rs.getLong("total_count"), + rs.getLong("failed_count"), + rs.getLong("avg_duration_ms"), + rs.getLong("p99_duration_ms"), + rs.getLong("active_count")), + Timestamp.from(prevFrom), Timestamp.from(prevTo)); + + // Today total (midnight UTC to now) + Instant todayStart = Instant.now().truncatedTo(java.time.temporal.ChronoUnit.DAYS); + Long totalToday = jdbcTemplate.queryForObject( + "SELECT count() FROM route_executions WHERE start_time >= ?", + Long.class, Timestamp.from(todayStart)); + + return new ExecutionStats( + current.totalCount, current.failedCount, current.avgDurationMs, + current.p99LatencyMs, current.activeCount, + totalToday != null ? totalToday : 0L, + prev.totalCount, prev.failedCount, prev.avgDurationMs, prev.p99LatencyMs); } @Override diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java index b4fac6b5..1579fa62 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java @@ -1,11 +1,17 @@ package com.cameleer3.server.core.search; /** - * Aggregate execution statistics within a time window. - * - * @param failedCount number of failed executions - * @param avgDurationMs average duration in milliseconds - * @param p99LatencyMs 99th percentile duration in milliseconds - * @param activeCount number of currently running executions + * Aggregate execution statistics within a time window, with comparison to the + * equivalent previous period (shifted back 24 h) and a "today" total. */ -public record ExecutionStats(long failedCount, long avgDurationMs, long p99LatencyMs, long activeCount) {} +public record ExecutionStats( + long totalCount, + long failedCount, + long avgDurationMs, + long p99LatencyMs, + long activeCount, + long totalToday, + long prevTotalCount, + long prevFailedCount, + long prevAvgDurationMs, + long prevP99LatencyMs) {} diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json index 1dab1a6e..814f7318 100644 --- a/ui/src/api/openapi.json +++ b/ui/src/api/openapi.json @@ -1049,6 +1049,10 @@ "ExecutionStats": { "type": "object", "properties": { + "totalCount": { + "type": "integer", + "format": "int64" + }, "failedCount": { "type": "integer", "format": "int64" @@ -1064,6 +1068,26 @@ "activeCount": { "type": "integer", "format": "int64" + }, + "totalToday": { + "type": "integer", + "format": "int64" + }, + "prevTotalCount": { + "type": "integer", + "format": "int64" + }, + "prevFailedCount": { + "type": "integer", + "format": "int64" + }, + "prevAvgDurationMs": { + "type": "integer", + "format": "int64" + }, + "prevP99LatencyMs": { + "type": "integer", + "format": "int64" } } }, diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index 5d52548a..28b67f04 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -217,10 +217,16 @@ export interface ProcessorNode { export type ProcessorSnapshot = Record; export interface ExecutionStats { + totalCount: number; failedCount: number; avgDurationMs: number; p99LatencyMs: number; activeCount: number; + totalToday: number; + prevTotalCount: number; + prevFailedCount: number; + prevAvgDurationMs: number; + prevP99LatencyMs: number; } export interface StatsTimeseries { diff --git a/ui/src/pages/executions/ExecutionExplorer.tsx b/ui/src/pages/executions/ExecutionExplorer.tsx index aa8fb084..4a860807 100644 --- a/ui/src/pages/executions/ExecutionExplorer.tsx +++ b/ui/src/pages/executions/ExecutionExplorer.tsx @@ -6,6 +6,20 @@ import { SearchFilters } from './SearchFilters'; import { ResultsTable } from './ResultsTable'; import styles from './ExecutionExplorer.module.css'; +function formatCompact(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; + return n.toLocaleString(); +} + +function pctChange(current: number, previous: number): { text: string; direction: 'up' | 'down' | 'neutral' } { + if (previous === 0) return { text: 'no prior data', direction: 'neutral' }; + const pct = ((current - previous) / previous) * 100; + if (Math.abs(pct) < 0.5) return { text: '~0% vs yesterday', direction: 'neutral' }; + const arrow = pct > 0 ? '\u2191' : '\u2193'; + return { text: `${arrow} ${Math.abs(pct).toFixed(1)}% vs yesterday`, direction: pct > 0 ? 'up' : 'down' }; +} + export function ExecutionExplorer() { const { toSearchRequest, offset, limit, setOffset, live, toggleLive } = useExecutionSearch(); const searchRequest = toSearchRequest(); @@ -24,6 +38,17 @@ export function ExecutionExplorer() { const total = data?.total ?? 0; const results = data?.data ?? []; + // Failure rate as percentage + const failureRate = stats && stats.totalCount > 0 + ? (stats.failedCount / stats.totalCount) * 100 : 0; + const prevFailureRate = stats && stats.prevTotalCount > 0 + ? (stats.prevFailedCount / stats.prevTotalCount) * 100 : 0; + + // Comparison vs yesterday + const avgChange = stats ? pctChange(stats.avgDurationMs, stats.prevAvgDurationMs) : null; + const failRateChange = stats ? pctChange(failureRate, prevFailureRate) : null; + const p99Change = stats ? pctChange(stats.p99LatencyMs, stats.prevP99LatencyMs) : null; + const showFrom = total > 0 ? offset + 1 : 0; const showTo = Math.min(offset + limit, total); @@ -43,10 +68,10 @@ export function ExecutionExplorer() { {/* Stats Bar */}
- - - - + + + +