From 0cea8af6bc968d2d6eed1a8c12ac76653b065e2c Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:51:31 +0100 Subject: [PATCH] feat: add response/request DTOs for admin infrastructure endpoints Co-Authored-By: Claude Opus 4.6 (1M context) --- .../server/app/dto/ActiveQueryResponse.java | 11 ++ .../server/app/dto/AuditLogPageResponse.java | 15 ++ .../app/dto/ConnectionPoolResponse.java | 12 ++ .../app/dto/DatabaseStatusResponse.java | 12 ++ .../server/app/dto/IndexInfoResponse.java | 14 ++ .../server/app/dto/IndicesPageResponse.java | 16 ++ .../app/dto/OpenSearchStatusResponse.java | 12 ++ .../server/app/dto/PerformanceResponse.java | 13 ++ .../server/app/dto/PipelineStatsResponse.java | 16 ++ .../server/app/dto/TableSizeResponse.java | 13 ++ .../app/dto/ThresholdConfigRequest.java | 144 ++++++++++++++++++ 11 files changed, 278 insertions(+) create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ActiveQueryResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AuditLogPageResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ConnectionPoolResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/DatabaseStatusResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/IndexInfoResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/IndicesPageResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/OpenSearchStatusResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/PerformanceResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/PipelineStatsResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/TableSizeResponse.java create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ThresholdConfigRequest.java diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ActiveQueryResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ActiveQueryResponse.java new file mode 100644 index 00000000..39046869 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ActiveQueryResponse.java @@ -0,0 +1,11 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Currently running database query") +public record ActiveQueryResponse( + @Schema(description = "Backend process ID") int pid, + @Schema(description = "Query duration in seconds") double durationSeconds, + @Schema(description = "Backend state (active, idle, etc.)") String state, + @Schema(description = "SQL query text") String query +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AuditLogPageResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AuditLogPageResponse.java new file mode 100644 index 00000000..b1b1ea58 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AuditLogPageResponse.java @@ -0,0 +1,15 @@ +package com.cameleer3.server.app.dto; + +import com.cameleer3.server.core.admin.AuditRecord; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "Paginated audit log entries") +public record AuditLogPageResponse( + @Schema(description = "Audit log entries") List items, + @Schema(description = "Total number of matching entries") long totalCount, + @Schema(description = "Current page number (0-based)") int page, + @Schema(description = "Page size") int pageSize, + @Schema(description = "Total number of pages") int totalPages +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ConnectionPoolResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ConnectionPoolResponse.java new file mode 100644 index 00000000..cc3f0b60 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ConnectionPoolResponse.java @@ -0,0 +1,12 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "HikariCP connection pool statistics") +public record ConnectionPoolResponse( + @Schema(description = "Number of currently active connections") int activeConnections, + @Schema(description = "Number of idle connections") int idleConnections, + @Schema(description = "Number of threads waiting for a connection") int pendingThreads, + @Schema(description = "Maximum wait time in milliseconds") long maxWaitMs, + @Schema(description = "Maximum pool size") int maxPoolSize +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/DatabaseStatusResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/DatabaseStatusResponse.java new file mode 100644 index 00000000..c845c355 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/DatabaseStatusResponse.java @@ -0,0 +1,12 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Database connection and version status") +public record DatabaseStatusResponse( + @Schema(description = "Whether the database is reachable") boolean connected, + @Schema(description = "PostgreSQL version string") String version, + @Schema(description = "Database host") String host, + @Schema(description = "Current schema search path") String schema, + @Schema(description = "Whether TimescaleDB extension is available") boolean timescaleDb +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/IndexInfoResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/IndexInfoResponse.java new file mode 100644 index 00000000..6ab5dcd3 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/IndexInfoResponse.java @@ -0,0 +1,14 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "OpenSearch index information") +public record IndexInfoResponse( + @Schema(description = "Index name") String name, + @Schema(description = "Document count") long docCount, + @Schema(description = "Human-readable index size") String size, + @Schema(description = "Index size in bytes") long sizeBytes, + @Schema(description = "Index health status") String health, + @Schema(description = "Number of primary shards") int primaryShards, + @Schema(description = "Number of replica shards") int replicaShards +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/IndicesPageResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/IndicesPageResponse.java new file mode 100644 index 00000000..469ab84e --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/IndicesPageResponse.java @@ -0,0 +1,16 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "Paginated list of OpenSearch indices") +public record IndicesPageResponse( + @Schema(description = "Index list for current page") List indices, + @Schema(description = "Total number of indices") long totalIndices, + @Schema(description = "Total document count across all indices") long totalDocs, + @Schema(description = "Human-readable total size") String totalSize, + @Schema(description = "Current page number (0-based)") int page, + @Schema(description = "Page size") int pageSize, + @Schema(description = "Total number of pages") int totalPages +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/OpenSearchStatusResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/OpenSearchStatusResponse.java new file mode 100644 index 00000000..612982fe --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/OpenSearchStatusResponse.java @@ -0,0 +1,12 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "OpenSearch cluster status") +public record OpenSearchStatusResponse( + @Schema(description = "Whether the cluster is reachable") boolean reachable, + @Schema(description = "Cluster health status (GREEN, YELLOW, RED)") String clusterHealth, + @Schema(description = "OpenSearch version") String version, + @Schema(description = "Number of nodes in the cluster") int nodeCount, + @Schema(description = "OpenSearch host") String host +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/PerformanceResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/PerformanceResponse.java new file mode 100644 index 00000000..d34a3fad --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/PerformanceResponse.java @@ -0,0 +1,13 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "OpenSearch performance metrics") +public record PerformanceResponse( + @Schema(description = "Query cache hit rate (0.0-1.0)") double queryCacheHitRate, + @Schema(description = "Request cache hit rate (0.0-1.0)") double requestCacheHitRate, + @Schema(description = "Average search latency in milliseconds") double searchLatencyMs, + @Schema(description = "Average indexing latency in milliseconds") double indexingLatencyMs, + @Schema(description = "JVM heap used in bytes") long jvmHeapUsedBytes, + @Schema(description = "JVM heap max in bytes") long jvmHeapMaxBytes +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/PipelineStatsResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/PipelineStatsResponse.java new file mode 100644 index 00000000..f4285dc5 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/PipelineStatsResponse.java @@ -0,0 +1,16 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.Instant; + +@Schema(description = "Search indexing pipeline statistics") +public record PipelineStatsResponse( + @Schema(description = "Current queue depth") int queueDepth, + @Schema(description = "Maximum queue size") int maxQueueSize, + @Schema(description = "Number of failed indexing operations") long failedCount, + @Schema(description = "Number of successfully indexed documents") long indexedCount, + @Schema(description = "Debounce interval in milliseconds") long debounceMs, + @Schema(description = "Current indexing rate (docs/sec)") double indexingRate, + @Schema(description = "Timestamp of last indexed document") Instant lastIndexedAt +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/TableSizeResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/TableSizeResponse.java new file mode 100644 index 00000000..6849b528 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/TableSizeResponse.java @@ -0,0 +1,13 @@ +package com.cameleer3.server.app.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Table size and row count information") +public record TableSizeResponse( + @Schema(description = "Table name") String tableName, + @Schema(description = "Approximate row count") long rowCount, + @Schema(description = "Human-readable data size") String dataSize, + @Schema(description = "Human-readable index size") String indexSize, + @Schema(description = "Data size in bytes") long dataSizeBytes, + @Schema(description = "Index size in bytes") long indexSizeBytes +) {} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ThresholdConfigRequest.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ThresholdConfigRequest.java new file mode 100644 index 00000000..736210cb --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/ThresholdConfigRequest.java @@ -0,0 +1,144 @@ +package com.cameleer3.server.app.dto; + +import com.cameleer3.server.core.admin.ThresholdConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Schema(description = "Threshold configuration for admin monitoring") +public record ThresholdConfigRequest( + @Valid @NotNull DatabaseThresholdsRequest database, + @Valid @NotNull OpenSearchThresholdsRequest opensearch +) { + + @Schema(description = "Database monitoring thresholds") + public record DatabaseThresholdsRequest( + @Min(0) @Max(100) + @Schema(description = "Connection pool usage warning threshold (percentage)") + int connectionPoolWarning, + + @Min(0) @Max(100) + @Schema(description = "Connection pool usage critical threshold (percentage)") + int connectionPoolCritical, + + @Positive + @Schema(description = "Query duration warning threshold (seconds)") + double queryDurationWarning, + + @Positive + @Schema(description = "Query duration critical threshold (seconds)") + double queryDurationCritical + ) {} + + @Schema(description = "OpenSearch monitoring thresholds") + public record OpenSearchThresholdsRequest( + @NotBlank + @Schema(description = "Cluster health warning threshold (GREEN, YELLOW, RED)") + String clusterHealthWarning, + + @NotBlank + @Schema(description = "Cluster health critical threshold (GREEN, YELLOW, RED)") + String clusterHealthCritical, + + @Min(0) + @Schema(description = "Queue depth warning threshold") + int queueDepthWarning, + + @Min(0) + @Schema(description = "Queue depth critical threshold") + int queueDepthCritical, + + @Min(0) @Max(100) + @Schema(description = "JVM heap usage warning threshold (percentage)") + int jvmHeapWarning, + + @Min(0) @Max(100) + @Schema(description = "JVM heap usage critical threshold (percentage)") + int jvmHeapCritical, + + @Min(0) + @Schema(description = "Failed document count warning threshold") + int failedDocsWarning, + + @Min(0) + @Schema(description = "Failed document count critical threshold") + int failedDocsCritical + ) {} + + /** Convert to core domain model */ + public ThresholdConfig toConfig() { + return new ThresholdConfig( + new ThresholdConfig.DatabaseThresholds( + database.connectionPoolWarning(), + database.connectionPoolCritical(), + database.queryDurationWarning(), + database.queryDurationCritical() + ), + new ThresholdConfig.OpenSearchThresholds( + opensearch.clusterHealthWarning(), + opensearch.clusterHealthCritical(), + opensearch.queueDepthWarning(), + opensearch.queueDepthCritical(), + opensearch.jvmHeapWarning(), + opensearch.jvmHeapCritical(), + opensearch.failedDocsWarning(), + opensearch.failedDocsCritical() + ) + ); + } + + /** Validate semantic constraints beyond annotation-level validation */ + public List validate() { + List errors = new ArrayList<>(); + + if (database != null) { + if (database.connectionPoolWarning() > database.connectionPoolCritical()) { + errors.add("database.connectionPoolWarning must be <= connectionPoolCritical"); + } + if (database.queryDurationWarning() > database.queryDurationCritical()) { + errors.add("database.queryDurationWarning must be <= queryDurationCritical"); + } + } + + if (opensearch != null) { + if (opensearch.queueDepthWarning() > opensearch.queueDepthCritical()) { + errors.add("opensearch.queueDepthWarning must be <= queueDepthCritical"); + } + if (opensearch.jvmHeapWarning() > opensearch.jvmHeapCritical()) { + errors.add("opensearch.jvmHeapWarning must be <= jvmHeapCritical"); + } + if (opensearch.failedDocsWarning() > opensearch.failedDocsCritical()) { + errors.add("opensearch.failedDocsWarning must be <= failedDocsCritical"); + } + // Validate health severity ordering: GREEN < YELLOW < RED + int warningSeverity = healthSeverity(opensearch.clusterHealthWarning()); + int criticalSeverity = healthSeverity(opensearch.clusterHealthCritical()); + if (warningSeverity < 0) { + errors.add("opensearch.clusterHealthWarning must be GREEN, YELLOW, or RED"); + } + if (criticalSeverity < 0) { + errors.add("opensearch.clusterHealthCritical must be GREEN, YELLOW, or RED"); + } + if (warningSeverity >= 0 && criticalSeverity >= 0 && warningSeverity > criticalSeverity) { + errors.add("opensearch.clusterHealthWarning severity must be <= clusterHealthCritical (GREEN < YELLOW < RED)"); + } + } + + return errors; + } + + private static final Map HEALTH_SEVERITY = + Map.of("GREEN", 0, "YELLOW", 1, "RED", 2); + + private static int healthSeverity(String health) { + return HEALTH_SEVERITY.getOrDefault(health != null ? health.toUpperCase() : "", -1); + } +}