feat: remove OpenSearch, add ClickHouse admin page
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:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -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-*")));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user