From f82aa2637177a6b5332edc41adaf5bc758d3a1f2 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:18:06 +0200 Subject: [PATCH] fix: improve ClickHouse admin page, fix AgentHealth type error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite ClickHouse admin to show useful storage metrics instead of often-empty system.events data. Add active queries section. - Replace performance endpoint: query system.parts for disk size, uncompressed size, compression ratio, total rows, part count - Add /queries endpoint querying system.processes for active queries - Frontend: storage overview strip, tables with total size, active queries DataTable - Fix AgentHealth.tsx type: agentId → instanceId in inline type cast Co-Authored-By: Claude Opus 4.6 (1M context) --- .../controller/ClickHouseAdminController.java | 82 +++++++++++++++---- .../dto/ClickHousePerformanceResponse.java | 12 +-- .../server/app/dto/ClickHouseQueryInfo.java | 12 +++ ui/src/api/queries/admin/clickhouse.ts | 25 +++++- .../Admin/ClickHouseAdminPage.module.css | 5 ++ ui/src/pages/Admin/ClickHouseAdminPage.tsx | 58 ++++++++++--- ui/src/pages/AgentHealth/AgentHealth.tsx | 2 +- 7 files changed, 159 insertions(+), 37 deletions(-) create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ClickHouseQueryInfo.java diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClickHouseAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClickHouseAdminController.java index 78d3a447..0dfc98c5 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClickHouseAdminController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClickHouseAdminController.java @@ -1,6 +1,7 @@ package com.cameleer3.server.app.controller; import com.cameleer3.server.app.dto.ClickHousePerformanceResponse; +import com.cameleer3.server.app.dto.ClickHouseQueryInfo; import com.cameleer3.server.app.dto.ClickHouseStatusResponse; import com.cameleer3.server.app.dto.ClickHouseTableInfo; import com.cameleer3.server.app.dto.IndexerPipelineResponse; @@ -80,20 +81,72 @@ public class ClickHouseAdminController { } @GetMapping("/performance") - @Operation(summary = "ClickHouse performance metrics") + @Operation(summary = "ClickHouse storage and performance metrics") public ClickHousePerformanceResponse getPerformance() { try { - long selectQueries = queryEvent("SelectQuery"); - long insertQueries = queryEvent("InsertQuery"); - long insertedRows = queryEvent("InsertedRows"); - long readRows = queryEvent("SelectedRows"); - String memoryUsage = clickHouseJdbc.queryForObject( - "SELECT formatReadableSize(value) FROM system.metrics WHERE metric = 'MemoryTracking'", - String.class); - return new ClickHousePerformanceResponse(selectQueries, insertQueries, - memoryUsage != null ? memoryUsage : "0 B", insertedRows, readRows); + var row = clickHouseJdbc.queryForMap(""" + SELECT + formatReadableSize(sum(bytes_on_disk)) AS disk_size, + formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size, + if(sum(data_uncompressed_bytes) > 0, + round(sum(bytes_on_disk) / sum(data_uncompressed_bytes), 3), 0) AS compression_ratio, + sum(rows) AS total_rows, + count() AS part_count + FROM system.parts + WHERE database = currentDatabase() AND active + """); + + String memory = "N/A"; + try { + memory = clickHouseJdbc.queryForObject( + "SELECT formatReadableSize(value) FROM system.metrics WHERE metric = 'MemoryTracking'", + String.class); + } catch (Exception ignored) {} + + int currentQueries = 0; + try { + Integer q = clickHouseJdbc.queryForObject( + "SELECT toInt32(value) FROM system.metrics WHERE metric = 'Query'", + Integer.class); + if (q != null) currentQueries = q; + } catch (Exception ignored) {} + + return new ClickHousePerformanceResponse( + (String) row.get("disk_size"), + (String) row.get("uncompressed_size"), + ((Number) row.get("compression_ratio")).doubleValue(), + ((Number) row.get("total_rows")).longValue(), + ((Number) row.get("part_count")).intValue(), + memory != null ? memory : "N/A", + currentQueries); } catch (Exception e) { - return new ClickHousePerformanceResponse(0, 0, "N/A", 0, 0); + return new ClickHousePerformanceResponse("N/A", "N/A", 0, 0, 0, "N/A", 0); + } + } + + @GetMapping("/queries") + @Operation(summary = "Active ClickHouse queries") + public List getQueries() { + try { + return clickHouseJdbc.query(""" + SELECT + query_id, + round(elapsed, 2) AS elapsed_seconds, + formatReadableSize(memory_usage) AS memory, + read_rows, + substring(query, 1, 200) AS query + FROM system.processes + WHERE is_initial_query = 1 + ORDER BY elapsed DESC + """, + (rs, rowNum) -> new ClickHouseQueryInfo( + rs.getString("query_id"), + rs.getDouble("elapsed_seconds"), + rs.getString("memory"), + rs.getLong("read_rows"), + rs.getString("query"))); + } catch (Exception e) { + return List.of(); } } @@ -109,11 +162,4 @@ public class ClickHouseAdminController { indexerStats.getIndexingRate(), indexerStats.getLastIndexedAt()); } - - private long queryEvent(String eventName) { - Long val = clickHouseJdbc.queryForObject( - "SELECT value FROM system.events WHERE event = ?", - Long.class, eventName); - return val != null ? val : 0; - } } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ClickHousePerformanceResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ClickHousePerformanceResponse.java index 5c85c2c0..bd9d34fd 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ClickHousePerformanceResponse.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ClickHousePerformanceResponse.java @@ -2,11 +2,13 @@ package com.cameleer3.server.app.dto; import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "ClickHouse performance metrics") +@Schema(description = "ClickHouse storage and performance metrics") public record ClickHousePerformanceResponse( - long queryCount, - long insertQueryCount, + String diskSize, + String uncompressedSize, + double compressionRatio, + long totalRows, + int partCount, String memoryUsage, - long insertedRows, - long readRows + int currentQueries ) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ClickHouseQueryInfo.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ClickHouseQueryInfo.java new file mode 100644 index 00000000..d92595f9 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ClickHouseQueryInfo.java @@ -0,0 +1,12 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Active ClickHouse query information") +public record ClickHouseQueryInfo( + String queryId, + double elapsedSeconds, + String memory, + long readRows, + String query +) {} diff --git a/ui/src/api/queries/admin/clickhouse.ts b/ui/src/api/queries/admin/clickhouse.ts index d8aeb687..e7f904f6 100644 --- a/ui/src/api/queries/admin/clickhouse.ts +++ b/ui/src/api/queries/admin/clickhouse.ts @@ -21,11 +21,21 @@ export interface ClickHouseTableInfo { } export interface ClickHousePerformance { - queryCount: number; - insertQueryCount: number; + diskSize: string; + uncompressedSize: string; + compressionRatio: number; + totalRows: number; + partCount: number; memoryUsage: string; - insertedRows: number; + currentQueries: number; +} + +export interface ClickHouseQuery { + queryId: string; + elapsedSeconds: number; + memory: string; readRows: number; + query: string; } export interface IndexerPipeline { @@ -67,6 +77,15 @@ export function useClickHousePerformance() { }); } +export function useClickHouseQueries() { + const refetchInterval = useRefreshInterval(5_000); + return useQuery({ + queryKey: ['admin', 'clickhouse', 'queries'], + queryFn: () => adminFetch('/clickhouse/queries'), + refetchInterval, + }); +} + export function useIndexerPipeline() { const refetchInterval = useRefreshInterval(10_000); return useQuery({ diff --git a/ui/src/pages/Admin/ClickHouseAdminPage.module.css b/ui/src/pages/Admin/ClickHouseAdminPage.module.css index ed91ecd4..4c8f554e 100644 --- a/ui/src/pages/Admin/ClickHouseAdminPage.module.css +++ b/ui/src/pages/Admin/ClickHouseAdminPage.module.css @@ -61,3 +61,8 @@ color: var(--text-muted); font-family: var(--font-mono); } + +.queryText { + font-size: 0.75rem; + font-family: var(--font-mono); +} diff --git a/ui/src/pages/Admin/ClickHouseAdminPage.tsx b/ui/src/pages/Admin/ClickHouseAdminPage.tsx index 7bf4672f..0bc3b4a1 100644 --- a/ui/src/pages/Admin/ClickHouseAdminPage.tsx +++ b/ui/src/pages/Admin/ClickHouseAdminPage.tsx @@ -1,15 +1,19 @@ import { StatCard, DataTable, ProgressBar } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; -import { useClickHouseStatus, useClickHouseTables, useClickHousePerformance, useIndexerPipeline } from '../../api/queries/admin/clickhouse'; +import { useClickHouseStatus, useClickHouseTables, useClickHousePerformance, useClickHouseQueries, useIndexerPipeline } from '../../api/queries/admin/clickhouse'; import styles from './ClickHouseAdminPage.module.css'; export default function ClickHouseAdminPage() { const { data: status, isError: statusError } = useClickHouseStatus(); const { data: tables } = useClickHouseTables(); const { data: perf } = useClickHousePerformance(); + const { data: queries } = useClickHouseQueries(); const { data: pipeline } = useIndexerPipeline(); const unreachable = statusError || (status && !status.reachable); + const totalSize = (tables || []).reduce((sum, t) => sum + (t.dataSizeBytes || 0), 0); + const totalSizeLabel = totalSize > 0 ? formatBytes(totalSize) : ''; + const tableColumns: Column[] = [ { key: 'name', header: 'Table', sortable: true }, { key: 'engine', header: 'Engine' }, @@ -18,14 +22,35 @@ export default function ClickHouseAdminPage() { { key: 'partitionCount', header: 'Partitions', sortable: true }, ]; + const queryColumns: Column[] = [ + { key: 'elapsedSeconds', header: 'Elapsed', render: (v) => `${v}s` }, + { key: 'memory', header: 'Memory' }, + { key: 'readRows', header: 'Rows Read', render: (v) => Number(v).toLocaleString() }, + { key: 'query', header: 'Query', render: (v) => {String(v).slice(0, 100)} }, + ]; + return (
+ {/* Status */}
+ {/* Storage overview */} + {perf && ( +
+ + + + + + +
+ )} + + {/* Pipeline */} {pipeline && (
Indexer Pipeline
@@ -39,18 +64,11 @@ export default function ClickHouseAdminPage() {
)} - {perf && ( -
- - - - -
- )} - + {/* Tables */}
Tables ({(tables || []).length}) + {totalSizeLabel && {totalSizeLabel} total}
+ + {/* Active Queries */} +
+
+ Active Queries ({(queries || []).length}) +
+ ({ ...q, id: q.queryId || String(i) }))} + flush + /> +
); } + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const val = bytes / Math.pow(1024, i); + return `${val.toFixed(val < 10 ? 2 : 1)} ${units[i]}`; +} diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index b897111d..779cde70 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -189,7 +189,7 @@ export default function AgentHealth() { // Map events to FeedEvent const feedEvents: FeedEvent[] = useMemo(() => { - const mapped = (events ?? []).map((e: { id: number; agentId: string; eventType: string; detail: string; timestamp: string }) => ({ + const mapped = (events ?? []).map((e: { id: number; instanceId: string; eventType: string; detail: string; timestamp: string }) => ({ id: String(e.id), severity: e.eventType === 'WENT_DEAD'