From f6123b8a7c56d85ae7882c0c578197e6116f94a9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:13:52 +0200 Subject: [PATCH] fix: use explicit UTC formatting in ClickHouse DateTime literals Timestamp.toString() uses JVM local timezone which can mismatch with ClickHouse's UTC timezone, causing time-filtered queries to return empty results. Replaced with DateTimeFormatter.withZone(UTC) in all lit() methods. Also added warn logging to RouteCatalogController catch blocks to surface query errors instead of silently swallowing them. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/controller/AgentRegistrationController.java | 7 +++---- .../app/controller/RouteCatalogController.java | 13 +++++++------ .../app/controller/RouteMetricsController.java | 7 +++---- .../server/app/storage/ClickHouseStatsStore.java | 9 +++------ 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java index 7c89c8e5..83e7f9a9 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java @@ -297,9 +297,8 @@ public class AgentRegistrationController { /** Format an Instant as a ClickHouse DateTime literal. */ private static String lit(Instant instant) { - Instant truncated = instant.truncatedTo(ChronoUnit.SECONDS); - String ts = new Timestamp(truncated.toEpochMilli()).toString(); - if (ts.endsWith(".0")) ts = ts.substring(0, ts.length() - 2); - return "'" + ts + "'"; + return "'" + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withZone(java.time.ZoneOffset.UTC) + .format(instant.truncatedTo(ChronoUnit.SECONDS)) + "'"; } } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java index f6780a7d..6486891e 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java @@ -35,6 +35,8 @@ import java.util.stream.Collectors; @Tag(name = "Route Catalog", description = "Route catalog and discovery") public class RouteCatalogController { + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RouteCatalogController.class); + private final AgentRegistryService registryService; private final DiagramStore diagramStore; private final JdbcTemplate jdbc; @@ -94,7 +96,7 @@ public class RouteCatalogController { if (ts != null) routeLastSeen.put(key, ts.toInstant()); }); } catch (Exception e) { - // AggregatingMergeTree table may not exist yet + log.warn("Failed to query route exchange counts: {}", e.getMessage()); } // Per-agent TPS from the last minute @@ -157,12 +159,11 @@ public class RouteCatalogController { .orElse(null); } - /** Format an Instant as a ClickHouse DateTime literal. */ + /** Format an Instant as a ClickHouse DateTime literal in UTC. */ private static String lit(Instant instant) { - Instant truncated = instant.truncatedTo(ChronoUnit.SECONDS); - String ts = new Timestamp(truncated.toEpochMilli()).toString(); - if (ts.endsWith(".0")) ts = ts.substring(0, ts.length() - 2); - return "'" + ts + "'"; + return "'" + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withZone(java.time.ZoneOffset.UTC) + .format(instant.truncatedTo(ChronoUnit.SECONDS)) + "'"; } private String computeWorstHealth(List agents) { diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteMetricsController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteMetricsController.java index 69c14843..99e63041 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteMetricsController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteMetricsController.java @@ -187,10 +187,9 @@ public class RouteMetricsController { /** Format an Instant as a ClickHouse DateTime literal. */ private static String lit(Instant instant) { - Instant truncated = instant.truncatedTo(ChronoUnit.SECONDS); - String ts = new Timestamp(truncated.toEpochMilli()).toString(); - if (ts.endsWith(".0")) ts = ts.substring(0, ts.length() - 2); - return "'" + ts + "'"; + return "'" + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withZone(java.time.ZoneOffset.UTC) + .format(instant.truncatedTo(ChronoUnit.SECONDS)) + "'"; } /** Format a string as a SQL literal with single-quote escaping. */ diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/ClickHouseStatsStore.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/ClickHouseStatsStore.java index 1d8ddcd1..5ab8df55 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/ClickHouseStatsStore.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/ClickHouseStatsStore.java @@ -299,12 +299,9 @@ public class ClickHouseStatsStore implements StatsStore { * column compatibility. */ private static String lit(Instant instant) { - // Truncate to seconds — ClickHouse DateTime has second precision - Instant truncated = instant.truncatedTo(ChronoUnit.SECONDS); - String ts = new Timestamp(truncated.toEpochMilli()).toString(); - // Remove trailing ".0" that Timestamp.toString() always appends - if (ts.endsWith(".0")) ts = ts.substring(0, ts.length() - 2); - return "'" + ts + "'"; + return "'" + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withZone(java.time.ZoneOffset.UTC) + .format(instant.truncatedTo(ChronoUnit.SECONDS)) + "'"; } /** Format a string as a SQL literal with single-quote escaping. */