feat: store raw processor tree JSON and add error categorization fields
Fixes iteration overlay corruption caused by flat storage collapsing duplicate processorIds across loop iterations. Server: - Store raw processor tree as processors_json JSONB on executions table - Detail endpoint serves from processors_json (faithful tree), falls back to flat record reconstruction for older executions - V10 migration: processors_json, error categorization (errorType, errorCategory, rootCauseType, rootCauseMessage), OTel (traceId, spanId), circuit breaker (circuitBreakerState, fallbackTriggered), drops erroneous splitDepth/loopDepth columns - Add all new fields through full ingestion/storage/API chain UI: - Fix overlay wrapper filtering: check wrapper type before status filter - Add new fields to schema.d.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
package com.cameleer3.server.core.detail;
|
||||
|
||||
import com.cameleer3.common.model.ProcessorExecution;
|
||||
import com.cameleer3.server.core.storage.ExecutionStore;
|
||||
import com.cameleer3.server.core.storage.ExecutionStore.ProcessorRecord;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class DetailService {
|
||||
|
||||
private static final ObjectMapper JSON = new ObjectMapper();
|
||||
private static final ObjectMapper JSON = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
private static final TypeReference<Map<String, String>> STR_MAP = new TypeReference<>() {};
|
||||
private static final TypeReference<List<ProcessorExecution>> PROCESSOR_EXEC_LIST = new TypeReference<>() {};
|
||||
|
||||
private final ExecutionStore executionStore;
|
||||
|
||||
@@ -21,8 +25,14 @@ public class DetailService {
|
||||
public Optional<ExecutionDetail> getDetail(String executionId) {
|
||||
return executionStore.findById(executionId)
|
||||
.map(exec -> {
|
||||
List<ProcessorRecord> processors = executionStore.findProcessors(executionId);
|
||||
List<ProcessorNode> roots = buildTree(processors);
|
||||
// Prefer the raw processor tree (faithful to agent data) over
|
||||
// flat-record reconstruction (which loses iteration context).
|
||||
List<ProcessorNode> processors = parseProcessorsJson(exec.processorsJson());
|
||||
if (processors == null) {
|
||||
// Fallback for executions ingested before processors_json was added
|
||||
List<ProcessorRecord> records = executionStore.findProcessors(executionId);
|
||||
processors = buildTree(records);
|
||||
}
|
||||
return new ExecutionDetail(
|
||||
exec.executionId(), exec.routeId(), exec.agentId(),
|
||||
exec.applicationName(),
|
||||
@@ -30,10 +40,13 @@ public class DetailService {
|
||||
exec.durationMs() != null ? exec.durationMs() : 0L,
|
||||
exec.correlationId(), exec.exchangeId(),
|
||||
exec.errorMessage(), exec.errorStacktrace(),
|
||||
exec.diagramContentHash(), roots,
|
||||
exec.diagramContentHash(), processors,
|
||||
exec.inputBody(), exec.outputBody(),
|
||||
exec.inputHeaders(), exec.outputHeaders(),
|
||||
parseAttributes(exec.attributes())
|
||||
parseAttributes(exec.attributes()),
|
||||
exec.errorType(), exec.errorCategory(),
|
||||
exec.rootCauseType(), exec.rootCauseMessage(),
|
||||
exec.traceId(), exec.spanId()
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -50,6 +63,50 @@ public class DetailService {
|
||||
});
|
||||
}
|
||||
|
||||
/** Parse the raw processor tree JSON stored alongside the execution. */
|
||||
private List<ProcessorNode> parseProcessorsJson(String json) {
|
||||
if (json == null || json.isBlank()) return null;
|
||||
try {
|
||||
List<ProcessorExecution> executions = JSON.readValue(json, PROCESSOR_EXEC_LIST);
|
||||
return convertProcessors(executions);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert agent ProcessorExecution tree to detail ProcessorNode tree. */
|
||||
private List<ProcessorNode> convertProcessors(List<ProcessorExecution> executions) {
|
||||
if (executions == null) return List.of();
|
||||
List<ProcessorNode> result = new ArrayList<>();
|
||||
for (ProcessorExecution p : executions) {
|
||||
ProcessorNode node = new ProcessorNode(
|
||||
p.getProcessorId(), p.getProcessorType(),
|
||||
p.getStatus() != null ? p.getStatus().name() : null,
|
||||
p.getStartTime(), p.getEndTime(),
|
||||
p.getDurationMs(),
|
||||
p.getErrorMessage(), p.getErrorStackTrace(),
|
||||
p.getAttributes() != null ? new LinkedHashMap<>(p.getAttributes()) : null,
|
||||
p.getLoopIndex(), p.getLoopSize(),
|
||||
p.getSplitIndex(), p.getSplitSize(),
|
||||
p.getMulticastIndex(),
|
||||
p.getResolvedEndpointUri(),
|
||||
p.getErrorType(), p.getErrorCategory(),
|
||||
p.getRootCauseType(), p.getRootCauseMessage(),
|
||||
p.getErrorHandlerType(), p.getCircuitBreakerState(),
|
||||
p.getFallbackTriggered()
|
||||
);
|
||||
for (ProcessorNode child : convertProcessors(p.getChildren())) {
|
||||
node.addChild(child);
|
||||
}
|
||||
result.add(node);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: reconstruct processor tree from flat records.
|
||||
* Note: this loses iteration context for processors with the same ID across iterations.
|
||||
*/
|
||||
List<ProcessorNode> buildTree(List<ProcessorRecord> processors) {
|
||||
if (processors.isEmpty()) return List.of();
|
||||
|
||||
@@ -64,7 +121,11 @@ public class DetailService {
|
||||
p.loopIndex(), p.loopSize(),
|
||||
p.splitIndex(), p.splitSize(),
|
||||
p.multicastIndex(),
|
||||
p.resolvedEndpointUri()
|
||||
p.resolvedEndpointUri(),
|
||||
p.errorType(), p.errorCategory(),
|
||||
p.rootCauseType(), p.rootCauseMessage(),
|
||||
p.errorHandlerType(), p.circuitBreakerState(),
|
||||
p.fallbackTriggered()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,12 @@ public record ExecutionDetail(
|
||||
String outputBody,
|
||||
String inputHeaders,
|
||||
String outputHeaders,
|
||||
Map<String, String> attributes
|
||||
Map<String, String> attributes,
|
||||
String errorType,
|
||||
String errorCategory,
|
||||
String rootCauseType,
|
||||
String rootCauseMessage,
|
||||
String traceId,
|
||||
String spanId
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ public final class ProcessorNode {
|
||||
private final Integer splitSize;
|
||||
private final Integer multicastIndex;
|
||||
private final String resolvedEndpointUri;
|
||||
private final String errorType;
|
||||
private final String errorCategory;
|
||||
private final String rootCauseType;
|
||||
private final String rootCauseMessage;
|
||||
private final String errorHandlerType;
|
||||
private final String circuitBreakerState;
|
||||
private final Boolean fallbackTriggered;
|
||||
private final List<ProcessorNode> children;
|
||||
|
||||
public ProcessorNode(String processorId, String processorType, String status,
|
||||
@@ -37,7 +44,11 @@ public final class ProcessorNode {
|
||||
Integer loopIndex, Integer loopSize,
|
||||
Integer splitIndex, Integer splitSize,
|
||||
Integer multicastIndex,
|
||||
String resolvedEndpointUri) {
|
||||
String resolvedEndpointUri,
|
||||
String errorType, String errorCategory,
|
||||
String rootCauseType, String rootCauseMessage,
|
||||
String errorHandlerType, String circuitBreakerState,
|
||||
Boolean fallbackTriggered) {
|
||||
this.processorId = processorId;
|
||||
this.processorType = processorType;
|
||||
this.status = status;
|
||||
@@ -53,6 +64,13 @@ public final class ProcessorNode {
|
||||
this.splitSize = splitSize;
|
||||
this.multicastIndex = multicastIndex;
|
||||
this.resolvedEndpointUri = resolvedEndpointUri;
|
||||
this.errorType = errorType;
|
||||
this.errorCategory = errorCategory;
|
||||
this.rootCauseType = rootCauseType;
|
||||
this.rootCauseMessage = rootCauseMessage;
|
||||
this.errorHandlerType = errorHandlerType;
|
||||
this.circuitBreakerState = circuitBreakerState;
|
||||
this.fallbackTriggered = fallbackTriggered;
|
||||
this.children = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -75,5 +93,12 @@ public final class ProcessorNode {
|
||||
public Integer getSplitSize() { return splitSize; }
|
||||
public Integer getMulticastIndex() { return multicastIndex; }
|
||||
public String getResolvedEndpointUri() { return resolvedEndpointUri; }
|
||||
public String getErrorType() { return errorType; }
|
||||
public String getErrorCategory() { return errorCategory; }
|
||||
public String getRootCauseType() { return rootCauseType; }
|
||||
public String getRootCauseMessage() { return rootCauseMessage; }
|
||||
public String getErrorHandlerType() { return errorHandlerType; }
|
||||
public String getCircuitBreakerState() { return circuitBreakerState; }
|
||||
public Boolean getFallbackTriggered() { return fallbackTriggered; }
|
||||
public List<ProcessorNode> getChildren() { return List.copyOf(children); }
|
||||
}
|
||||
|
||||
@@ -107,7 +107,11 @@ public class IngestionService {
|
||||
diagramHash,
|
||||
exec.getEngineLevel(),
|
||||
inputBody, outputBody, inputHeaders, outputHeaders,
|
||||
toJson(exec.getAttributes())
|
||||
toJson(exec.getAttributes()),
|
||||
exec.getErrorType(), exec.getErrorCategory(),
|
||||
exec.getRootCauseType(), exec.getRootCauseMessage(),
|
||||
exec.getTraceId(), exec.getSpanId(),
|
||||
toJsonObject(exec.getProcessors())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,8 +137,10 @@ public class IngestionService {
|
||||
p.getSplitIndex(), p.getSplitSize(),
|
||||
p.getMulticastIndex(),
|
||||
p.getResolvedEndpointUri(),
|
||||
getDepthSafe(p, "splitDepth"),
|
||||
getDepthSafe(p, "loopDepth")
|
||||
p.getErrorType(), p.getErrorCategory(),
|
||||
p.getRootCauseType(), p.getRootCauseMessage(),
|
||||
p.getErrorHandlerType(), p.getCircuitBreakerState(),
|
||||
p.getFallbackTriggered()
|
||||
));
|
||||
if (p.getChildren() != null) {
|
||||
flat.addAll(flattenProcessors(
|
||||
@@ -145,16 +151,6 @@ public class IngestionService {
|
||||
return flat;
|
||||
}
|
||||
|
||||
/** Safely call getSplitDepth()/getLoopDepth() — returns 0 if not available in this cameleer3-common version. */
|
||||
private static int getDepthSafe(ProcessorExecution p, String field) {
|
||||
try {
|
||||
var method = p.getClass().getMethod("get" + Character.toUpperCase(field.charAt(0)) + field.substring(1));
|
||||
return (int) method.invoke(p);
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private String truncateBody(String body) {
|
||||
if (body == null) return null;
|
||||
if (body.length() > bodySizeLimit) return body.substring(0, bodySizeLimit);
|
||||
@@ -169,4 +165,13 @@ public class IngestionService {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
private static String toJsonObject(Object obj) {
|
||||
if (obj == null) return null;
|
||||
try {
|
||||
return JSON.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,11 @@ public interface ExecutionStore {
|
||||
String errorMessage, String errorStacktrace, String diagramContentHash,
|
||||
String engineLevel,
|
||||
String inputBody, String outputBody, String inputHeaders, String outputHeaders,
|
||||
String attributes
|
||||
String attributes,
|
||||
String errorType, String errorCategory,
|
||||
String rootCauseType, String rootCauseMessage,
|
||||
String traceId, String spanId,
|
||||
String processorsJson
|
||||
) {}
|
||||
|
||||
record ProcessorRecord(
|
||||
@@ -40,7 +44,9 @@ public interface ExecutionStore {
|
||||
Integer splitIndex, Integer splitSize,
|
||||
Integer multicastIndex,
|
||||
String resolvedEndpointUri,
|
||||
int splitDepth,
|
||||
int loopDepth
|
||||
String errorType, String errorCategory,
|
||||
String rootCauseType, String rootCauseMessage,
|
||||
String errorHandlerType, String circuitBreakerState,
|
||||
Boolean fallbackTriggered
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class TreeReconstructionTest {
|
||||
status, NOW, NOW, 10L,
|
||||
null, null, null, null, null, null, null,
|
||||
null, null, null, null, null,
|
||||
null, 0, 0
|
||||
null, null, null, null, null, null, null, null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user