feat: remove OpenSearch, add ClickHouse admin page
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 33s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

Remove all OpenSearch code, dependencies, configuration, deployment
manifests, and CI/CD references. Replace the OpenSearch admin page
with a ClickHouse admin page showing cluster status, table sizes,
performance metrics, and indexer pipeline stats.

- Delete 11 OpenSearch Java files (config, search impl, admin controller, DTOs, tests)
- Delete 3 OpenSearch frontend files (admin page, CSS, query hooks)
- Delete deploy/opensearch.yaml K8s manifest
- Remove opensearch Maven dependencies from pom.xml
- Remove opensearch config from application.yml, Dockerfile, docker-compose
- Remove opensearch from CI workflow (secrets, deploy, cleanup steps)
- Simplify ThresholdConfig (remove OpenSearch thresholds, database-only)
- Change default search backend from opensearch to clickhouse
- Add ClickHouseAdminController with /status, /tables, /performance, /pipeline
- Add ClickHouseAdminPage with StatCards, pipeline ProgressBar, tables DataTable
- Update CLAUDE.md, HOWTO.md, and source comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-01 18:56:06 +02:00
parent 5ed7d38bf7
commit 283e38a20d
49 changed files with 356 additions and 1753 deletions

View File

@@ -1,6 +1,5 @@
package com.cameleer3.server.app;
import org.opensearch.testcontainers.OpensearchContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -20,7 +19,6 @@ public abstract class AbstractPostgresIT {
.asCompatibleSubstituteFor("postgres");
static final PostgreSQLContainer<?> postgres;
static final OpensearchContainer<?> opensearch;
static final ClickHouseContainer clickhouse;
static {
@@ -30,9 +28,6 @@ public abstract class AbstractPostgresIT {
.withPassword("test");
postgres.start();
opensearch = new OpensearchContainer<>("opensearchproject/opensearch:2.19.0");
opensearch.start();
clickhouse = new ClickHouseContainer("clickhouse/clickhouse-server:24.12");
clickhouse.start();
}
@@ -50,7 +45,6 @@ public abstract class AbstractPostgresIT {
registry.add("spring.flyway.url", postgres::getJdbcUrl);
registry.add("spring.flyway.user", postgres::getUsername);
registry.add("spring.flyway.password", postgres::getPassword);
registry.add("opensearch.url", opensearch::getHttpHostAddress);
registry.add("clickhouse.enabled", () -> "true");
registry.add("clickhouse.url", clickhouse::getJdbcUrl);
registry.add("clickhouse.username", clickhouse::getUsername);

View File

@@ -1,112 +0,0 @@
package com.cameleer3.server.app.controller;
import com.cameleer3.server.app.AbstractPostgresIT;
import com.cameleer3.server.app.TestSecurityHelper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
class OpenSearchAdminControllerIT extends AbstractPostgresIT {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private TestSecurityHelper securityHelper;
private String adminJwt;
private String viewerJwt;
@BeforeEach
void setUp() {
adminJwt = securityHelper.adminToken();
viewerJwt = securityHelper.viewerToken();
}
@Test
void getStatus_asAdmin_returns200() throws Exception {
ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/admin/opensearch/status", HttpMethod.GET,
new HttpEntity<>(securityHelper.authHeadersNoBody(adminJwt)),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode body = objectMapper.readTree(response.getBody());
assertThat(body.get("reachable").asBoolean()).isTrue();
assertThat(body.has("clusterHealth")).isTrue();
assertThat(body.has("version")).isTrue();
}
@Test
void getStatus_asViewer_returns403() {
ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/admin/opensearch/status", HttpMethod.GET,
new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
void getPipeline_asAdmin_returns200() throws Exception {
ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/admin/opensearch/pipeline", HttpMethod.GET,
new HttpEntity<>(securityHelper.authHeadersNoBody(adminJwt)),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode body = objectMapper.readTree(response.getBody());
assertThat(body.has("queueDepth")).isTrue();
assertThat(body.has("maxQueueSize")).isTrue();
assertThat(body.has("indexedCount")).isTrue();
}
@Test
void getIndices_asAdmin_returns200() throws Exception {
ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/admin/opensearch/indices", HttpMethod.GET,
new HttpEntity<>(securityHelper.authHeadersNoBody(adminJwt)),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode body = objectMapper.readTree(response.getBody());
assertThat(body.has("indices")).isTrue();
assertThat(body.has("totalIndices")).isTrue();
assertThat(body.has("page")).isTrue();
}
@Test
void deleteIndex_nonExistent_returns404() {
ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/admin/opensearch/indices/nonexistent-index-xyz", HttpMethod.DELETE,
new HttpEntity<>(securityHelper.authHeadersNoBody(adminJwt)),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
void getPerformance_asAdmin_returns200() throws Exception {
ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/admin/opensearch/performance", HttpMethod.GET,
new HttpEntity<>(securityHelper.authHeadersNoBody(adminJwt)),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode body = objectMapper.readTree(response.getBody());
assertThat(body.has("queryCacheHitRate")).isTrue();
assertThat(body.has("jvmHeapUsedBytes")).isTrue();
}
}

View File

@@ -161,7 +161,7 @@ class SearchControllerIT extends AbstractPostgresIT {
Integer.class);
assertThat(count).isEqualTo(10);
// Wait for async OpenSearch indexing (debounce + index time)
// Wait for async search indexing (debounce + index time)
// Check for last seeded execution specifically to avoid false positives from other test classes
await().atMost(30, SECONDS).untilAsserted(() -> {
ResponseEntity<String> r = searchGet("?correlationId=corr-page-10");

View File

@@ -46,7 +46,6 @@ class ThresholdAdminControllerIT extends AbstractPostgresIT {
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode body = objectMapper.readTree(response.getBody());
assertThat(body.has("database")).isTrue();
assertThat(body.has("opensearch")).isTrue();
assertThat(body.path("database").path("connectionPoolWarning").asInt()).isEqualTo(80);
}
@@ -69,16 +68,6 @@ class ThresholdAdminControllerIT extends AbstractPostgresIT {
"connectionPoolCritical": 90,
"queryDurationWarning": 2.0,
"queryDurationCritical": 15.0
},
"opensearch": {
"clusterHealthWarning": "YELLOW",
"clusterHealthCritical": "RED",
"queueDepthWarning": 200,
"queueDepthCritical": 1000,
"jvmHeapWarning": 80,
"jvmHeapCritical": 95,
"failedDocsWarning": 5,
"failedDocsCritical": 20
}
}
""";
@@ -102,16 +91,6 @@ class ThresholdAdminControllerIT extends AbstractPostgresIT {
"connectionPoolCritical": 80,
"queryDurationWarning": 2.0,
"queryDurationCritical": 15.0
},
"opensearch": {
"clusterHealthWarning": "YELLOW",
"clusterHealthCritical": "RED",
"queueDepthWarning": 100,
"queueDepthCritical": 500,
"jvmHeapWarning": 75,
"jvmHeapCritical": 90,
"failedDocsWarning": 1,
"failedDocsCritical": 10
}
}
""";

View File

@@ -1,84 +0,0 @@
package com.cameleer3.server.app.search;
import com.cameleer3.server.app.AbstractPostgresIT;
import com.cameleer3.server.core.search.ExecutionSummary;
import com.cameleer3.server.core.search.SearchRequest;
import com.cameleer3.server.core.search.SearchResult;
import com.cameleer3.server.core.storage.SearchIndex;
import com.cameleer3.server.core.storage.model.ExecutionDocument;
import com.cameleer3.server.core.storage.model.ExecutionDocument.ProcessorDoc;
import org.junit.jupiter.api.Test;
import org.opensearch.client.opensearch.OpenSearchClient;
import org.opensearch.client.opensearch.indices.RefreshRequest;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.Instant;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
// Extends AbstractPostgresIT which provides both PostgreSQL and OpenSearch testcontainers
class OpenSearchIndexIT extends AbstractPostgresIT {
@Autowired
SearchIndex searchIndex;
@Autowired
OpenSearchClient openSearchClient;
@Test
void indexAndSearchByText() throws Exception {
Instant now = Instant.now();
ExecutionDocument doc = new ExecutionDocument(
"search-1", "route-a", "agent-1", "app-1",
"FAILED", "corr-1", "exch-1",
now, now.plusMillis(100), 100L,
"OrderNotFoundException: order-12345 not found", null,
List.of(new ProcessorDoc("proc-1", "log", "COMPLETED",
null, null, "request body with customer-99", null, null, null, null)),
null, false, false);
searchIndex.index(doc);
refreshOpenSearchIndices();
SearchRequest request = new SearchRequest(
null, now.minusSeconds(60), now.plusSeconds(60),
null, null, null,
"OrderNotFoundException", null, null, null,
null, null, null, null, null,
0, 50, "startTime", "desc");
SearchResult<ExecutionSummary> result = searchIndex.search(request);
assertTrue(result.total() > 0);
assertEquals("search-1", result.data().get(0).executionId());
}
@Test
void wildcardSearchFindsSubstring() throws Exception {
Instant now = Instant.now();
ExecutionDocument doc = new ExecutionDocument(
"wild-1", "route-b", "agent-1", "app-1",
"COMPLETED", null, null,
now, now.plusMillis(50), 50L, null, null,
List.of(new ProcessorDoc("proc-1", "bean", "COMPLETED",
null, null, "UniquePayloadIdentifier12345", null, null, null, null)),
null, false, false);
searchIndex.index(doc);
refreshOpenSearchIndices();
SearchRequest request = new SearchRequest(
null, now.minusSeconds(60), now.plusSeconds(60),
null, null, null,
"PayloadIdentifier", null, null, null,
null, null, null, null, null,
0, 50, "startTime", "desc");
SearchResult<ExecutionSummary> result = searchIndex.search(request);
assertTrue(result.total() > 0);
}
private void refreshOpenSearchIndices() throws Exception {
openSearchClient.indices().refresh(RefreshRequest.of(r -> r.index("executions-*")));
}
}

View File

@@ -2,9 +2,9 @@ spring:
flyway:
enabled: true
opensearch:
url: http://localhost:9200
debounce-ms: 100
cameleer:
indexer:
debounce-ms: 100
ingestion:
buffer-capacity: 100