feat(logs): widen source filter to multi-value OR list
Replaces LogSearchRequest.source (String) with sources (List<String>) and emits 'source IN (...)' when non-empty. LogQueryController parses ?source=a,b,c the same way it parses ?level=a,b,c.
This commit is contained in:
@@ -61,12 +61,20 @@ public class LogQueryController {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> 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 fromInstant = from != null ? Instant.parse(from) : null;
|
||||||
Instant toInstant = to != null ? Instant.parse(to) : null;
|
Instant toInstant = to != null ? Instant.parse(to) : null;
|
||||||
|
|
||||||
LogSearchRequest request = new LogSearchRequest(
|
LogSearchRequest request = new LogSearchRequest(
|
||||||
searchText, levels, application, instanceId, exchangeId,
|
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);
|
LogSearchResponse result = logIndex.search(request);
|
||||||
|
|
||||||
|
|||||||
@@ -146,9 +146,12 @@ public class ClickHouseLogStore implements LogIndex {
|
|||||||
baseParams.add("%" + escapeLike(request.logger()) + "%");
|
baseParams.add("%" + escapeLike(request.logger()) + "%");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.source() != null && !request.source().isEmpty()) {
|
if (request.sources() != null && !request.sources().isEmpty()) {
|
||||||
baseConditions.add("source = ?");
|
String placeholders = String.join(", ", Collections.nCopies(request.sources().size(), "?"));
|
||||||
baseParams.add(request.source());
|
baseConditions.add("source IN (" + placeholders + ")");
|
||||||
|
for (String s : request.sources()) {
|
||||||
|
baseParams.add(s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.from() != null) {
|
if (request.from() != null) {
|
||||||
|
|||||||
@@ -323,4 +323,52 @@ class ClickHouseLogStoreIT {
|
|||||||
String.class);
|
String.class);
|
||||||
assertThat(customVal).isEqualTo("custom-value");
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import java.util.List;
|
|||||||
* Immutable search criteria for querying application logs.
|
* Immutable search criteria for querying application logs.
|
||||||
*
|
*
|
||||||
* @param q free-text search across message and stack trace
|
* @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 application application ID filter (nullable = all apps)
|
||||||
* @param instanceId agent instance ID filter
|
* @param instanceId agent instance ID filter
|
||||||
* @param exchangeId Camel exchange ID filter
|
* @param exchangeId Camel exchange ID filter
|
||||||
* @param logger logger name substring filter
|
* @param logger logger name substring filter
|
||||||
* @param environment optional environment filter (e.g. "dev", "staging", "prod")
|
* @param environment optional environment filter (e.g. "dev", "staging", "prod")
|
||||||
* @param source optional source filter: "app" or "agent"
|
* @param sources optional source filter (e.g. ["app","container","agent"]), OR-joined
|
||||||
* @param from inclusive start of time range (required)
|
* @param from inclusive start of time range
|
||||||
* @param to inclusive end of time range (required)
|
* @param to inclusive end of time range
|
||||||
* @param cursor ISO timestamp cursor for keyset pagination
|
* @param cursor ISO timestamp cursor for keyset pagination
|
||||||
* @param limit page size (1-500, default 100)
|
* @param limit page size (1-500, default 100)
|
||||||
* @param sort sort direction: "asc" or "desc" (default "desc")
|
* @param sort sort direction: "asc" or "desc" (default "desc")
|
||||||
@@ -28,7 +28,7 @@ public record LogSearchRequest(
|
|||||||
String exchangeId,
|
String exchangeId,
|
||||||
String logger,
|
String logger,
|
||||||
String environment,
|
String environment,
|
||||||
String source,
|
List<String> sources,
|
||||||
Instant from,
|
Instant from,
|
||||||
Instant to,
|
Instant to,
|
||||||
String cursor,
|
String cursor,
|
||||||
@@ -44,5 +44,6 @@ public record LogSearchRequest(
|
|||||||
if (limit > MAX_LIMIT) limit = MAX_LIMIT;
|
if (limit > MAX_LIMIT) limit = MAX_LIMIT;
|
||||||
if (sort == null || !"asc".equalsIgnoreCase(sort)) sort = "desc";
|
if (sort == null || !"asc".equalsIgnoreCase(sort)) sort = "desc";
|
||||||
if (levels == null) levels = List.of();
|
if (levels == null) levels = List.of();
|
||||||
|
if (sources == null) sources = List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user