This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-25750-body-type-dump in repository https://gitbox.apache.org/repos/asf/camel.git
commit 7f89abbf98c0a55e16b705fd7b2011074b4d5c80 Author: Claus Ibsen <[email protected]> AuthorDate: Tue Oct 20 14:51:18 2026 +0200 CAMEL-25750: camel-management, camel-console - the body types over JMX, in the route coverage dump and in a body-type console So the body types a workload brought to each step can be read and kept, such as after a test run: - JMX: each processor has BodyTypes and BodyKinds, and each route counts its steps reached with more than one type (VariedBodyTypes) and with more than one kind (MixedBodyKinds). The route console reads its counts from there. - The route coverage dump (dumpRoutesCoverageAsXml, written by the Camel test kits) has bodyTypes and bodyKinds attributes on each step a message reached. - The new body-type dev console lists those steps per route, with varied and filter options. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/catalog/dev-consoles-openapi.json | 98 +++++++++++++ .../apache/camel/catalog/dev-consoles.properties | 1 + .../camel/catalog/dev-consoles/body-type.json | 108 +++++++++++++++ .../apache/camel/catalog/docs/message-history.adoc | 11 ++ .../org/apache/camel/dev-console/body-type.json | 108 +++++++++++++++ .../org/apache/camel/dev-console/body-type | 2 + .../org/apache/camel/dev-consoles.properties | 2 +- .../camel/impl/console/BodyTypeDevConsole.java | 152 +++++++++++++++++++++ .../apache/camel/impl/console/RouteDevConsole.java | 32 +---- .../impl/console/RouteStructureDevConsole.java | 8 +- .../camel/impl/console/BodyTypeDevConsoleTest.java | 109 +++++++++++++++ .../docs/modules/eips/pages/message-history.adoc | 11 ++ .../management/mbean/ManagedProcessorMBean.java | 6 + .../api/management/mbean/ManagedRouteMBean.java | 6 + .../camel/management/mbean/ManagedProcessor.java | 21 +++ .../camel/management/mbean/ManagedRoute.java | 21 +++ .../management/mbean/RouteCoverageXmlParser.java | 8 ++ .../camel/management/ManagedBodyTypesTest.java | 114 ++++++++++++++++ 18 files changed, 786 insertions(+), 32 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json index 49f33a6b703d..3e50feb68773 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json @@ -624,6 +624,104 @@ } } }, + "\/q\/dev\/body-type": { + "get": { + "summary": "Body Type", + "description": "The body types each step of the routes has been reached with", + "operationId": "body-type", + "parameters": [ + { + "name": "filter", + "in": "query", + "description": "Filters the routes matching by route id, route uri, and source location", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "varied", + "in": "query", + "description": "Whether to include only the steps that have been reached with more than one body type", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Body Type output", + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "routes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "routeId": { + "type": "string", + "description": "The route ID" + }, + "source": { + "type": "string", + "description": "The source location (only present when known)" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The processor ID" + }, + "type": { + "type": "string", + "description": "The processor type" + }, + "line": { + "type": "integer", + "description": "The source line number (only present when known)" + }, + "code": { + "type": "string", + "description": "The code representation of this processor" + }, + "bodyTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The body types this processor has been reached with, across the messages, in the order first seen: more than one means the type varied" + }, + "bodyKinds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The kinds of the bodies this processor has been reached with: null, payload (text, bytes, a stream or a file), data (a Map, a List or an object) or iterator. More than one means the step needs attention" + } + } + }, + "description": "The steps of the route that a message has reached, with their body types" + } + } + }, + "description": "The routes with a step that a message has reached (the message history records the body types, such as in the dev profile)" + } + } + } + } + } + } + } + } + }, "\/q\/dev\/browse": { "get": { "summary": "Browse", diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties index 0c4a488209c1..d1ffc72f9789 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties @@ -6,6 +6,7 @@ azure-secrets bean bean-model blocked +body-type browse circuit-breaker consumer diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/body-type.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/body-type.json new file mode 100644 index 000000000000..9de50b978451 --- /dev/null +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/body-type.json @@ -0,0 +1,108 @@ +{ + "console": { + "kind": "console", + "group": "camel", + "name": "body-type", + "title": "Body Type", + "description": "The body types each step of the routes has been reached with", + "deprecated": false, + "readOnly": true, + "javaType": "org.apache.camel.impl.console.BodyTypeDevConsole", + "groupId": "org.apache.camel", + "artifactId": "camel-console", + "version": "4.24.0-SNAPSHOT" + }, + "options": { + "filter": { + "index": 0, + "kind": "option", + "displayName": "Filter", + "group": "query", + "label": "query", + "required": false, + "type": "string", + "javaType": "java.lang.String", + "deprecated": false, + "autowired": false, + "secret": false, + "description": "Filters the routes matching by route id, route uri, and source location" + }, + "varied": { + "index": 1, + "kind": "option", + "displayName": "Varied", + "group": "query", + "label": "query", + "required": false, + "type": "boolean", + "javaType": "java.lang.Boolean", + "deprecated": false, + "autowired": false, + "secret": false, + "defaultValue": false, + "description": "Whether to include only the steps that have been reached with more than one body type" + } + }, + "response": { + "type": "object", + "properties": { + "routes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "routeId": { + "type": "string", + "description": "The route ID" + }, + "source": { + "type": "string", + "description": "The source location (only present when known)" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The processor ID" + }, + "type": { + "type": "string", + "description": "The processor type" + }, + "line": { + "type": "integer", + "description": "The source line number (only present when known)" + }, + "code": { + "type": "string", + "description": "The code representation of this processor" + }, + "bodyTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The body types this processor has been reached with, across the messages, in the order first seen: more than one means the type varied" + }, + "bodyKinds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The kinds of the bodies this processor has been reached with: null, payload (text, bytes, a stream or a file), data (a Map, a List or an object) or iterator. More than one means the step needs attention" + } + } + }, + "description": "The steps of the route that a message has reached, with their body types" + } + } + }, + "description": "The routes with a step that a message has reached (the message history records the body types, such as in the dev profile)" + } + } + } +} + diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/message-history.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/message-history.adoc index 174e4ad313fe..40d18ec93aec 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/message-history.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/message-history.adoc @@ -477,6 +477,17 @@ List<BodyTypeRegistry.Kind> kinds = registry.getBodyKinds("orders", "to1"); The Camel CLI shows them as well, see xref:manual::camel-jbang-diagnostics.adoc[Camel CLI diagnostics]. +=== Dumping the body types + +Run the messages of the expected workload, such as in a test, and then read what each step has been reached with: + +* The `body-type` dev console lists the steps that a message has reached, per route, with their types and kinds. + `varied=true` keeps only the steps reached with more than one type, and `filter` selects the routes. +* Over JMX, each processor has the `BodyTypes` and `BodyKinds` attributes, and each route counts its steps reached with + more than one type (`VariedBodyTypes`) and with bodies of more than one kind (`MixedBodyKinds`). +* The route coverage dump (`dumpRoutesCoverageAsXml`, which the Camel test kits write when route coverage is enabled) + has `bodyTypes` and `bodyKinds` attributes on each step a message has reached. + == Configuring MessageHistory format The Message History format in Camel is controlled by `Java String.format` patterns stored as global options on the CamelContext. diff --git a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/body-type.json b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/body-type.json new file mode 100644 index 000000000000..9de50b978451 --- /dev/null +++ b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/body-type.json @@ -0,0 +1,108 @@ +{ + "console": { + "kind": "console", + "group": "camel", + "name": "body-type", + "title": "Body Type", + "description": "The body types each step of the routes has been reached with", + "deprecated": false, + "readOnly": true, + "javaType": "org.apache.camel.impl.console.BodyTypeDevConsole", + "groupId": "org.apache.camel", + "artifactId": "camel-console", + "version": "4.24.0-SNAPSHOT" + }, + "options": { + "filter": { + "index": 0, + "kind": "option", + "displayName": "Filter", + "group": "query", + "label": "query", + "required": false, + "type": "string", + "javaType": "java.lang.String", + "deprecated": false, + "autowired": false, + "secret": false, + "description": "Filters the routes matching by route id, route uri, and source location" + }, + "varied": { + "index": 1, + "kind": "option", + "displayName": "Varied", + "group": "query", + "label": "query", + "required": false, + "type": "boolean", + "javaType": "java.lang.Boolean", + "deprecated": false, + "autowired": false, + "secret": false, + "defaultValue": false, + "description": "Whether to include only the steps that have been reached with more than one body type" + } + }, + "response": { + "type": "object", + "properties": { + "routes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "routeId": { + "type": "string", + "description": "The route ID" + }, + "source": { + "type": "string", + "description": "The source location (only present when known)" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The processor ID" + }, + "type": { + "type": "string", + "description": "The processor type" + }, + "line": { + "type": "integer", + "description": "The source line number (only present when known)" + }, + "code": { + "type": "string", + "description": "The code representation of this processor" + }, + "bodyTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The body types this processor has been reached with, across the messages, in the order first seen: more than one means the type varied" + }, + "bodyKinds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The kinds of the bodies this processor has been reached with: null, payload (text, bytes, a stream or a file), data (a Map, a List or an object) or iterator. More than one means the step needs attention" + } + } + }, + "description": "The steps of the route that a message has reached, with their body types" + } + } + }, + "description": "The routes with a step that a message has reached (the message history records the body types, such as in the dev profile)" + } + } + } +} + diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/body-type b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/body-type new file mode 100644 index 000000000000..bb4f0bd228f5 --- /dev/null +++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/body-type @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.impl.console.BodyTypeDevConsole diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties index 259ccb5a6d1e..4ecce3e5c9ff 100644 --- a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties +++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties @@ -1,5 +1,5 @@ # Generated by camel build tools - do NOT edit this file! -dev-consoles=activity api bean blocked browse circuit-breaker consumer context datasource debug endpoint errors eval-language event gc health heap-dump heap-histogram inflight internal-tasks java-security jfr-memory-leak jvm log memory message-history processor processor-detail producer properties receive reload rest rest-spec route route-controller route-dump route-group route-structure route-topology send service simple-language source sql-query sql-trace startup-recorder system-proper [...] +dev-consoles=activity api bean blocked body-type browse circuit-breaker consumer context datasource debug endpoint errors eval-language event gc health heap-dump heap-histogram inflight internal-tasks java-security jfr-memory-leak jvm log memory message-history processor processor-detail producer properties receive reload rest rest-spec route route-controller route-dump route-group route-structure route-topology send service simple-language source sql-query sql-trace startup-recorder sys [...] groupId=org.apache.camel artifactId=camel-console version=4.24.0-SNAPSHOT diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/BodyTypeDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/BodyTypeDevConsole.java new file mode 100644 index 000000000000..d48fce215651 --- /dev/null +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/BodyTypeDevConsole.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.impl.console; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.camel.NamedRoute; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.ModelDumpLine; +import org.apache.camel.spi.ModelToStructureDumper; +import org.apache.camel.spi.annotations.DevConsole; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.console.AbstractDevConsole; +import org.apache.camel.util.json.JsonRecordSupport; +import org.apache.camel.util.json.Jsoner; + +import static org.apache.camel.impl.console.ConsoleHelper.extractSourceLocationLineNumber; +import static org.apache.camel.impl.console.ConsoleHelper.extractSourceLocationNoLineNumber; + +/** + * The body types each step of the routes has been reached with, across the messages, as the + * {@link org.apache.camel.spi.BodyTypeRegistry} keeps them while the message history is on (CAMEL-25750). + */ +@DevConsole(name = "body-type", description = "The body types each step of the routes has been reached with") +public class BodyTypeDevConsole extends AbstractDevConsole { + + public record StepEntry( + @Metadata(description = "The processor ID") String id, + @Metadata(description = "The processor type") String type, + @Metadata(description = "The source line number (only present when known)") Integer line, + @Metadata(description = "The code representation of this processor") String code, + @Metadata(description = "The body types this processor has been reached with, across the messages, in the order first seen: more than one means the type varied") List<String> bodyTypes, + @Metadata(description = "The kinds of the bodies this processor has been reached with: null, payload (text, bytes, a stream or a file), data (a Map, a List or an object) or iterator. More than one means the step needs attention") List<String> bodyKinds) { + } + + public record RouteEntry( + @Metadata(description = "The route ID") String routeId, + @Metadata(description = "The source location (only present when known)") String source, + @Metadata(description = "The steps of the route that a message has reached, with their body types") List<StepEntry> steps) { + } + + public record Response( + @Metadata(description = "The routes with a step that a message has reached (the message history records the body types, such as in the dev profile)") List<RouteEntry> routes) { + } + + @Metadata(label = "query", description = "Filters the routes matching by route id, route uri, and source location", + javaType = "java.lang.String") + public static final String FILTER = "filter"; + + @Metadata(label = "query", + description = "Whether to include only the steps that have been reached with more than one body type", + javaType = "java.lang.Boolean", defaultValue = "false") + public static final String VARIED = "varied"; + + public BodyTypeDevConsole() { + super("camel", "body-type", "Body Type", "The body types each step of the routes has been reached with"); + } + + @Override + protected String doCallText(Map<String, Object> options) { + List<RouteEntry> routes = routes(options); + if (routes.isEmpty()) { + return "No step has been reached by a message while the message history is on (the dev profile turns it on)\n"; + } + StringBuilder sb = new StringBuilder(); + for (RouteEntry route : routes) { + sb.append(String.format(" Route: %s", route.routeId())); + if (route.source() != null) { + sb.append(String.format(" (%s)", route.source())); + } + sb.append("\n\n"); + int width = 0; + for (StepEntry step : route.steps()) { + width = Math.max(width, Jsoner.unescape(step.code()).length()); + } + for (StepEntry step : route.steps()) { + String num = step.line() != null ? String.format("%4d: ", step.line()) : " "; + String text = String.format("%-" + width + "s body: %s", Jsoner.unescape(step.code()), + RouteStructureDevConsole.bodyTypesText( + new RouteStructureDevConsole.LineBodyTypes(step.bodyTypes(), step.bodyKinds()))); + sb.append(num).append(text).append("\n"); + } + sb.append("\n"); + } + return sb.toString(); + } + + @Override + protected Map<String, Object> doCallJson(Map<String, Object> options) { + return JsonRecordSupport.toJsonObject(new Response(routes(options))); + } + + private List<RouteEntry> routes(Map<String, Object> options) { + final String filter = optionString(options, FILTER); + final boolean varied = optionBoolean(options, VARIED, false); + + List<RouteEntry> answer = new ArrayList<>(); + List<NamedRoute> routes = getCamelContext().getNamedRouteDefinitions(); + routes.sort((o1, o2) -> o1.getRouteId().compareToIgnoreCase(o2.getRouteId())); + for (NamedRoute route : routes) { + if (!RouteStructureDevConsole.accept(route, filter)) { + continue; + } + List<StepEntry> steps = steps(route.getRouteId(), varied); + if (!steps.isEmpty()) { + String source = route.getResource() != null + ? extractSourceLocationNoLineNumber(route.getResource().getLocation()) : null; + answer.add(new RouteEntry(route.getRouteId(), source, steps)); + } + } + return answer; + } + + private List<StepEntry> steps(String routeId, boolean varied) { + List<StepEntry> answer = new ArrayList<>(); + try { + ModelToStructureDumper dumper = PluginHelper.getModelToStructureDumper(getCamelContext()); + List<ModelDumpLine> lines = dumper.dumpStructure(getCamelContext(), routeId, false); + List<RouteStructureDevConsole.LineBodyTypes> bodyTypes + = RouteStructureDevConsole.bodyTypesPerLine(getCamelContext(), routeId, lines); + for (int i = 0; i < lines.size(); i++) { + RouteStructureDevConsole.LineBodyTypes types = bodyTypes.get(i); + if (types == null || varied && types.types().size() < 2) { + continue; + } + ModelDumpLine line = lines.get(i); + answer.add(new StepEntry( + line.id(), line.type(), extractSourceLocationLineNumber(line.location()), Jsoner.escape(line.code()), + types.types(), types.kinds())); + } + } catch (Exception e) { + // the route has no model to dump, such as a route added without one + } + return answer; + } +} diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java index 6d605a29a771..a05f83d9f399 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java @@ -32,7 +32,6 @@ import org.apache.camel.Route; import org.apache.camel.api.management.ManagedCamelContext; import org.apache.camel.api.management.mbean.ManagedProcessorMBean; import org.apache.camel.api.management.mbean.ManagedRouteMBean; -import org.apache.camel.spi.BodyTypeRegistry; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.annotations.DevConsole; import org.apache.camel.support.ExceptionHelper; @@ -316,12 +315,15 @@ public class RouteDevConsole extends AbstractDevConsole { List<ProcessorDevConsole.ProcessorEntry> procList = processors ? includeProcessorsJson(mrb) : null; Route r = getCamelContext().getRoute(mrb.getRouteId()); - BodyTypeCounts bodyTypes = bodyTypeCounts(mrb.getRouteId()); + // the steps reached with more than one body type, and with more than one kind (CAMEL-25750) + int varied = mrb.getVariedBodyTypes(); + int mixed = mrb.getMixedBodyKinds(); list.add(new RouteEntry( mrb.getRouteId(), mrb.getRouteGroup(), mrb.getNodePrefixId(), mrb.getDescription(), mrb.getNote(), mrb.isCreatedByKamelet(), mrb.isCreatedByRouteTemplate(), mrb.getRouteTemplateId(), mrb.getEndpointUri(), mrb.isRemoteEndpoint(), mrb.getSourceLocation(), mrb.getTemplatedRouteSource(), mrb.getState(), - r != null && r.supportsSuspension(), mrb.getUptime(), bodyTypes.varied(), bodyTypes.mixed(), lastError, + r != null && r.supportsSuspension(), mrb.getUptime(), varied > 0 ? varied : null, mixed > 0 ? mixed : null, + lastError, stats, procList)); return null; }; @@ -513,30 +515,6 @@ public class RouteDevConsole extends AbstractDevConsole { } } - /** - * How many steps of a route have been reached with more than one body type, and with bodies of more than one kind - * (each null when there are none). - */ - private record BodyTypeCounts(Integer varied, Integer mixed) { - } - - private BodyTypeCounts bodyTypeCounts(String routeId) { - BodyTypeRegistry registry = getCamelContext().getCamelContextExtension().getContextPlugin(BodyTypeRegistry.class); - int varied = 0; - int mixed = 0; - if (registry != null) { - for (Map.Entry<String, List<String>> node : registry.getBodyTypes(routeId).entrySet()) { - if (node.getValue().size() > 1) { - varied++; - if (registry.getBodyKinds(routeId, node.getKey()).size() > 1) { - mixed++; - } - } - } - } - return new BodyTypeCounts(varied > 0 ? varied : null, mixed > 0 ? mixed : null); - } - public static JsonObject gatherRouteStats(CamelContext camelContext, ManagedRouteMBean mrb) { return JsonRecordSupport.toJsonObject(buildStatistics(camelContext, mrb)); } diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java index cb95e0bf9916..e320429cc64c 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java @@ -67,7 +67,7 @@ public class RouteStructureDevConsole extends AbstractDevConsole { /** * The body types and kinds a line of the route structure has been reached with. */ - private record LineBodyTypes(List<String> types, List<String> kinds) { + record LineBodyTypes(List<String> types, List<String> kinds) { } public record RouteEntry( @@ -210,7 +210,7 @@ public class RouteStructureDevConsole extends AbstractDevConsole { .forEach(task::apply); } - private static boolean accept(NamedRoute route, String filter) { + static boolean accept(NamedRoute route, String filter) { if (filter == null || filter.isBlank()) { return true; } @@ -280,7 +280,7 @@ public class RouteStructureDevConsole extends AbstractDevConsole { * for the route and its from (the first two lines), and for a processor no exchange has reached. The first * processor has the body as the from gave it. */ - private static List<LineBodyTypes> bodyTypesPerLine( + static List<LineBodyTypes> bodyTypesPerLine( CamelContext camelContext, String routeId, List<ModelDumpLine> lines) { BodyTypeRegistry registry = camelContext.getCamelContextExtension().getContextPlugin(BodyTypeRegistry.class); List<LineBodyTypes> answer = new ArrayList<>(lines.size()); @@ -313,7 +313,7 @@ public class RouteStructureDevConsole extends AbstractDevConsole { * The body types by their simple class names, such as {@code String | LinkedHashMap}, followed by the kinds when * there is more than one: {@code String | LinkedHashMap (payload and data)}. */ - private static String bodyTypesText(LineBodyTypes types) { + static String bodyTypesText(LineBodyTypes types) { StringJoiner sj = new StringJoiner(" | "); for (String type : types.types()) { sj.add(StringHelper.afterLast(type, ".", type)); diff --git a/core/camel-console/src/test/java/org/apache/camel/impl/console/BodyTypeDevConsoleTest.java b/core/camel-console/src/test/java/org/apache/camel/impl/console/BodyTypeDevConsoleTest.java new file mode 100644 index 000000000000..35b53c142a1e --- /dev/null +++ b/core/camel-console/src/test/java/org/apache/camel/impl/console/BodyTypeDevConsoleTest.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.impl.console; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.console.DevConsole; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * The body-type console dumps what each step has been reached with (CAMEL-25750). + */ +public class BodyTypeDevConsoleTest extends ContextTestSupport { + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + context.setMessageHistory(true); + from("direct:start").routeId("orders") + .log("${body}").id("logIt") + .to("mock:result").id("result"); + from("direct:quiet").routeId("quiet") + .to("mock:quiet").id("quietResult"); + } + }; + } + + @Test + public void testTheStepsAMessageReached() { + template.sendBody("direct:start", "Hello World"); + template.sendBody("direct:start", new LinkedHashMap<>(Map.of("id", 1))); + + DevConsole con = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("body-type"); + Assertions.assertNotNull(con); + Assertions.assertEquals("camel", con.getGroup()); + + JsonObject out = (JsonObject) con.call(DevConsole.MediaType.JSON); + JsonArray routes = out.getCollection("routes"); + // the quiet route has not been reached by a message + Assertions.assertEquals(1, routes.size()); + JsonObject route = (JsonObject) routes.get(0); + Assertions.assertEquals("orders", route.getString("routeId")); + JsonArray steps = route.getCollection("steps"); + Assertions.assertEquals(2, steps.size()); + JsonObject result = (JsonObject) steps.get(1); + Assertions.assertEquals("result", result.getString("id")); + Assertions.assertEquals(List.of("java.lang.String", "java.util.LinkedHashMap"), result.getCollection("bodyTypes")); + Assertions.assertEquals(List.of("payload", "data"), result.getCollection("bodyKinds")); + + String text = (String) con.call(DevConsole.MediaType.TEXT); + Assertions.assertTrue(text.contains("Route: orders"), text); + Assertions.assertTrue(text.contains("body: String | LinkedHashMap (payload and data)"), text); + } + + @Test + public void testOnlyTheVariedSteps() { + template.sendBody("direct:start", "Hello World"); + template.sendBody("direct:start", "Bye World"); + template.sendBody("direct:quiet", "Hello"); + + DevConsole con = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("body-type"); + JsonObject all = (JsonObject) con.call(DevConsole.MediaType.JSON); + Assertions.assertEquals(2, ((JsonArray) all.getCollection("routes")).size()); + + // every step was reached with text only + JsonObject varied = (JsonObject) con.call(DevConsole.MediaType.JSON, Map.of(BodyTypeDevConsole.VARIED, "true")); + Assertions.assertTrue(((JsonArray) varied.getCollection("routes")).isEmpty()); + + // a filter by route id + JsonObject quiet = (JsonObject) con.call(DevConsole.MediaType.JSON, Map.of(BodyTypeDevConsole.FILTER, "quiet")); + JsonArray routes = quiet.getCollection("routes"); + Assertions.assertEquals(1, routes.size()); + Assertions.assertEquals("quiet", ((JsonObject) routes.get(0)).getString("routeId")); + } + + @Test + public void testNothingReachedYet() { + DevConsole con = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("body-type"); + JsonObject out = (JsonObject) con.call(DevConsole.MediaType.JSON); + Assertions.assertTrue(((JsonArray) out.getCollection("routes")).isEmpty()); + + String text = (String) con.call(DevConsole.MediaType.TEXT); + Assertions.assertTrue(text.startsWith("No step has been reached"), text); + } +} diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/message-history.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/message-history.adoc index 174e4ad313fe..40d18ec93aec 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/message-history.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/message-history.adoc @@ -477,6 +477,17 @@ List<BodyTypeRegistry.Kind> kinds = registry.getBodyKinds("orders", "to1"); The Camel CLI shows them as well, see xref:manual::camel-jbang-diagnostics.adoc[Camel CLI diagnostics]. +=== Dumping the body types + +Run the messages of the expected workload, such as in a test, and then read what each step has been reached with: + +* The `body-type` dev console lists the steps that a message has reached, per route, with their types and kinds. + `varied=true` keeps only the steps reached with more than one type, and `filter` selects the routes. +* Over JMX, each processor has the `BodyTypes` and `BodyKinds` attributes, and each route counts its steps reached with + more than one type (`VariedBodyTypes`) and with bodies of more than one kind (`MixedBodyKinds`). +* The route coverage dump (`dumpRoutesCoverageAsXml`, which the Camel test kits write when route coverage is enabled) + has `bodyTypes` and `bodyKinds` attributes on each step a message has reached. + == Configuring MessageHistory format The Message History format in Camel is controlled by `Java String.format` patterns stored as global options on the CamelContext. diff --git a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedProcessorMBean.java b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedProcessorMBean.java index e60a4dc99718..bb76c62e91c3 100644 --- a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedProcessorMBean.java +++ b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedProcessorMBean.java @@ -69,6 +69,12 @@ public interface ManagedProcessorMBean extends ManagedPerformanceCounterMBean { @ManagedAttribute(description = "Line number of this node in the source file (when loaded from a line number aware parser)") Integer getSourceLineNumber(); + @ManagedAttribute(description = "The body types this processor has been reached with, across the messages, in the order first seen (recorded while the message history is on)") + String[] getBodyTypes(); + + @ManagedAttribute(description = "The kinds of the bodies this processor has been reached with: null, payload, data or iterator. More than one needs attention: a step that writes fails for data, one that reads fields fails for a payload") + String[] getBodyKinds(); + @ManagedAttribute(description = "Whether this processor supports extended JMX information") Boolean getSupportExtendedInformation(); diff --git a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java index 25b5c673b98b..f7e8731179a2 100644 --- a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java +++ b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java @@ -58,6 +58,12 @@ public interface ManagedRouteMBean extends ManagedPerformanceCounterMBean { @ManagedAttribute(description = "Route Source Location (Short)") String getSourceLocationShort(); + @ManagedAttribute(description = "The number of steps that have been reached with more than one body type, across the messages (recorded while the message history is on)") + int getVariedBodyTypes(); + + @ManagedAttribute(description = "The number of steps that have been reached with bodies of more than one kind (null, payload, data or iterator), which need attention") + int getMixedBodyKinds(); + @ManagedAttribute(description = "The id of the route template (or Kamelet) this route was created from") String getRouteTemplateId(); diff --git a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedProcessor.java b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedProcessor.java index 79a6d2b1338f..74b32085235c 100644 --- a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedProcessor.java +++ b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedProcessor.java @@ -16,6 +16,7 @@ */ package org.apache.camel.management.mbean; +import java.util.Locale; import java.util.Objects; import org.apache.camel.CamelContext; @@ -33,6 +34,7 @@ import org.apache.camel.model.ProcessorDefinition; import org.apache.camel.model.ProcessorDefinitionHelper; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.StepDefinition; +import org.apache.camel.spi.BodyTypeRegistry; import org.apache.camel.spi.ManagementStrategy; import org.apache.camel.spi.NodeIdFactory; import org.apache.camel.spi.RouteIdAware; @@ -144,6 +146,25 @@ public class ManagedProcessor extends ManagedPerformanceCounter return line >= 0 ? line : null; } + @Override + public String[] getBodyTypes() { + BodyTypeRegistry registry = context.getCamelContextExtension().getContextPlugin(BodyTypeRegistry.class); + if (registry == null || getRouteId() == null || getProcessorId() == null) { + return new String[0]; + } + return registry.getBodyTypes(getRouteId(), getProcessorId()).toArray(new String[0]); + } + + @Override + public String[] getBodyKinds() { + BodyTypeRegistry registry = context.getCamelContextExtension().getContextPlugin(BodyTypeRegistry.class); + if (registry == null || getRouteId() == null || getProcessorId() == null) { + return new String[0]; + } + return registry.getBodyKinds(getRouteId(), getProcessorId()).stream() + .map(kind -> kind.name().toLowerCase(Locale.ROOT)).toArray(String[]::new); + } + @Override public Boolean getSupportExtendedInformation() { return false; diff --git a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java index 9a54c5331053..ccf087f26c87 100644 --- a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java +++ b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java @@ -55,6 +55,7 @@ import org.apache.camel.model.Model; import org.apache.camel.model.ModelCamelContext; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.RoutesDefinition; +import org.apache.camel.spi.BodyTypeRegistry; import org.apache.camel.spi.InflightRepository; import org.apache.camel.spi.ManagementAgent; import org.apache.camel.spi.ManagementStrategy; @@ -204,6 +205,26 @@ public class ManagedRoute extends ManagedPerformanceCounter implements ManagedRo return sourceLocationShort; } + @Override + public int getVariedBodyTypes() { + BodyTypeRegistry registry = context.getCamelContextExtension().getContextPlugin(BodyTypeRegistry.class); + if (registry == null || getRouteId() == null) { + return 0; + } + return (int) registry.getBodyTypes(getRouteId()).values().stream().filter(types -> types.size() > 1).count(); + } + + @Override + public int getMixedBodyKinds() { + BodyTypeRegistry registry = context.getCamelContextExtension().getContextPlugin(BodyTypeRegistry.class); + if (registry == null || getRouteId() == null) { + return 0; + } + String routeId = getRouteId(); + return (int) registry.getBodyTypes(routeId).keySet().stream() + .filter(nodeId -> registry.getBodyKinds(routeId, nodeId).size() > 1).count(); + } + @Override public String getRouteTemplateId() { return route.getRouteTemplateId(); diff --git a/core/camel-management/src/main/java/org/apache/camel/management/mbean/RouteCoverageXmlParser.java b/core/camel-management/src/main/java/org/apache/camel/management/mbean/RouteCoverageXmlParser.java index 50df46a5730c..00b64f8e5406 100644 --- a/core/camel-management/src/main/java/org/apache/camel/management/mbean/RouteCoverageXmlParser.java +++ b/core/camel-management/src/main/java/org/apache/camel/management/mbean/RouteCoverageXmlParser.java @@ -46,6 +46,8 @@ import org.apache.camel.api.management.mbean.ManagedRouteMBean; * <ul> * <li>exchangesTotal - Total number of exchanges</li> * <li>totalProcessingTime - Total processing time in millis</li> + * <li>bodyTypes - The body types the step has been reached with (when the message history recorded any)</li> + * <li>bodyKinds - The kinds of those bodies: null, payload, data or iterator</li> * </ul> * Is included as attributes on the route nodes. */ @@ -152,6 +154,12 @@ public final class RouteCoverageXmlParser { el.setAttribute("totalProcessingTime", Long.toString(totalTime)); int index = processor.getIndex(); el.setAttribute("index", Integer.toString(index)); + // the body types the step was reached with, and their kinds (CAMEL-25750) + String[] types = processor.getBodyTypes(); + if (types != null && types.length > 0) { + el.setAttribute("bodyTypes", String.join(",", types)); + el.setAttribute("bodyKinds", String.join(",", processor.getBodyKinds())); + } } } } catch (Exception e) { diff --git a/core/camel-management/src/test/java/org/apache/camel/management/ManagedBodyTypesTest.java b/core/camel-management/src/test/java/org/apache/camel/management/ManagedBodyTypesTest.java new file mode 100644 index 000000000000..35e07ac452c1 --- /dev/null +++ b/core/camel-management/src/test/java/org/apache/camel/management/ManagedBodyTypesTest.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.management; + +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.w3c.dom.Document; + +import org.apache.camel.builder.RouteBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import static org.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_PROCESSOR; +import static org.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_ROUTE; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The body types each step has been reached with, over JMX and in the route coverage dump (CAMEL-25750). + */ +@DisabledOnOs(OS.AIX) +public class ManagedBodyTypesTest extends ManagementTestSupport { + + @Test + public void testBodyTypesOverJmx() throws Exception { + sendTextAndData(); + + MBeanServer mbeanServer = getMBeanServer(); + ObjectName result = getCamelObjectName(TYPE_PROCESSOR, "result"); + assertArrayEquals(new String[] { "java.lang.String", "java.util.LinkedHashMap" }, + (String[]) mbeanServer.getAttribute(result, "BodyTypes")); + assertArrayEquals(new String[] { "payload", "data" }, (String[]) mbeanServer.getAttribute(result, "BodyKinds")); + + ObjectName maybeMap = getCamelObjectName(TYPE_PROCESSOR, "maybeMap"); + assertArrayEquals(new String[] { "java.lang.String" }, (String[]) mbeanServer.getAttribute(maybeMap, "BodyTypes")); + + // the route counts its steps: the choice and the mock were reached with text and with data + ObjectName route = getCamelObjectName(TYPE_ROUTE, "orders"); + assertEquals(2, mbeanServer.getAttribute(route, "VariedBodyTypes")); + assertEquals(2, mbeanServer.getAttribute(route, "MixedBodyKinds")); + } + + @Test + public void testNoBodyTypesBeforeAMessage() throws Exception { + MBeanServer mbeanServer = getMBeanServer(); + ObjectName result = getCamelObjectName(TYPE_PROCESSOR, "result"); + assertArrayEquals(new String[0], (String[]) mbeanServer.getAttribute(result, "BodyTypes")); + assertEquals(0, mbeanServer.getAttribute(getCamelObjectName(TYPE_ROUTE, "orders"), "MixedBodyKinds")); + } + + @Test + public void testRouteCoverageHasTheBodyTypes() throws Exception { + sendTextAndData(); + + String xml = (String) getMBeanServer().invoke(getContextObjectName(), "dumpRoutesCoverageAsXml", null, null); + log.info(xml); + + assertTrue(xml.contains("bodyTypes=\"java.lang.String,java.util.LinkedHashMap\""), xml); + assertTrue(xml.contains("bodyKinds=\"payload,data\""), xml); + // should be valid XML + Document doc = context.getTypeConverter().convertTo(Document.class, xml); + assertNotNull(doc); + } + + private void sendTextAndData() throws Exception { + getMockEndpoint("mock:result").expectedMessageCount(2); + template.sendBody("direct:start", "hello"); + template.sendBody("direct:start", "map"); + assertMockEndpointsSatisfied(); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + context.setMessageHistory(true); + + from("direct:start").routeId("orders") + .process(e -> { + if ("map".equals(e.getMessage().getBody())) { + e.getMessage().setBody(new LinkedHashMap<>(Map.of("id", 1))); + } + }).id("maybeMap") + .choice().id("choice") + .when(body().isInstanceOf(Map.class)) + .log("a map").id("onlyMaps") + .end() + .to("mock:result").id("result"); + } + }; + } +}
