feat: remove all ClickHouse dependencies from SaaS layer
- Delete log/ package (ClickHouseConfig, ContainerLogService, LogController) - Delete observability/ package (AgentStatusService, AgentStatusController) - Remove clickhouse-jdbc dependency from pom.xml - Remove cameleer.clickhouse config section from application.yml - Delete associated test files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
pom.xml
8
pom.xml
@@ -92,14 +92,6 @@
|
|||||||
<version>3.4.1</version>
|
<version>3.4.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- ClickHouse JDBC -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.clickhouse</groupId>
|
|
||||||
<artifactId>clickhouse-jdbc</artifactId>
|
|
||||||
<version>0.7.1</version>
|
|
||||||
<classifier>all</classifier>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Test -->
|
<!-- Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.log;
|
|
||||||
|
|
||||||
import com.zaxxer.hikari.HikariDataSource;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.context.annotation.Primary;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableConfigurationProperties(ClickHouseProperties.class)
|
|
||||||
@ConditionalOnProperty(name = "cameleer.clickhouse.enabled", havingValue = "true", matchIfMissing = true)
|
|
||||||
public class ClickHouseConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Explicit primary PG DataSource. Required because adding a second DataSource
|
|
||||||
* (ClickHouse) prevents Spring Boot auto-configuration from creating the default one.
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@Primary
|
|
||||||
public DataSource dataSource(DataSourceProperties properties) {
|
|
||||||
return properties.initializeDataSourceBuilder().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@Primary
|
|
||||||
public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
|
|
||||||
return new JdbcTemplate(dataSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "clickHouseDataSource")
|
|
||||||
public DataSource clickHouseDataSource(ClickHouseProperties props) {
|
|
||||||
HikariDataSource ds = new HikariDataSource();
|
|
||||||
ds.setJdbcUrl(props.getUrl());
|
|
||||||
ds.setUsername(props.getUsername());
|
|
||||||
ds.setPassword(props.getPassword());
|
|
||||||
ds.setMaximumPoolSize(10);
|
|
||||||
ds.setMinimumIdle(2);
|
|
||||||
ds.setConnectionTimeout(5000);
|
|
||||||
ds.setPoolName("clickhouse-pool");
|
|
||||||
return ds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.log;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "cameleer.clickhouse")
|
|
||||||
public class ClickHouseProperties {
|
|
||||||
|
|
||||||
private boolean enabled = true;
|
|
||||||
private String url = "jdbc:clickhouse://clickhouse:8123/cameleer";
|
|
||||||
private String username = "default";
|
|
||||||
private String password = "";
|
|
||||||
|
|
||||||
public boolean isEnabled() { return enabled; }
|
|
||||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
|
||||||
|
|
||||||
public String getUrl() { return url; }
|
|
||||||
public void setUrl(String url) { this.url = url; }
|
|
||||||
|
|
||||||
public String getUsername() { return username; }
|
|
||||||
public void setUsername(String username) { this.username = username; }
|
|
||||||
|
|
||||||
public String getPassword() { return password; }
|
|
||||||
public void setPassword(String password) { this.password = password; }
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.log;
|
|
||||||
|
|
||||||
import net.siegeln.cameleer.saas.log.dto.LogEntry;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class ContainerLogService {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ContainerLogService.class);
|
|
||||||
private static final int FLUSH_THRESHOLD = 100;
|
|
||||||
|
|
||||||
private final DataSource clickHouseDataSource;
|
|
||||||
private final ConcurrentLinkedQueue<Object[]> buffer = new ConcurrentLinkedQueue<>();
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public ContainerLogService(
|
|
||||||
@Autowired(required = false) @Qualifier("clickHouseDataSource") DataSource clickHouseDataSource) {
|
|
||||||
this.clickHouseDataSource = clickHouseDataSource;
|
|
||||||
if (clickHouseDataSource == null) {
|
|
||||||
log.warn("ClickHouse data source not available — ContainerLogService running in no-op mode");
|
|
||||||
} else {
|
|
||||||
initSchema();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void initSchema() {
|
|
||||||
if (clickHouseDataSource == null) return;
|
|
||||||
try (var conn = clickHouseDataSource.getConnection();
|
|
||||||
var stmt = conn.createStatement()) {
|
|
||||||
stmt.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS container_logs (
|
|
||||||
tenant_id UUID,
|
|
||||||
environment_id UUID,
|
|
||||||
app_id UUID,
|
|
||||||
deployment_id UUID,
|
|
||||||
timestamp DateTime64(3),
|
|
||||||
stream String,
|
|
||||||
message String
|
|
||||||
) ENGINE = MergeTree()
|
|
||||||
ORDER BY (tenant_id, environment_id, app_id, timestamp)
|
|
||||||
""");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to initialize ClickHouse schema", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(UUID tenantId, UUID envId, UUID appId, UUID deploymentId,
|
|
||||||
String stream, String message, long timestampMillis) {
|
|
||||||
if (clickHouseDataSource == null) return;
|
|
||||||
buffer.add(new Object[]{tenantId, envId, appId, deploymentId, timestampMillis, stream, message});
|
|
||||||
if (buffer.size() >= FLUSH_THRESHOLD) {
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void flush() {
|
|
||||||
if (clickHouseDataSource == null || buffer.isEmpty()) return;
|
|
||||||
List<Object[]> batch = new ArrayList<>(FLUSH_THRESHOLD);
|
|
||||||
Object[] row;
|
|
||||||
while ((row = buffer.poll()) != null) {
|
|
||||||
batch.add(row);
|
|
||||||
}
|
|
||||||
if (batch.isEmpty()) return;
|
|
||||||
String sql = "INSERT INTO container_logs (tenant_id, environment_id, app_id, deployment_id, timestamp, stream, message) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
|
||||||
try (var conn = clickHouseDataSource.getConnection();
|
|
||||||
var ps = conn.prepareStatement(sql)) {
|
|
||||||
for (Object[] entry : batch) {
|
|
||||||
ps.setObject(1, entry[0]); // tenant_id
|
|
||||||
ps.setObject(2, entry[1]); // environment_id
|
|
||||||
ps.setObject(3, entry[2]); // app_id
|
|
||||||
ps.setObject(4, entry[3]); // deployment_id
|
|
||||||
ps.setTimestamp(5, new Timestamp((Long) entry[4]));
|
|
||||||
ps.setString(6, (String) entry[5]);
|
|
||||||
ps.setString(7, (String) entry[6]);
|
|
||||||
ps.addBatch();
|
|
||||||
}
|
|
||||||
ps.executeBatch();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to flush log batch to ClickHouse ({} entries)", batch.size(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<LogEntry> query(UUID appId, Instant since, Instant until, int limit, String stream) {
|
|
||||||
if (clickHouseDataSource == null) return List.of();
|
|
||||||
StringBuilder sql = new StringBuilder(
|
|
||||||
"SELECT app_id, deployment_id, timestamp, stream, message FROM container_logs WHERE app_id = ?");
|
|
||||||
List<Object> params = new ArrayList<>();
|
|
||||||
params.add(appId);
|
|
||||||
if (since != null) {
|
|
||||||
sql.append(" AND timestamp >= ?");
|
|
||||||
params.add(Timestamp.from(since));
|
|
||||||
}
|
|
||||||
if (until != null) {
|
|
||||||
sql.append(" AND timestamp <= ?");
|
|
||||||
params.add(Timestamp.from(until));
|
|
||||||
}
|
|
||||||
if (stream != null && !"both".equalsIgnoreCase(stream)) {
|
|
||||||
sql.append(" AND stream = ?");
|
|
||||||
params.add(stream);
|
|
||||||
}
|
|
||||||
sql.append(" ORDER BY timestamp LIMIT ?");
|
|
||||||
params.add(limit);
|
|
||||||
List<LogEntry> results = new ArrayList<>();
|
|
||||||
try (var conn = clickHouseDataSource.getConnection();
|
|
||||||
var ps = conn.prepareStatement(sql.toString())) {
|
|
||||||
for (int i = 0; i < params.size(); i++) {
|
|
||||||
ps.setObject(i + 1, params.get(i));
|
|
||||||
}
|
|
||||||
try (var rs = ps.executeQuery()) {
|
|
||||||
while (rs.next()) {
|
|
||||||
results.add(new LogEntry(
|
|
||||||
UUID.fromString(rs.getString("app_id")),
|
|
||||||
UUID.fromString(rs.getString("deployment_id")),
|
|
||||||
rs.getTimestamp("timestamp").toInstant(),
|
|
||||||
rs.getString("stream"),
|
|
||||||
rs.getString("message")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to query container logs for appId={}", appId, e);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.log;
|
|
||||||
|
|
||||||
import net.siegeln.cameleer.saas.log.dto.LogEntry;
|
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/apps/{appId}/logs")
|
|
||||||
public class LogController {
|
|
||||||
|
|
||||||
private final ContainerLogService containerLogService;
|
|
||||||
|
|
||||||
public LogController(ContainerLogService containerLogService) {
|
|
||||||
this.containerLogService = containerLogService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("hasAuthority('SCOPE_observe:read')")
|
|
||||||
public ResponseEntity<List<LogEntry>> query(
|
|
||||||
@PathVariable UUID appId,
|
|
||||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant since,
|
|
||||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant until,
|
|
||||||
@RequestParam(defaultValue = "500") int limit,
|
|
||||||
@RequestParam(defaultValue = "both") String stream) {
|
|
||||||
List<LogEntry> entries = containerLogService.query(appId, since, until, limit, stream);
|
|
||||||
return ResponseEntity.ok(entries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.log.dto;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public record LogEntry(
|
|
||||||
UUID appId, UUID deploymentId, Instant timestamp, String stream, String message
|
|
||||||
) {}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.observability;
|
|
||||||
|
|
||||||
import net.siegeln.cameleer.saas.observability.dto.AgentStatusResponse;
|
|
||||||
import net.siegeln.cameleer.saas.observability.dto.ObservabilityStatusResponse;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/apps/{appId}")
|
|
||||||
public class AgentStatusController {
|
|
||||||
|
|
||||||
private final AgentStatusService agentStatusService;
|
|
||||||
|
|
||||||
public AgentStatusController(AgentStatusService agentStatusService) {
|
|
||||||
this.agentStatusService = agentStatusService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/agent-status")
|
|
||||||
@PreAuthorize("hasAuthority('SCOPE_observe:read')")
|
|
||||||
public ResponseEntity<AgentStatusResponse> getAgentStatus(@PathVariable UUID appId) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
return ResponseEntity.ok(agentStatusService.getAgentStatus(appId));
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/observability-status")
|
|
||||||
@PreAuthorize("hasAuthority('SCOPE_observe:read')")
|
|
||||||
public ResponseEntity<ObservabilityStatusResponse> getObservabilityStatus(@PathVariable UUID appId) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
return ResponseEntity.ok(agentStatusService.getObservabilityStatus(appId));
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.observability;
|
|
||||||
|
|
||||||
import net.siegeln.cameleer.saas.app.AppRepository;
|
|
||||||
import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
|
|
||||||
import net.siegeln.cameleer.saas.identity.ServerApiClient;
|
|
||||||
import net.siegeln.cameleer.saas.observability.dto.AgentStatusResponse;
|
|
||||||
import net.siegeln.cameleer.saas.observability.dto.ObservabilityStatusResponse;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class AgentStatusService {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(AgentStatusService.class);
|
|
||||||
|
|
||||||
private final AppRepository appRepository;
|
|
||||||
private final EnvironmentRepository environmentRepository;
|
|
||||||
private final ServerApiClient serverApiClient;
|
|
||||||
|
|
||||||
@Autowired(required = false)
|
|
||||||
@Qualifier("clickHouseDataSource")
|
|
||||||
private DataSource clickHouseDataSource;
|
|
||||||
|
|
||||||
public AgentStatusService(AppRepository appRepository,
|
|
||||||
EnvironmentRepository environmentRepository,
|
|
||||||
ServerApiClient serverApiClient) {
|
|
||||||
this.appRepository = appRepository;
|
|
||||||
this.environmentRepository = environmentRepository;
|
|
||||||
this.serverApiClient = serverApiClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AgentStatusResponse getAgentStatus(UUID appId) {
|
|
||||||
var app = appRepository.findById(appId)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("App not found: " + appId));
|
|
||||||
|
|
||||||
var env = environmentRepository.findById(app.getEnvironmentId())
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Environment not found: " + app.getEnvironmentId()));
|
|
||||||
|
|
||||||
if (!serverApiClient.isAvailable()) {
|
|
||||||
return unknownStatus(app.getSlug(), env.getSlug());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Map<String, Object>> agents = serverApiClient.get("/api/v1/agents")
|
|
||||||
.retrieve()
|
|
||||||
.body(List.class);
|
|
||||||
|
|
||||||
if (agents == null) {
|
|
||||||
return unknownStatus(app.getSlug(), env.getSlug());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Map<String, Object> agent : agents) {
|
|
||||||
String agentAppId = (String) agent.get("applicationId");
|
|
||||||
String agentEnvId = (String) agent.get("environmentId");
|
|
||||||
if (app.getSlug().equals(agentAppId) && env.getSlug().equals(agentEnvId)) {
|
|
||||||
String state = (String) agent.getOrDefault("state", "UNKNOWN");
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<String> routeIds = (List<String>) agent.getOrDefault("routeIds", Collections.emptyList());
|
|
||||||
return new AgentStatusResponse(
|
|
||||||
true,
|
|
||||||
state,
|
|
||||||
null,
|
|
||||||
routeIds,
|
|
||||||
app.getSlug(),
|
|
||||||
env.getSlug()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return unknownStatus(app.getSlug(), env.getSlug());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to fetch agent status from cameleer3-server: {}", e.getMessage());
|
|
||||||
return unknownStatus(app.getSlug(), env.getSlug());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservabilityStatusResponse getObservabilityStatus(UUID appId) {
|
|
||||||
var app = appRepository.findById(appId)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("App not found: " + appId));
|
|
||||||
|
|
||||||
var env = environmentRepository.findById(app.getEnvironmentId())
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Environment not found: " + app.getEnvironmentId()));
|
|
||||||
|
|
||||||
if (clickHouseDataSource == null) {
|
|
||||||
return new ObservabilityStatusResponse(false, false, false, null, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
try (var conn = clickHouseDataSource.getConnection()) {
|
|
||||||
String sql = "SELECT count() as cnt, max(start_time) as last_trace " +
|
|
||||||
"FROM executions " +
|
|
||||||
"WHERE application_id = ? AND environment_id = ? " +
|
|
||||||
"AND start_time >= now() - INTERVAL 24 HOUR";
|
|
||||||
try (var stmt = conn.prepareStatement(sql)) {
|
|
||||||
stmt.setString(1, app.getSlug());
|
|
||||||
stmt.setString(2, env.getSlug());
|
|
||||||
try (var rs = stmt.executeQuery()) {
|
|
||||||
if (rs.next()) {
|
|
||||||
long count = rs.getLong("cnt");
|
|
||||||
Timestamp lastTrace = rs.getTimestamp("last_trace");
|
|
||||||
boolean hasTraces = count > 0;
|
|
||||||
return new ObservabilityStatusResponse(
|
|
||||||
hasTraces,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
hasTraces && lastTrace != null ? lastTrace.toInstant() : null,
|
|
||||||
count
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to query ClickHouse for observability status: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ObservabilityStatusResponse(false, false, false, null, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AgentStatusResponse unknownStatus(String applicationId, String environmentId) {
|
|
||||||
return new AgentStatusResponse(false, "UNKNOWN", null, Collections.emptyList(), applicationId, environmentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.observability;
|
|
||||||
|
|
||||||
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.client.RestClient;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class ConnectivityHealthCheck {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ConnectivityHealthCheck.class);
|
|
||||||
|
|
||||||
private final RuntimeConfig runtimeConfig;
|
|
||||||
|
|
||||||
public ConnectivityHealthCheck(RuntimeConfig runtimeConfig) {
|
|
||||||
this.runtimeConfig = runtimeConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void verifyConnectivity() {
|
|
||||||
checkCameleer3Server();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkCameleer3Server() {
|
|
||||||
try {
|
|
||||||
var client = RestClient.builder()
|
|
||||||
.baseUrl(runtimeConfig.getCameleer3ServerEndpoint())
|
|
||||||
.build();
|
|
||||||
var response = client.get()
|
|
||||||
.uri("/api/v1/health")
|
|
||||||
.retrieve()
|
|
||||||
.toBodilessEntity();
|
|
||||||
if (response.getStatusCode().is2xxSuccessful()) {
|
|
||||||
log.info("cameleer3-server connectivity: OK ({})",
|
|
||||||
runtimeConfig.getCameleer3ServerEndpoint());
|
|
||||||
} else {
|
|
||||||
log.warn("cameleer3-server connectivity: HTTP {} ({})",
|
|
||||||
response.getStatusCode(), runtimeConfig.getCameleer3ServerEndpoint());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("cameleer3-server connectivity: FAILED ({}) - {}",
|
|
||||||
runtimeConfig.getCameleer3ServerEndpoint(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.observability.dto;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public record AgentStatusResponse(
|
|
||||||
boolean registered,
|
|
||||||
String state,
|
|
||||||
Instant lastHeartbeat,
|
|
||||||
List<String> routeIds,
|
|
||||||
String applicationId,
|
|
||||||
String environmentId
|
|
||||||
) {}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.observability.dto;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
public record ObservabilityStatusResponse(
|
|
||||||
boolean hasTraces,
|
|
||||||
boolean hasMetrics,
|
|
||||||
boolean hasDiagrams,
|
|
||||||
Instant lastTraceAt,
|
|
||||||
long traceCount24h
|
|
||||||
) {}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.observability.dto;
|
|
||||||
|
|
||||||
public record UpdateRoutingRequest(
|
|
||||||
Integer exposedPort
|
|
||||||
) {}
|
|
||||||
@@ -56,8 +56,3 @@ cameleer:
|
|||||||
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
|
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
|
||||||
cameleer3-server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
|
cameleer3-server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
|
||||||
domain: ${DOMAIN:localhost}
|
domain: ${DOMAIN:localhost}
|
||||||
clickhouse:
|
|
||||||
enabled: ${CLICKHOUSE_ENABLED:true}
|
|
||||||
url: ${CLICKHOUSE_URL:jdbc:clickhouse://clickhouse:8123/cameleer}
|
|
||||||
username: ${CLICKHOUSE_USERNAME:default}
|
|
||||||
password: ${CLICKHOUSE_PASSWORD:}
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.log;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
class ContainerLogServiceTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void buffer_shouldAccumulateEntries() {
|
|
||||||
var buffer = new ConcurrentLinkedQueue<String>();
|
|
||||||
buffer.add("entry1");
|
|
||||||
buffer.add("entry2");
|
|
||||||
assertEquals(2, buffer.size());
|
|
||||||
assertEquals("entry1", buffer.poll());
|
|
||||||
assertEquals(1, buffer.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package net.siegeln.cameleer.saas.observability;
|
|
||||||
|
|
||||||
import net.siegeln.cameleer.saas.app.AppEntity;
|
|
||||||
import net.siegeln.cameleer.saas.app.AppRepository;
|
|
||||||
import net.siegeln.cameleer.saas.environment.EnvironmentEntity;
|
|
||||||
import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
|
|
||||||
import net.siegeln.cameleer.saas.identity.ServerApiClient;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.mockito.junit.jupiter.MockitoSettings;
|
|
||||||
import org.mockito.quality.Strictness;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
|
||||||
class AgentStatusServiceTest {
|
|
||||||
|
|
||||||
@Mock private AppRepository appRepository;
|
|
||||||
@Mock private EnvironmentRepository environmentRepository;
|
|
||||||
@Mock private ServerApiClient serverApiClient;
|
|
||||||
|
|
||||||
private AgentStatusService agentStatusService;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
when(serverApiClient.isAvailable()).thenReturn(false);
|
|
||||||
agentStatusService = new AgentStatusService(appRepository, environmentRepository, serverApiClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getAgentStatus_appNotFound_shouldThrow() {
|
|
||||||
when(appRepository.findById(any())).thenReturn(Optional.empty());
|
|
||||||
assertThrows(IllegalArgumentException.class,
|
|
||||||
() -> agentStatusService.getAgentStatus(UUID.randomUUID()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getAgentStatus_shouldReturnUnknownWhenServerUnreachable() {
|
|
||||||
var appId = UUID.randomUUID();
|
|
||||||
var envId = UUID.randomUUID();
|
|
||||||
|
|
||||||
var app = new AppEntity();
|
|
||||||
app.setId(appId);
|
|
||||||
app.setEnvironmentId(envId);
|
|
||||||
app.setSlug("my-app");
|
|
||||||
when(appRepository.findById(appId)).thenReturn(Optional.of(app));
|
|
||||||
|
|
||||||
var env = new EnvironmentEntity();
|
|
||||||
env.setId(envId);
|
|
||||||
env.setSlug("default");
|
|
||||||
when(environmentRepository.findById(envId)).thenReturn(Optional.of(env));
|
|
||||||
|
|
||||||
// Server at localhost:9999 won't be running — should return UNKNOWN gracefully
|
|
||||||
var result = agentStatusService.getAgentStatus(appId);
|
|
||||||
|
|
||||||
assertNotNull(result);
|
|
||||||
assertFalse(result.registered());
|
|
||||||
assertEquals("UNKNOWN", result.state());
|
|
||||||
assertEquals("my-app", result.applicationId());
|
|
||||||
assertEquals("default", result.environmentId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getObservabilityStatus_shouldReturnEmptyWhenClickHouseUnavailable() {
|
|
||||||
var appId = UUID.randomUUID();
|
|
||||||
var envId = UUID.randomUUID();
|
|
||||||
|
|
||||||
var app = new AppEntity();
|
|
||||||
app.setId(appId);
|
|
||||||
app.setEnvironmentId(envId);
|
|
||||||
app.setSlug("my-app");
|
|
||||||
when(appRepository.findById(appId)).thenReturn(Optional.of(app));
|
|
||||||
|
|
||||||
var env = new EnvironmentEntity();
|
|
||||||
env.setId(envId);
|
|
||||||
env.setSlug("default");
|
|
||||||
when(environmentRepository.findById(envId)).thenReturn(Optional.of(env));
|
|
||||||
|
|
||||||
// No ClickHouse DataSource injected — should return empty status
|
|
||||||
var result = agentStatusService.getObservabilityStatus(appId);
|
|
||||||
|
|
||||||
assertNotNull(result);
|
|
||||||
assertFalse(result.hasTraces());
|
|
||||||
assertEquals(0, result.traceCount24h());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user