From 769752a3270a21717adc819d01e0973280216806 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:48:10 +0200 Subject: [PATCH] feat(logs): widen source filter to multi-value OR list Replaces LogSearchRequest.source (String) with sources (List) and emits 'source IN (...)' when non-empty. LogQueryController parses ?source=a,b,c the same way it parses ?level=a,b,c. --- .../app/controller/LogQueryController.java | 10 +++- .../server/app/search/ClickHouseLogStore.java | 9 ++-- .../app/search/ClickHouseLogStoreIT.java | 48 +++++++++++++++++++ .../server/core/search/LogSearchRequest.java | 11 +++-- 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LogQueryController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LogQueryController.java index c61d3377..950a669f 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LogQueryController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LogQueryController.java @@ -61,12 +61,20 @@ public class LogQueryController { .toList(); } + List sources = List.of(); + if (source != null && !source.isEmpty()) { + sources = Arrays.stream(source.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + } + Instant fromInstant = from != null ? Instant.parse(from) : null; Instant toInstant = to != null ? Instant.parse(to) : null; LogSearchRequest request = new LogSearchRequest( searchText, levels, application, instanceId, exchangeId, - logger, env.slug(), source, fromInstant, toInstant, cursor, limit, sort); + logger, env.slug(), sources, fromInstant, toInstant, cursor, limit, sort); LogSearchResponse result = logIndex.search(request); diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseLogStore.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseLogStore.java index ca09ad99..47b220e5 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseLogStore.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseLogStore.java @@ -146,9 +146,12 @@ public class ClickHouseLogStore implements LogIndex { baseParams.add("%" + escapeLike(request.logger()) + "%"); } - if (request.source() != null && !request.source().isEmpty()) { - baseConditions.add("source = ?"); - baseParams.add(request.source()); + if (request.sources() != null && !request.sources().isEmpty()) { + String placeholders = String.join(", ", Collections.nCopies(request.sources().size(), "?")); + baseConditions.add("source IN (" + placeholders + ")"); + for (String s : request.sources()) { + baseParams.add(s); + } } if (request.from() != null) { diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/search/ClickHouseLogStoreIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/search/ClickHouseLogStoreIT.java index 7c961189..caaa761b 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/search/ClickHouseLogStoreIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/search/ClickHouseLogStoreIT.java @@ -323,4 +323,52 @@ class ClickHouseLogStoreIT { String.class); assertThat(customVal).isEqualTo("custom-value"); } + + @Test + void search_bySources_singleValue_filtersCorrectly() { + Instant now = Instant.parse("2026-03-31T12:00:00Z"); + // "source" column is populated by indexBatch via LogEntry.getSource(); default is "app" when null. + // Force one row to "container" via a direct insert to avoid coupling to LogEntry constructor. + store.indexBatch("agent-1", "my-app", List.of( + entry(now, "INFO", "logger", "app msg", "t1", null, null) + )); + jdbc.update("INSERT INTO logs (tenant_id, environment, timestamp, application, instance_id, level, " + + "logger_name, message, thread_name, stack_trace, exchange_id, mdc, source) VALUES " + + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "default", "default", java.sql.Timestamp.from(now.plusSeconds(1)), "my-app", "agent-1", + "INFO", "logger", "container msg", "t1", "", "", java.util.Map.of(), "container"); + + LogSearchResponse result = store.search(new LogSearchRequest( + null, null, "my-app", null, null, null, null, + List.of("container"), null, null, null, 100, "desc")); + + assertThat(result.data()).hasSize(1); + assertThat(result.data().get(0).message()).isEqualTo("container msg"); + } + + @Test + void search_bySources_multiValue_joinsAsOr() { + Instant now = Instant.parse("2026-03-31T12:00:00Z"); + store.indexBatch("agent-1", "my-app", List.of( + entry(now, "INFO", "logger", "app msg", "t1", null, null) + )); + jdbc.update("INSERT INTO logs (tenant_id, environment, timestamp, application, instance_id, level, " + + "logger_name, message, thread_name, stack_trace, exchange_id, mdc, source) VALUES " + + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "default", "default", java.sql.Timestamp.from(now.plusSeconds(1)), "my-app", "agent-1", + "INFO", "logger", "container msg", "t1", "", "", java.util.Map.of(), "container"); + jdbc.update("INSERT INTO logs (tenant_id, environment, timestamp, application, instance_id, level, " + + "logger_name, message, thread_name, stack_trace, exchange_id, mdc, source) VALUES " + + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "default", "default", java.sql.Timestamp.from(now.plusSeconds(2)), "my-app", "agent-1", + "INFO", "logger", "agent msg", "t1", "", "", java.util.Map.of(), "agent"); + + LogSearchResponse result = store.search(new LogSearchRequest( + null, null, "my-app", null, null, null, null, + List.of("app", "container"), null, null, null, 100, "desc")); + + assertThat(result.data()).hasSize(2); + assertThat(result.data()).extracting(LogEntryResult::message) + .containsExactlyInAnyOrder("app msg", "container msg"); + } } diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/search/LogSearchRequest.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/search/LogSearchRequest.java index f14e23dc..e6a45315 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/search/LogSearchRequest.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/search/LogSearchRequest.java @@ -7,15 +7,15 @@ import java.util.List; * Immutable search criteria for querying application logs. * * @param q free-text search across message and stack trace - * @param levels log level filter (e.g. ["WARN","ERROR"]) + * @param levels log level filter (e.g. ["WARN","ERROR"]), OR-joined * @param application application ID filter (nullable = all apps) * @param instanceId agent instance ID filter * @param exchangeId Camel exchange ID filter * @param logger logger name substring filter * @param environment optional environment filter (e.g. "dev", "staging", "prod") - * @param source optional source filter: "app" or "agent" - * @param from inclusive start of time range (required) - * @param to inclusive end of time range (required) + * @param sources optional source filter (e.g. ["app","container","agent"]), OR-joined + * @param from inclusive start of time range + * @param to inclusive end of time range * @param cursor ISO timestamp cursor for keyset pagination * @param limit page size (1-500, default 100) * @param sort sort direction: "asc" or "desc" (default "desc") @@ -28,7 +28,7 @@ public record LogSearchRequest( String exchangeId, String logger, String environment, - String source, + List sources, Instant from, Instant to, String cursor, @@ -44,5 +44,6 @@ public record LogSearchRequest( if (limit > MAX_LIMIT) limit = MAX_LIMIT; if (sort == null || !"asc".equalsIgnoreCase(sort)) sort = "desc"; if (levels == null) levels = List.of(); + if (sources == null) sources = List.of(); } }