From d02fa7308061210d6076323ffa185477071eb0f4 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:19:42 +0200 Subject: [PATCH] fix: scope correlation-chain query to the exchange's own env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correlated exchanges always share the env of the one being viewed — using the globally-selected env from the picker was wrong if the user switched envs after opening a detail view (or arrived via permalink). Thread `environment` through: - `ExecutionStore.ExecutionRecord` gains `environment` field; the ClickHouse `executions` table already stores this, just not read back. - `ClickHouseExecutionStore.findById` SELECT adds the column; mapper populates it. - `ExecutionDetail` gains `environment`; `DetailService` passes through. - `IngestionService.toExecutionRecord` passes null — this legacy PG ingestion path isn't active when ClickHouse is enabled, and the read-side is what drives the correlation UI. - UI `ExchangeHeader` reads `detail.environment ?? storeEnv` and extends the TS type locally (schema.d.ts catches up on next regen). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../server/app/storage/ClickHouseExecutionStore.java | 3 ++- .../java/com/cameleer/server/core/detail/DetailService.java | 2 +- .../com/cameleer/server/core/detail/ExecutionDetail.java | 2 ++ .../cameleer/server/core/ingestion/IngestionService.java | 1 + .../com/cameleer/server/core/storage/ExecutionStore.java | 1 + ui/src/components/ExecutionDiagram/types.ts | 5 ++++- ui/src/pages/Exchanges/ExchangeHeader.tsx | 6 +++++- 7 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseExecutionStore.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseExecutionStore.java index 78eb6240..2e21cacb 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseExecutionStore.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseExecutionStore.java @@ -204,7 +204,7 @@ public class ClickHouseExecutionStore implements ExecutionStore { @Override public Optional findById(String executionId) { List results = jdbc.query(""" - SELECT execution_id, route_id, instance_id, application_id, status, + SELECT execution_id, route_id, instance_id, application_id, environment, status, correlation_id, exchange_id, start_time, end_time, duration_ms, error_message, error_stacktrace, diagram_content_hash, engine_level, input_body, output_body, input_headers, output_headers, @@ -304,6 +304,7 @@ public class ClickHouseExecutionStore implements ExecutionStore { emptyToNull(rs.getString("route_id")), emptyToNull(rs.getString("instance_id")), emptyToNull(rs.getString("application_id")), + emptyToNull(rs.getString("environment")), emptyToNull(rs.getString("status")), emptyToNull(rs.getString("correlation_id")), emptyToNull(rs.getString("exchange_id")), diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/detail/DetailService.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/detail/DetailService.java index 79bb5b59..c3b27c4d 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/detail/DetailService.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/detail/DetailService.java @@ -36,7 +36,7 @@ public class DetailService { } return new ExecutionDetail( exec.executionId(), exec.routeId(), exec.instanceId(), - exec.applicationId(), + exec.applicationId(), exec.environment(), exec.status(), exec.startTime(), exec.endTime(), exec.durationMs() != null ? exec.durationMs() : 0L, exec.correlationId(), exec.exchangeId(), diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/detail/ExecutionDetail.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/detail/ExecutionDetail.java index e375e9e2..1956ed53 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/detail/ExecutionDetail.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/detail/ExecutionDetail.java @@ -13,6 +13,7 @@ import java.util.Map; * @param executionId unique execution identifier * @param routeId Camel route ID * @param instanceId agent instance that reported the execution + * @param environment environment slug this exchange was recorded in * @param status execution status (COMPLETED, FAILED, RUNNING) * @param startTime execution start time * @param endTime execution end time (may be null for RUNNING) @@ -33,6 +34,7 @@ public record ExecutionDetail( String routeId, String instanceId, String applicationId, + String environment, String status, Instant startTime, Instant endTime, diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/ingestion/IngestionService.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/ingestion/IngestionService.java index e586f92b..bc925a09 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/ingestion/IngestionService.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/ingestion/IngestionService.java @@ -114,6 +114,7 @@ public class IngestionService { return new ExecutionRecord( exec.getExchangeId(), exec.getRouteId(), instanceId, applicationId, + null, // environment: legacy PG path; ClickHouse path uses MergedExecution with env resolved from registry exec.getStatus() != null ? exec.getStatus().name() : "RUNNING", exec.getCorrelationId(), exec.getExchangeId(), exec.getStartTime(), exec.getEndTime(), diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/ExecutionStore.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/ExecutionStore.java index 7f4946ad..9c227128 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/ExecutionStore.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/ExecutionStore.java @@ -20,6 +20,7 @@ public interface ExecutionStore { record ExecutionRecord( String executionId, String routeId, String instanceId, String applicationId, + String environment, String status, String correlationId, String exchangeId, Instant startTime, Instant endTime, Long durationMs, String errorMessage, String errorStacktrace, String diagramContentHash, diff --git a/ui/src/components/ExecutionDiagram/types.ts b/ui/src/components/ExecutionDiagram/types.ts index 4848c355..ba067bfc 100644 --- a/ui/src/components/ExecutionDiagram/types.ts +++ b/ui/src/components/ExecutionDiagram/types.ts @@ -1,6 +1,9 @@ import type { components } from '../../api/schema'; -export type ExecutionDetail = components['schemas']['ExecutionDetail']; +export type ExecutionDetail = components['schemas']['ExecutionDetail'] & { + /** Environment slug this exchange was recorded in. Added to the backend record; schema.d.ts will pick it up on next openapi regen. */ + environment?: string; +}; export type ProcessorNode = components['schemas']['ProcessorNode']; export interface NodeExecutionState { diff --git a/ui/src/pages/Exchanges/ExchangeHeader.tsx b/ui/src/pages/Exchanges/ExchangeHeader.tsx index 930415b0..c3887a55 100644 --- a/ui/src/pages/Exchanges/ExchangeHeader.tsx +++ b/ui/src/pages/Exchanges/ExchangeHeader.tsx @@ -33,7 +33,11 @@ function statusVariant(s: string): StatusVariant { export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }: ExchangeHeaderProps) { const navigate = useNavigate(); const { timeRange } = useGlobalFilters(); - const environment = useEnvironmentStore((s) => s.environment); + const storeEnv = useEnvironmentStore((s) => s.environment); + // Prefer the exchange's own env over the selected env — correlated exchanges + // always live in the same env as the one being viewed, and the user may have + // switched env-picker after opening this detail. + const environment = detail.environment ?? storeEnv; const { data: chainResult } = useCorrelationChain(detail.correlationId ?? null, environment); const chain = chainResult?.data; const showChain = chain && chain.length > 1;