feat(logs): add instanceIds multi-value filter to /logs endpoint
Adds List<String> instanceIds to LogSearchRequest (null-normalized to List.of() in compact ctor) and generates an IN clause in both ClickHouseLogStore.search() and countLogs(), mirroring the existing sources pattern. LogQueryController parses ?instanceIds= as a comma-split list. All existing LogSearchRequest call sites updated. New ClickHouseLogStoreInstanceIdsIT covers: multi-value filter, empty filter (all rows), null filter (all rows), single-value filter, and coexistence with the singular instanceId field. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,7 +61,8 @@ public class LogPatternEvaluator implements ConditionEvaluator<LogPatternConditi
|
|||||||
to,
|
to,
|
||||||
null, // cursor
|
null, // cursor
|
||||||
1, // limit (count query; value irrelevant)
|
1, // limit (count query; value irrelevant)
|
||||||
"desc" // sort
|
"desc", // sort
|
||||||
|
null // instanceIds
|
||||||
);
|
);
|
||||||
return logStore.countLogs(req);
|
return logStore.countLogs(req);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public class LogQueryController {
|
|||||||
@RequestParam(required = false) String exchangeId,
|
@RequestParam(required = false) String exchangeId,
|
||||||
@RequestParam(required = false) String logger,
|
@RequestParam(required = false) String logger,
|
||||||
@RequestParam(required = false) String source,
|
@RequestParam(required = false) String source,
|
||||||
|
@RequestParam(required = false) String instanceIds,
|
||||||
@RequestParam(required = false) String from,
|
@RequestParam(required = false) String from,
|
||||||
@RequestParam(required = false) String to,
|
@RequestParam(required = false) String to,
|
||||||
@RequestParam(required = false) String cursor,
|
@RequestParam(required = false) String cursor,
|
||||||
@@ -69,12 +70,21 @@ public class LogQueryController {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> instanceIdList = List.of();
|
||||||
|
if (instanceIds != null && !instanceIds.isEmpty()) {
|
||||||
|
instanceIdList = Arrays.stream(instanceIds.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(), sources, fromInstant, toInstant, cursor, limit, sort);
|
logger, env.slug(), sources, fromInstant, toInstant, cursor, limit, sort,
|
||||||
|
instanceIdList);
|
||||||
|
|
||||||
LogSearchResponse result = logIndex.search(request);
|
LogSearchResponse result = logIndex.search(request);
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,14 @@ public class ClickHouseLogStore implements LogIndex {
|
|||||||
baseParams.add(request.instanceId());
|
baseParams.add(request.instanceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.instanceIds() != null && !request.instanceIds().isEmpty()) {
|
||||||
|
String placeholders = String.join(", ", Collections.nCopies(request.instanceIds().size(), "?"));
|
||||||
|
baseConditions.add("instance_id IN (" + placeholders + ")");
|
||||||
|
for (String id : request.instanceIds()) {
|
||||||
|
baseParams.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.exchangeId() != null && !request.exchangeId().isEmpty()) {
|
if (request.exchangeId() != null && !request.exchangeId().isEmpty()) {
|
||||||
baseConditions.add("(exchange_id = ?" +
|
baseConditions.add("(exchange_id = ?" +
|
||||||
" OR (mapContains(mdc, 'cameleer.exchangeId') AND mdc['cameleer.exchangeId'] = ?)" +
|
" OR (mapContains(mdc, 'cameleer.exchangeId') AND mdc['cameleer.exchangeId'] = ?)" +
|
||||||
@@ -281,6 +289,14 @@ public class ClickHouseLogStore implements LogIndex {
|
|||||||
params.add(request.instanceId());
|
params.add(request.instanceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.instanceIds() != null && !request.instanceIds().isEmpty()) {
|
||||||
|
String placeholders = String.join(", ", Collections.nCopies(request.instanceIds().size(), "?"));
|
||||||
|
conditions.add("instance_id IN (" + placeholders + ")");
|
||||||
|
for (String id : request.instanceIds()) {
|
||||||
|
params.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.exchangeId() != null && !request.exchangeId().isEmpty()) {
|
if (request.exchangeId() != null && !request.exchangeId().isEmpty()) {
|
||||||
conditions.add("(exchange_id = ?" +
|
conditions.add("(exchange_id = ?" +
|
||||||
" OR (mapContains(mdc, 'cameleer.exchangeId') AND mdc['cameleer.exchangeId'] = ?)" +
|
" OR (mapContains(mdc, 'cameleer.exchangeId') AND mdc['cameleer.exchangeId'] = ?)" +
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ class ClickHouseLogStoreCountIT {
|
|||||||
base.plusSeconds(30),
|
base.plusSeconds(30),
|
||||||
null,
|
null,
|
||||||
100,
|
100,
|
||||||
"desc"));
|
"desc",
|
||||||
|
null));
|
||||||
|
|
||||||
assertThat(count).isEqualTo(3);
|
assertThat(count).isEqualTo(3);
|
||||||
}
|
}
|
||||||
@@ -102,7 +103,8 @@ class ClickHouseLogStoreCountIT {
|
|||||||
base.plusSeconds(30),
|
base.plusSeconds(30),
|
||||||
null,
|
null,
|
||||||
100,
|
100,
|
||||||
"desc"));
|
"desc",
|
||||||
|
null));
|
||||||
|
|
||||||
assertThat(count).isZero();
|
assertThat(count).isZero();
|
||||||
}
|
}
|
||||||
@@ -120,7 +122,7 @@ class ClickHouseLogStoreCountIT {
|
|||||||
null, List.of("ERROR"), "orders", null, null, null,
|
null, List.of("ERROR"), "orders", null, null, null,
|
||||||
"dev", List.of(),
|
"dev", List.of(),
|
||||||
base.minusSeconds(1), base.plusSeconds(60),
|
base.minusSeconds(1), base.plusSeconds(60),
|
||||||
null, 100, "desc"));
|
null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(devCount).isEqualTo(2);
|
assertThat(devCount).isEqualTo(2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class ClickHouseLogStoreIT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private LogSearchRequest req(String application) {
|
private LogSearchRequest req(String application) {
|
||||||
return new LogSearchRequest(null, null, application, null, null, null, null, null, null, null, null, 100, "desc");
|
return new LogSearchRequest(null, null, application, null, null, null, null, null, null, null, null, 100, "desc", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tests ─────────────────────────────────────────────────────────────
|
// ── Tests ─────────────────────────────────────────────────────────────
|
||||||
@@ -99,7 +99,7 @@ class ClickHouseLogStoreIT {
|
|||||||
));
|
));
|
||||||
|
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, List.of("ERROR"), "my-app", null, null, null, null, null, null, null, null, 100, "desc"));
|
null, List.of("ERROR"), "my-app", null, null, null, null, null, null, null, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(1);
|
assertThat(result.data()).hasSize(1);
|
||||||
assertThat(result.data().get(0).level()).isEqualTo("ERROR");
|
assertThat(result.data().get(0).level()).isEqualTo("ERROR");
|
||||||
@@ -116,7 +116,7 @@ class ClickHouseLogStoreIT {
|
|||||||
));
|
));
|
||||||
|
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, List.of("WARN", "ERROR"), "my-app", null, null, null, null, null, null, null, null, 100, "desc"));
|
null, List.of("WARN", "ERROR"), "my-app", null, null, null, null, null, null, null, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(2);
|
assertThat(result.data()).hasSize(2);
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ class ClickHouseLogStoreIT {
|
|||||||
));
|
));
|
||||||
|
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
"order #12345", null, "my-app", null, null, null, null, null, null, null, null, 100, "desc"));
|
"order #12345", null, "my-app", null, null, null, null, null, null, null, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(1);
|
assertThat(result.data()).hasSize(1);
|
||||||
assertThat(result.data().get(0).message()).contains("order #12345");
|
assertThat(result.data().get(0).message()).contains("order #12345");
|
||||||
@@ -147,7 +147,7 @@ class ClickHouseLogStoreIT {
|
|||||||
));
|
));
|
||||||
|
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, "exchange-abc", null, null, null, null, null, null, 100, "desc"));
|
null, null, "my-app", null, "exchange-abc", null, null, null, null, null, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(1);
|
assertThat(result.data()).hasSize(1);
|
||||||
assertThat(result.data().get(0).message()).isEqualTo("msg with exchange");
|
assertThat(result.data().get(0).message()).isEqualTo("msg with exchange");
|
||||||
@@ -170,7 +170,7 @@ class ClickHouseLogStoreIT {
|
|||||||
Instant to = Instant.parse("2026-03-31T13:00:00Z");
|
Instant to = Instant.parse("2026-03-31T13:00:00Z");
|
||||||
|
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, null, null, null, null, from, to, null, 100, "desc"));
|
null, null, "my-app", null, null, null, null, null, from, to, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(1);
|
assertThat(result.data()).hasSize(1);
|
||||||
assertThat(result.data().get(0).message()).isEqualTo("noon");
|
assertThat(result.data().get(0).message()).isEqualTo("noon");
|
||||||
@@ -188,7 +188,7 @@ class ClickHouseLogStoreIT {
|
|||||||
|
|
||||||
// No application filter — should return both
|
// No application filter — should return both
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, null, null, null, null, null, null, null, null, null, null, 100, "desc"));
|
null, null, null, null, null, null, null, null, null, null, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(2);
|
assertThat(result.data()).hasSize(2);
|
||||||
}
|
}
|
||||||
@@ -202,7 +202,7 @@ class ClickHouseLogStoreIT {
|
|||||||
));
|
));
|
||||||
|
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, null, "OrderProcessor", null, null, null, null, null, 100, "desc"));
|
null, null, "my-app", null, null, "OrderProcessor", null, null, null, null, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(1);
|
assertThat(result.data()).hasSize(1);
|
||||||
assertThat(result.data().get(0).loggerName()).contains("OrderProcessor");
|
assertThat(result.data().get(0).loggerName()).contains("OrderProcessor");
|
||||||
@@ -221,7 +221,7 @@ class ClickHouseLogStoreIT {
|
|||||||
|
|
||||||
// Page 1: limit 2
|
// Page 1: limit 2
|
||||||
LogSearchResponse page1 = store.search(new LogSearchRequest(
|
LogSearchResponse page1 = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, null, null, null, null, null, null, null, 2, "desc"));
|
null, null, "my-app", null, null, null, null, null, null, null, null, 2, "desc", null));
|
||||||
|
|
||||||
assertThat(page1.data()).hasSize(2);
|
assertThat(page1.data()).hasSize(2);
|
||||||
assertThat(page1.hasMore()).isTrue();
|
assertThat(page1.hasMore()).isTrue();
|
||||||
@@ -230,7 +230,7 @@ class ClickHouseLogStoreIT {
|
|||||||
|
|
||||||
// Page 2: use cursor
|
// Page 2: use cursor
|
||||||
LogSearchResponse page2 = store.search(new LogSearchRequest(
|
LogSearchResponse page2 = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, null, null, null, null, null, null, page1.nextCursor(), 2, "desc"));
|
null, null, "my-app", null, null, null, null, null, null, null, page1.nextCursor(), 2, "desc", null));
|
||||||
|
|
||||||
assertThat(page2.data()).hasSize(2);
|
assertThat(page2.data()).hasSize(2);
|
||||||
assertThat(page2.hasMore()).isTrue();
|
assertThat(page2.hasMore()).isTrue();
|
||||||
@@ -238,7 +238,7 @@ class ClickHouseLogStoreIT {
|
|||||||
|
|
||||||
// Page 3: last page
|
// Page 3: last page
|
||||||
LogSearchResponse page3 = store.search(new LogSearchRequest(
|
LogSearchResponse page3 = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, null, null, null, null, null, null, page2.nextCursor(), 2, "desc"));
|
null, null, "my-app", null, null, null, null, null, null, null, page2.nextCursor(), 2, "desc", null));
|
||||||
|
|
||||||
assertThat(page3.data()).hasSize(1);
|
assertThat(page3.data()).hasSize(1);
|
||||||
assertThat(page3.hasMore()).isFalse();
|
assertThat(page3.hasMore()).isFalse();
|
||||||
@@ -257,7 +257,7 @@ class ClickHouseLogStoreIT {
|
|||||||
|
|
||||||
// Filter for ERROR only, but counts should include all levels
|
// Filter for ERROR only, but counts should include all levels
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, List.of("ERROR"), "my-app", null, null, null, null, null, null, null, null, 100, "desc"));
|
null, List.of("ERROR"), "my-app", null, null, null, null, null, null, null, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(1);
|
assertThat(result.data()).hasSize(1);
|
||||||
assertThat(result.levelCounts()).containsEntry("INFO", 2L);
|
assertThat(result.levelCounts()).containsEntry("INFO", 2L);
|
||||||
@@ -275,7 +275,7 @@ class ClickHouseLogStoreIT {
|
|||||||
));
|
));
|
||||||
|
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, null, null, null, null, null, null, null, 100, "asc"));
|
null, null, "my-app", null, null, null, null, null, null, null, null, 100, "asc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(3);
|
assertThat(result.data()).hasSize(3);
|
||||||
assertThat(result.data().get(0).message()).isEqualTo("msg-1");
|
assertThat(result.data().get(0).message()).isEqualTo("msg-1");
|
||||||
@@ -340,7 +340,7 @@ class ClickHouseLogStoreIT {
|
|||||||
|
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, null, null, null,
|
null, null, "my-app", null, null, null, null,
|
||||||
List.of("container"), null, null, null, 100, "desc"));
|
List.of("container"), null, null, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(1);
|
assertThat(result.data()).hasSize(1);
|
||||||
assertThat(result.data().get(0).message()).isEqualTo("container msg");
|
assertThat(result.data().get(0).message()).isEqualTo("container msg");
|
||||||
@@ -365,7 +365,7 @@ class ClickHouseLogStoreIT {
|
|||||||
|
|
||||||
LogSearchResponse result = store.search(new LogSearchRequest(
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, null, null, null,
|
null, null, "my-app", null, null, null, null,
|
||||||
List.of("app", "container"), null, null, null, 100, "desc"));
|
List.of("app", "container"), null, null, null, 100, "desc", null));
|
||||||
|
|
||||||
assertThat(result.data()).hasSize(2);
|
assertThat(result.data()).hasSize(2);
|
||||||
assertThat(result.data()).extracting(LogEntryResult::message)
|
assertThat(result.data()).extracting(LogEntryResult::message)
|
||||||
@@ -388,7 +388,7 @@ class ClickHouseLogStoreIT {
|
|||||||
for (int page = 0; page < 10; page++) {
|
for (int page = 0; page < 10; page++) {
|
||||||
LogSearchResponse resp = store.search(new LogSearchRequest(
|
LogSearchResponse resp = store.search(new LogSearchRequest(
|
||||||
null, null, "my-app", null, null, null, null, null,
|
null, null, "my-app", null, null, null, null, null,
|
||||||
null, null, cursor, 2, "desc"));
|
null, null, cursor, 2, "desc", null));
|
||||||
for (LogEntryResult r : resp.data()) {
|
for (LogEntryResult r : resp.data()) {
|
||||||
assertThat(seen.add(r.message())).as("duplicate row returned: " + r.message()).isTrue();
|
assertThat(seen.add(r.message())).as("duplicate row returned: " + r.message()).isTrue();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.cameleer.server.app.search;
|
||||||
|
|
||||||
|
import com.cameleer.server.core.ingestion.BufferedLogEntry;
|
||||||
|
import com.cameleer.server.core.search.LogSearchRequest;
|
||||||
|
import com.cameleer.server.core.search.LogSearchResponse;
|
||||||
|
import com.cameleer.common.model.LogEntry;
|
||||||
|
import com.cameleer.server.app.ClickHouseTestHelper;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.testcontainers.clickhouse.ClickHouseContainer;
|
||||||
|
import org.testcontainers.junit.jupiter.Container;
|
||||||
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration test for the {@code instanceIds} multi-value filter on
|
||||||
|
* {@link ClickHouseLogStore#search(LogSearchRequest)}.
|
||||||
|
*
|
||||||
|
* <p>Three rows are seeded with distinct {@code instance_id} values:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code prod-app1-0-aaa11111} — included in filter</li>
|
||||||
|
* <li>{@code prod-app1-1-aaa11111} — included in filter</li>
|
||||||
|
* <li>{@code prod-app1-0-bbb22222} — excluded from filter</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Testcontainers
|
||||||
|
class ClickHouseLogStoreInstanceIdsIT {
|
||||||
|
|
||||||
|
@Container
|
||||||
|
static final ClickHouseContainer clickhouse =
|
||||||
|
new ClickHouseContainer("clickhouse/clickhouse-server:24.12");
|
||||||
|
|
||||||
|
private JdbcTemplate jdbc;
|
||||||
|
private ClickHouseLogStore store;
|
||||||
|
|
||||||
|
private static final String TENANT = "default";
|
||||||
|
private static final String ENV = "prod";
|
||||||
|
private static final String APP = "app1";
|
||||||
|
private static final String INST_A = "prod-app1-0-aaa11111";
|
||||||
|
private static final String INST_B = "prod-app1-1-aaa11111";
|
||||||
|
private static final String INST_C = "prod-app1-0-bbb22222";
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
HikariDataSource ds = new HikariDataSource();
|
||||||
|
ds.setJdbcUrl(clickhouse.getJdbcUrl());
|
||||||
|
ds.setUsername(clickhouse.getUsername());
|
||||||
|
ds.setPassword(clickhouse.getPassword());
|
||||||
|
|
||||||
|
jdbc = new JdbcTemplate(ds);
|
||||||
|
ClickHouseTestHelper.executeInitSql(jdbc);
|
||||||
|
jdbc.execute("TRUNCATE TABLE logs");
|
||||||
|
|
||||||
|
store = new ClickHouseLogStore(TENANT, jdbc);
|
||||||
|
|
||||||
|
Instant base = Instant.parse("2026-04-23T09:00:00Z");
|
||||||
|
seedLog(INST_A, base, "msg-from-replica-0-gen-aaa");
|
||||||
|
seedLog(INST_B, base.plusSeconds(1), "msg-from-replica-1-gen-aaa");
|
||||||
|
seedLog(INST_C, base.plusSeconds(2), "msg-from-replica-0-gen-bbb");
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
jdbc.execute("TRUNCATE TABLE logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedLog(String instanceId, Instant ts, String message) {
|
||||||
|
LogEntry entry = new LogEntry(ts, "INFO", "com.example.Svc", message, "main", null, null);
|
||||||
|
store.insertBufferedBatch(List.of(
|
||||||
|
new BufferedLogEntry(TENANT, ENV, instanceId, APP, entry)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_instanceIds_returnsOnlyMatchingInstances() {
|
||||||
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
|
null,
|
||||||
|
List.of(),
|
||||||
|
APP,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ENV,
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
100,
|
||||||
|
"desc",
|
||||||
|
List.of(INST_A, INST_B)));
|
||||||
|
|
||||||
|
assertThat(result.data()).hasSize(2);
|
||||||
|
assertThat(result.data())
|
||||||
|
.extracting(r -> r.instanceId())
|
||||||
|
.containsExactlyInAnyOrder(INST_A, INST_B);
|
||||||
|
assertThat(result.data())
|
||||||
|
.extracting(r -> r.instanceId())
|
||||||
|
.doesNotContain(INST_C);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_emptyInstanceIds_returnsAllRows() {
|
||||||
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
|
null,
|
||||||
|
List.of(),
|
||||||
|
APP,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ENV,
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
100,
|
||||||
|
"desc",
|
||||||
|
List.of()));
|
||||||
|
|
||||||
|
assertThat(result.data()).hasSize(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_nullInstanceIds_returnsAllRows() {
|
||||||
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
|
null,
|
||||||
|
List.of(),
|
||||||
|
APP,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ENV,
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
100,
|
||||||
|
"desc",
|
||||||
|
null));
|
||||||
|
|
||||||
|
assertThat(result.data()).hasSize(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_instanceIds_singleValue_filtersToOneReplica() {
|
||||||
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
|
null,
|
||||||
|
List.of(),
|
||||||
|
APP,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ENV,
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
100,
|
||||||
|
"desc",
|
||||||
|
List.of(INST_C)));
|
||||||
|
|
||||||
|
assertThat(result.data()).hasSize(1);
|
||||||
|
assertThat(result.data().get(0).instanceId()).isEqualTo(INST_C);
|
||||||
|
assertThat(result.data().get(0).message()).isEqualTo("msg-from-replica-0-gen-bbb");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_instanceIds_doesNotConflictWithSingularInstanceId() {
|
||||||
|
// Singular instanceId=INST_A AND instanceIds=[INST_B] → intersection = empty
|
||||||
|
// (both conditions apply: instance_id = A AND instance_id IN (B))
|
||||||
|
LogSearchResponse result = store.search(new LogSearchRequest(
|
||||||
|
null,
|
||||||
|
List.of(),
|
||||||
|
APP,
|
||||||
|
INST_A, // singular
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ENV,
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
100,
|
||||||
|
"desc",
|
||||||
|
List.of(INST_B))); // plural — no overlap
|
||||||
|
|
||||||
|
assertThat(result.data()).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import java.util.List;
|
|||||||
* @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"]), OR-joined
|
* @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 (single value; coexists with instanceIds)
|
||||||
* @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")
|
||||||
@@ -19,6 +19,9 @@ import java.util.List;
|
|||||||
* @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")
|
||||||
|
* @param instanceIds multi-value instance ID filter (IN clause); scopes logs to one deployment's
|
||||||
|
* replicas when provided. Both instanceId and instanceIds may coexist — both
|
||||||
|
* conditions apply (AND). Empty/null means no additional filtering.
|
||||||
*/
|
*/
|
||||||
public record LogSearchRequest(
|
public record LogSearchRequest(
|
||||||
String q,
|
String q,
|
||||||
@@ -33,7 +36,8 @@ public record LogSearchRequest(
|
|||||||
Instant to,
|
Instant to,
|
||||||
String cursor,
|
String cursor,
|
||||||
int limit,
|
int limit,
|
||||||
String sort
|
String sort,
|
||||||
|
List<String> instanceIds
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private static final int DEFAULT_LIMIT = 100;
|
private static final int DEFAULT_LIMIT = 100;
|
||||||
@@ -45,5 +49,6 @@ public record LogSearchRequest(
|
|||||||
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();
|
if (sources == null) sources = List.of();
|
||||||
|
if (instanceIds == null) instanceIds = List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user