This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-23837-datasource-devconsole in repository https://gitbox.apache.org/repos/asf/camel.git
commit e7beff6b7f952d1f29423a040bce34bb23ecb428 Author: Claus Ibsen <[email protected]> AuthorDate: Sat Jun 27 20:59:39 2026 +0200 CAMEL-23837: Add DataSource connection pool dev-console and TUI tab Adds a new dev-console that discovers javax.sql.DataSource beans from the Camel registry and extracts connection pool metrics via reflection for HikariCP (Spring Boot) and Agroal (Quarkus). Includes a TUI DataSource tab showing pool name, active/idle/total connections, max pool size, and waiting threads with color-coded pool exhaustion warnings. Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../console/DataSourceDevConsoleConfigurer.java | 63 +++++ .../org/apache/camel/dev-console/datasource.json | 15 ++ ....apache.camel.impl.console.DataSourceDevConsole | 2 + .../org/apache/camel/dev-console/datasource | 2 + .../org/apache/camel/dev-consoles.properties | 2 +- .../camel/impl/console/DataSourceDevConsole.java | 225 ++++++++++++++++ .../camel/cli/connector/LocalCliConnector.java | 7 + .../dsl/jbang/core/commands/tui/CamelMonitor.java | 57 +++-- .../jbang/core/commands/tui/DataSourceInfo.java | 32 +++ .../dsl/jbang/core/commands/tui/DataSourceTab.java | 283 +++++++++++++++++++++ .../jbang/core/commands/tui/IntegrationInfo.java | 1 + .../dsl/jbang/core/commands/tui/StatusParser.java | 25 ++ 12 files changed, 689 insertions(+), 25 deletions(-) diff --git a/core/camel-console/src/generated/java/org/apache/camel/impl/console/DataSourceDevConsoleConfigurer.java b/core/camel-console/src/generated/java/org/apache/camel/impl/console/DataSourceDevConsoleConfigurer.java new file mode 100644 index 000000000000..f84a33a843d1 --- /dev/null +++ b/core/camel-console/src/generated/java/org/apache/camel/impl/console/DataSourceDevConsoleConfigurer.java @@ -0,0 +1,63 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.impl.console; + +import javax.annotation.processing.Generated; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.ExtendedPropertyConfigurerGetter; +import org.apache.camel.spi.PropertyConfigurerGetter; +import org.apache.camel.spi.ConfigurerStrategy; +import org.apache.camel.spi.GeneratedPropertyConfigurer; +import org.apache.camel.util.CaseInsensitiveMap; +import org.apache.camel.impl.console.DataSourceDevConsole; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo") +@SuppressWarnings("unchecked") +public class DataSourceDevConsoleConfigurer extends org.apache.camel.support.component.PropertyConfigurerSupport implements GeneratedPropertyConfigurer, ExtendedPropertyConfigurerGetter { + + private static final Map<String, Object> ALL_OPTIONS; + static { + Map<String, Object> map = new CaseInsensitiveMap(); + map.put("CamelContext", org.apache.camel.CamelContext.class); + ALL_OPTIONS = map; + } + + @Override + public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) { + org.apache.camel.impl.console.DataSourceDevConsole target = (org.apache.camel.impl.console.DataSourceDevConsole) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "camelcontext": + case "camelContext": target.setCamelContext(property(camelContext, org.apache.camel.CamelContext.class, value)); return true; + default: return false; + } + } + + @Override + public Map<String, Object> getAllOptions(Object target) { + return ALL_OPTIONS; + } + + @Override + public Class<?> getOptionType(String name, boolean ignoreCase) { + switch (ignoreCase ? name.toLowerCase() : name) { + case "camelcontext": + case "camelContext": return org.apache.camel.CamelContext.class; + default: return null; + } + } + + @Override + public Object getOptionValue(Object obj, String name, boolean ignoreCase) { + org.apache.camel.impl.console.DataSourceDevConsole target = (org.apache.camel.impl.console.DataSourceDevConsole) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "camelcontext": + case "camelContext": return target.getCamelContext(); + default: return null; + } + } +} + diff --git a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/datasource.json b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/datasource.json new file mode 100644 index 000000000000..8889232ffa1a --- /dev/null +++ b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/datasource.json @@ -0,0 +1,15 @@ +{ + "console": { + "kind": "console", + "group": "camel", + "name": "datasource", + "title": "DataSource", + "description": "Displays DataSource connection pool metrics", + "deprecated": false, + "javaType": "org.apache.camel.impl.console.DataSourceDevConsole", + "groupId": "org.apache.camel", + "artifactId": "camel-console", + "version": "4.21.0-SNAPSHOT" + } +} + diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.DataSourceDevConsole b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.DataSourceDevConsole new file mode 100644 index 000000000000..891a3bb086d7 --- /dev/null +++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.DataSourceDevConsole @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.impl.console.DataSourceDevConsoleConfigurer diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/datasource b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/datasource new file mode 100644 index 000000000000..84c6e3b35cca --- /dev/null +++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/datasource @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.impl.console.DataSourceDevConsole 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 84bbd4e88131..e25def6b5f32 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=bean blocked browse circuit-breaker consumer context debug endpoint errors eval-language event gc health inflight internal-tasks java-security jvm log memory message-history processor producer properties receive reload rest rest-spec route route-controller route-dump route-group route-structure route-topology send service simple-language source startup-recorder system-properties thread top trace transformers type-converters variables +dev-consoles=bean blocked browse circuit-breaker consumer context datasource debug endpoint errors eval-language event gc health inflight internal-tasks java-security jvm log memory message-history processor producer properties receive reload rest rest-spec route route-controller route-dump route-group route-structure route-topology send service simple-language source startup-recorder system-properties thread top trace transformers type-converters variables groupId=org.apache.camel artifactId=camel-console version=4.21.0-SNAPSHOT diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/DataSourceDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/DataSourceDevConsole.java new file mode 100644 index 000000000000..496add4835f7 --- /dev/null +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/DataSourceDevConsole.java @@ -0,0 +1,225 @@ +/* + * 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.lang.reflect.Method; +import java.util.Map; + +import javax.sql.DataSource; + +import org.apache.camel.spi.Configurer; +import org.apache.camel.spi.annotations.DevConsole; +import org.apache.camel.support.console.AbstractDevConsole; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; + +@DevConsole(name = "datasource", displayName = "DataSource", description = "Displays DataSource connection pool metrics") +@Configurer(extended = true) +public class DataSourceDevConsole extends AbstractDevConsole { + + private static final String HIKARI_CLASS = "com.zaxxer.hikari.HikariDataSource"; + private static final String AGROAL_CLASS = "io.agroal.api.AgroalDataSource"; + + public DataSourceDevConsole() { + super("camel", "datasource", "DataSource", "Displays DataSource connection pool metrics"); + } + + @Override + protected String doCallText(Map<String, Object> options) { + StringBuilder sb = new StringBuilder(); + + Map<String, DataSource> dataSources + = getCamelContext().getRegistry().findByTypeWithName(DataSource.class); + + if (dataSources.isEmpty()) { + sb.append("No DataSources found in registry\n"); + return sb.toString(); + } + + for (Map.Entry<String, DataSource> entry : dataSources.entrySet()) { + DataSource ds = entry.getValue(); + String poolType = detectPoolType(ds); + + sb.append(String.format("DataSource: %s (%s)%n", entry.getKey(), poolType)); + sb.append(String.format(" Type: %s%n", ds.getClass().getName())); + + if ("HikariCP".equals(poolType)) { + appendHikariText(sb, ds); + } else if ("Agroal".equals(poolType)) { + appendAgroalText(sb, ds); + } + sb.append("\n"); + } + + return sb.toString(); + } + + @Override + protected JsonObject doCallJson(Map<String, Object> options) { + JsonObject root = new JsonObject(); + JsonArray arr = new JsonArray(); + + Map<String, DataSource> dataSources + = getCamelContext().getRegistry().findByTypeWithName(DataSource.class); + + for (Map.Entry<String, DataSource> entry : dataSources.entrySet()) { + DataSource ds = entry.getValue(); + String poolType = detectPoolType(ds); + + JsonObject jo = new JsonObject(); + jo.put("name", entry.getKey()); + jo.put("type", ds.getClass().getName()); + jo.put("poolType", poolType); + + if ("HikariCP".equals(poolType)) { + collectHikariMetrics(jo, ds); + } else if ("Agroal".equals(poolType)) { + collectAgroalMetrics(jo, ds); + } + + arr.add(jo); + } + + root.put("dataSources", arr); + return root; + } + + private static String detectPoolType(DataSource ds) { + String className = ds.getClass().getName(); + if (HIKARI_CLASS.equals(className)) { + return "HikariCP"; + } else if (AGROAL_CLASS.equals(className)) { + return "Agroal"; + } + return "Unknown"; + } + + // ---- HikariCP ---- + + private void collectHikariMetrics(JsonObject jo, DataSource ds) { + Object poolName = invokeMethod(ds, "getPoolName"); + if (poolName != null) { + jo.put("poolName", String.valueOf(poolName)); + } + Object maxPoolSize = invokeMethod(ds, "getMaximumPoolSize"); + if (maxPoolSize instanceof Number n) { + jo.put("maxPoolSize", n.intValue()); + } + + Object mxBean = invokeMethod(ds, "getHikariPoolMXBean"); + if (mxBean != null) { + putInt(jo, "active", invokeMethod(mxBean, "getActiveConnections")); + putInt(jo, "idle", invokeMethod(mxBean, "getIdleConnections")); + putInt(jo, "total", invokeMethod(mxBean, "getTotalConnections")); + putInt(jo, "waiting", invokeMethod(mxBean, "getThreadsAwaitingConnection")); + } + } + + private void appendHikariText(StringBuilder sb, DataSource ds) { + Object poolName = invokeMethod(ds, "getPoolName"); + if (poolName != null) { + sb.append(String.format(" Pool Name: %s%n", poolName)); + } + Object maxPoolSize = invokeMethod(ds, "getMaximumPoolSize"); + if (maxPoolSize != null) { + sb.append(String.format(" Max Pool Size: %s%n", maxPoolSize)); + } + + Object mxBean = invokeMethod(ds, "getHikariPoolMXBean"); + if (mxBean != null) { + sb.append(String.format(" Active: %s%n", invokeMethod(mxBean, "getActiveConnections"))); + sb.append(String.format(" Idle: %s%n", invokeMethod(mxBean, "getIdleConnections"))); + sb.append(String.format(" Total: %s%n", invokeMethod(mxBean, "getTotalConnections"))); + sb.append(String.format(" Waiting: %s%n", invokeMethod(mxBean, "getThreadsAwaitingConnection"))); + } else { + sb.append(" Pool not yet initialized\n"); + } + } + + // ---- Agroal ---- + + private void collectAgroalMetrics(JsonObject jo, DataSource ds) { + Object metrics = invokeMethod(ds, "getMetrics"); + if (metrics != null) { + putLong(jo, "active", invokeMethod(metrics, "activeCount")); + putLong(jo, "idle", invokeMethod(metrics, "availableCount")); + putLong(jo, "maxUsed", invokeMethod(metrics, "maxUsedCount")); + putLong(jo, "leakDetection", invokeMethod(metrics, "leakDetectionCount")); + putLong(jo, "created", invokeMethod(metrics, "creationCount")); + } + + // max pool size via configuration chain + Object config = invokeMethod(ds, "getConfiguration"); + if (config != null) { + Object poolConfig = invokeMethod(config, "connectionPoolConfiguration"); + if (poolConfig != null) { + Object maxSize = invokeMethod(poolConfig, "maxSize"); + if (maxSize instanceof Number n) { + jo.put("maxPoolSize", n.intValue()); + } + } + } + + // compute total from active + idle + Object active = jo.get("active"); + Object idle = jo.get("idle"); + if (active instanceof Number a && idle instanceof Number i) { + jo.put("total", a.longValue() + i.longValue()); + } + } + + private void appendAgroalText(StringBuilder sb, DataSource ds) { + Object metrics = invokeMethod(ds, "getMetrics"); + if (metrics != null) { + sb.append(String.format(" Active: %s%n", invokeMethod(metrics, "activeCount"))); + sb.append(String.format(" Available: %s%n", invokeMethod(metrics, "availableCount"))); + sb.append(String.format(" Max Used: %s%n", invokeMethod(metrics, "maxUsedCount"))); + sb.append(String.format(" Leak Detection: %s%n", invokeMethod(metrics, "leakDetectionCount"))); + } + + Object config = invokeMethod(ds, "getConfiguration"); + if (config != null) { + Object poolConfig = invokeMethod(config, "connectionPoolConfiguration"); + if (poolConfig != null) { + sb.append(String.format(" Max Pool Size: %s%n", invokeMethod(poolConfig, "maxSize"))); + } + } + } + + // ---- Reflection helpers ---- + + private static Object invokeMethod(Object target, String methodName) { + try { + Method m = target.getClass().getMethod(methodName); + return m.invoke(target); + } catch (Exception e) { + return null; + } + } + + private static void putInt(JsonObject jo, String key, Object value) { + if (value instanceof Number n) { + jo.put(key, n.intValue()); + } + } + + private static void putLong(JsonObject jo, String key, Object value) { + if (value instanceof Number n) { + jo.put(key, n.longValue()); + } + } +} diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java index 98a2f5b5366d..21688dde8ca6 100644 --- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java +++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java @@ -1443,6 +1443,13 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C root.put("errors", summary); } } + DevConsole dc27 = dcr.resolveById("datasource"); + if (dc27 != null) { + JsonObject json = (JsonObject) dc27.call(DevConsole.MediaType.JSON); + if (json != null && !json.isEmpty()) { + root.put("dataSources", json); + } + } } // various details JsonObject mem = collectMemory(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index adb40965dca7..1ea44c3e486d 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -224,6 +224,7 @@ public class CamelMonitor extends CamelCommand { private SpansTab spansTab; private ProcessTab processTab; private OverviewTab overviewTab; + private DataSourceTab dataSourceTab; // "Switch integration" popup state private boolean showSwitchPopup; @@ -277,6 +278,7 @@ public class CamelMonitor extends CamelCommand { diagramTab = new DiagramTab(ctx); routesTab = new RoutesTab(ctx); consumersTab = new ConsumersTab(ctx); + dataSourceTab = new DataSourceTab(ctx); endpointsTab = new EndpointsTab(ctx, metrics); httpTab = new HttpTab(ctx); healthTab = new HealthTab(ctx); @@ -436,7 +438,7 @@ public class CamelMonitor extends CamelCommand { return true; } if (ke.isDown()) { - morePopupState.selectNext(13); + morePopupState.selectNext(14); return true; } int shortcutSel = morePopupShortcut(ke); @@ -455,13 +457,14 @@ public class CamelMonitor extends CamelCommand { case 3 -> classpathTab; case 4 -> configurationTab; case 5 -> consumersTab; - case 6 -> inflightTab; - case 7 -> memoryTab; - case 8 -> metricsTab; - case 9 -> spansTab; - case 10 -> processTab; - case 11 -> startupTab; - case 12 -> threadsTab; + case 6 -> dataSourceTab; + case 7 -> inflightTab; + case 8 -> memoryTab; + case 9 -> metricsTab; + case 10 -> spansTab; + case 11 -> processTab; + case 12 -> startupTab; + case 13 -> threadsTab; default -> null; }; if (activeMoreTab != null) { @@ -932,6 +935,7 @@ public class CamelMonitor extends CamelCommand { startupTab.onIntegrationChanged(); configurationTab.onIntegrationChanged(); consumersTab.onIntegrationChanged(); + dataSourceTab.onIntegrationChanged(); circuitBreakerTab.onIntegrationChanged(); inflightTab.onIntegrationChanged(); spansTab.onIntegrationChanged(); @@ -1234,7 +1238,7 @@ public class CamelMonitor extends CamelCommand { private void renderMorePopup(Frame frame, Rect area) { int popupW = 22; - int popupH = 15; + int popupH = 16; // Position just below the "0 More▾" tab label int dividerW = CharWidth.of(" | "); int tabBarX = 0; @@ -1262,6 +1266,7 @@ public class CamelMonitor extends CamelCommand { ListItem.from(Line.from(Span.raw(" Cl"), Span.styled("a", keyStyle), Span.raw("sspath"))), ListItem.from(Line.from(Span.raw(" Confi"), Span.styled("g", keyStyle), Span.raw("uration"))), ListItem.from(Line.from(Span.raw(" Co"), Span.styled("n", keyStyle), Span.raw("sumers"))), + ListItem.from(Line.from(Span.raw(" "), Span.styled("D", keyStyle), Span.raw("ataSource"))), ListItem.from(Line.from(Span.raw(" "), Span.styled("I", keyStyle), Span.raw("nflight"))), ListItem.from(Line.from(Span.raw(" "), Span.styled("M", keyStyle), Span.raw("emory"))), ListItem.from(Line.from(Span.raw(" M"), Span.styled("e", keyStyle), Span.raw("trics"))), @@ -1364,27 +1369,30 @@ public class CamelMonitor extends CamelCommand { if (ke.isChar('n')) { return 5; } - if (ke.isChar('i')) { + if (ke.isChar('d')) { return 6; } - if (ke.isChar('m')) { + if (ke.isChar('i')) { return 7; } - if (ke.isChar('e')) { + if (ke.isChar('m')) { return 8; } - if (ke.isChar('o')) { + if (ke.isChar('e')) { return 9; } - if (ke.isChar('p')) { + if (ke.isChar('o')) { return 10; } - if (ke.isChar('s')) { + if (ke.isChar('p')) { return 11; } - if (ke.isChar('t')) { + if (ke.isChar('s')) { return 12; } + if (ke.isChar('t')) { + return 13; + } return -1; } @@ -2464,7 +2472,7 @@ public class CamelMonitor extends CamelCommand { private static final String[] MORE_TAB_NAMES = { "Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration", - "Consumers", "Inflight", "Memory", "Metrics", "Spans", "Process", "Startup", "Threads" + "Consumers", "DataSource", "Inflight", "Memory", "Metrics", "Spans", "Process", "Startup", "Threads" }; String navigateToTab(String tabName) { @@ -2486,13 +2494,14 @@ public class CamelMonitor extends CamelCommand { case 3 -> classpathTab; case 4 -> configurationTab; case 5 -> consumersTab; - case 6 -> inflightTab; - case 7 -> memoryTab; - case 8 -> metricsTab; - case 9 -> spansTab; - case 10 -> processTab; - case 11 -> startupTab; - case 12 -> threadsTab; + case 6 -> dataSourceTab; + case 7 -> inflightTab; + case 8 -> memoryTab; + case 9 -> metricsTab; + case 10 -> spansTab; + case 11 -> processTab; + case 12 -> startupTab; + case 13 -> threadsTab; default -> null; }; if (activeMoreTab != null) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceInfo.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceInfo.java new file mode 100644 index 000000000000..83cbfd3be650 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceInfo.java @@ -0,0 +1,32 @@ +/* + * 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.dsl.jbang.core.commands.tui; + +class DataSourceInfo { + String name; + String type; + String poolType; + String poolName; + int active; + int idle; + int total; + int waiting; + int maxPoolSize; + int maxUsed; + int leakDetection; + int created; +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java new file mode 100644 index 000000000000..79db403af498 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java @@ -0,0 +1,283 @@ +/* + * 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.dsl.jbang.core.commands.tui; + +import java.util.ArrayList; +import java.util.List; + +import dev.tamboui.layout.Constraint; +import dev.tamboui.layout.Rect; +import dev.tamboui.style.Color; +import dev.tamboui.style.Style; +import dev.tamboui.terminal.Frame; +import dev.tamboui.text.Span; +import dev.tamboui.tui.event.KeyEvent; +import dev.tamboui.widgets.block.Block; +import dev.tamboui.widgets.block.BorderType; +import dev.tamboui.widgets.table.Cell; +import dev.tamboui.widgets.table.Row; +import dev.tamboui.widgets.table.Table; +import dev.tamboui.widgets.table.TableState; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; + +import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*; + +class DataSourceTab implements MonitorTab { + + private static final String[] SORT_COLUMNS = { "name", "pool", "active", "idle", "total", "max", "waiting" }; + + private final MonitorContext ctx; + private final TableState tableState = new TableState(); + private String sort = "name"; + private int sortIndex; + private boolean sortReversed; + + DataSourceTab(MonitorContext ctx) { + this.ctx = ctx; + } + + @Override + public boolean handleKeyEvent(KeyEvent ke) { + if (ke.isChar('s')) { + sortIndex = (sortIndex + 1) % SORT_COLUMNS.length; + sort = SORT_COLUMNS[sortIndex]; + sortReversed = false; + return true; + } + if (ke.isChar('S')) { + sortReversed = !sortReversed; + return true; + } + return false; + } + + @Override + public boolean handleEscape() { + return false; + } + + @Override + public void navigateUp() { + } + + @Override + public void navigateDown() { + } + + @Override + public void render(Frame frame, Rect area) { + IntegrationInfo info = ctx.findSelectedIntegration(); + if (info == null) { + renderNoSelection(frame, area); + return; + } + + List<DataSourceInfo> sorted = new ArrayList<>(info.dataSources); + sorted.sort(this::sortDataSource); + + List<Row> rows = new ArrayList<>(); + for (DataSourceInfo di : sorted) { + boolean exhausted = di.maxPoolSize > 0 && di.active >= di.maxPoolSize; + boolean waiters = di.waiting > 0; + + Style activeStyle = exhausted + ? Style.EMPTY.fg(Color.LIGHT_RED) + : Style.EMPTY.fg(Color.CYAN); + Style waitingStyle = waiters + ? Style.EMPTY.fg(Color.YELLOW) + : Style.EMPTY; + + String poolLabel = di.poolName != null ? di.poolName : (di.poolType != null ? di.poolType : ""); + + rows.add(Row.from( + Cell.from(Span.styled(di.name != null ? di.name : "", Style.EMPTY.fg(Color.CYAN))), + Cell.from(poolLabel), + rightCell(String.valueOf(di.active), 8, activeStyle), + rightCell(String.valueOf(di.idle), 8), + rightCell(String.valueOf(di.total), 8), + rightCell(di.maxPoolSize > 0 ? String.valueOf(di.maxPoolSize) : "", 8), + rightCell(String.valueOf(di.waiting), 8, waitingStyle), + Cell.from(Span.styled(shortType(di.type), Style.EMPTY.dim())))); + } + + if (rows.isEmpty()) { + rows.add(Row.from( + Cell.from(Span.styled("No DataSources", Style.EMPTY.dim())), + Cell.from(""), Cell.from(""), Cell.from(""), + Cell.from(""), Cell.from(""), Cell.from(""), Cell.from(""))); + } + + Table table = Table.builder() + .rows(rows) + .header(Row.from( + Cell.from(Span.styled(sortLabel("NAME", "name"), sortStyle("name"))), + Cell.from(Span.styled(sortLabel("POOL", "pool"), sortStyle("pool"))), + rightCell(sortLabel("ACTIVE", "active"), 8, sortStyle("active")), + rightCell(sortLabel("IDLE", "idle"), 8, sortStyle("idle")), + rightCell(sortLabel("TOTAL", "total"), 8, sortStyle("total")), + rightCell(sortLabel("MAX", "max"), 8, sortStyle("max")), + rightCell(sortLabel("WAITING", "waiting"), 8, sortStyle("waiting")), + Cell.from(Span.styled("TYPE", Style.EMPTY.bold())))) + .widths( + Constraint.length(22), + Constraint.length(16), + Constraint.length(8), + Constraint.length(8), + Constraint.length(8), + Constraint.length(8), + Constraint.length(8), + Constraint.fill()) + .block(Block.builder().borderType(BorderType.ROUNDED) + .title(" DataSource sort:" + sort + " ").build()) + .build(); + + frame.renderStatefulWidget(table, area, tableState); + } + + @Override + public void renderFooter(List<Span> spans) { + hint(spans, "Esc", "back"); + hint(spans, "s", "sort"); + } + + private String sortLabel(String label, String column) { + return MonitorContext.sortLabel(label, column, sort, sortReversed); + } + + private Style sortStyle(String column) { + return MonitorContext.sortStyle(column, sort); + } + + private int sortDataSource(DataSourceInfo a, DataSourceInfo b) { + int result = switch (sort) { + case "pool" -> { + String pa = a.poolType != null ? a.poolType : ""; + String pb = b.poolType != null ? b.poolType : ""; + yield pa.compareToIgnoreCase(pb); + } + case "active" -> Integer.compare(b.active, a.active); + case "idle" -> Integer.compare(b.idle, a.idle); + case "total" -> Integer.compare(b.total, a.total); + case "max" -> Integer.compare(b.maxPoolSize, a.maxPoolSize); + case "waiting" -> Integer.compare(b.waiting, a.waiting); + default -> { // "name" + String na = a.name != null ? a.name : ""; + String nb = b.name != null ? b.name : ""; + yield na.compareToIgnoreCase(nb); + } + }; + return sortReversed ? -result : result; + } + + private static String shortType(String type) { + if (type == null) { + return ""; + } + int dot = type.lastIndexOf('.'); + return dot >= 0 ? type.substring(dot + 1) : type; + } + + @Override + public SelectionContext getSelectionContext() { + IntegrationInfo info = ctx.findSelectedIntegration(); + if (info == null || info.dataSources.isEmpty()) { + return null; + } + List<DataSourceInfo> sorted = new ArrayList<>(info.dataSources); + sorted.sort(this::sortDataSource); + List<String> items = sorted.stream().map(d -> d.name != null ? d.name : "").toList(); + Integer sel = tableState.selected(); + return new SelectionContext("table", items, sel != null ? sel : -1, items.size(), "DataSource"); + } + + @Override + public String getHelpText() { + return """ + # DataSource + + Displays connection pool metrics for all `javax.sql.DataSource` beans + registered in the Camel registry. Supports **HikariCP** (Spring Boot default) + and **Agroal** (Quarkus default) connection pools. + + ## Table Columns + + - **NAME** — The bean name of the DataSource in the registry + - **POOL** — The pool name (HikariCP pool name) or pool type (HikariCP / Agroal / Unknown) + - **ACTIVE** — Number of connections currently in use. Shown in red when equal to MAX (pool exhausted) + - **IDLE** — Number of idle connections available in the pool + - **TOTAL** — Total connections (active + idle) currently managed by the pool + - **MAX** — Maximum pool size configured. When ACTIVE reaches MAX, new connection requests will wait + - **WAITING** — Number of threads waiting for a connection. Shown in yellow when > 0 + - **TYPE** — The Java class of the DataSource implementation + + ## Pool Exhaustion + + When **ACTIVE = MAX**, the pool is exhausted — no idle connections remain. + New requests must wait for a connection to be released. The ACTIVE column + turns red to signal this condition. If WAITING also increases, the application + may experience timeouts. + + ## Connection Pools + + ### HikariCP + The default connection pool for Spring Boot. Known for its speed and small + footprint. Default max pool size is 10. + + ### Agroal + The default connection pool for Quarkus. Provides leak detection and + connection validation. Shows additional metrics like max-used and leak count. + + ## Keys + + - `Up/Down` — select datasource + - `s` — cycle sort column + - `S` — reverse sort order + """; + } + + @Override + public JsonObject getTableDataAsJson() { + IntegrationInfo info = ctx.findSelectedIntegration(); + if (info == null) { + return null; + } + JsonObject result = new JsonObject(); + result.put("tab", "DataSource"); + JsonArray rows = new JsonArray(); + for (DataSourceInfo di : info.dataSources) { + JsonObject row = new JsonObject(); + row.put("name", di.name); + row.put("type", di.type); + row.put("poolType", di.poolType); + if (di.poolName != null) { + row.put("poolName", di.poolName); + } + row.put("active", di.active); + row.put("idle", di.idle); + row.put("total", di.total); + row.put("maxPoolSize", di.maxPoolSize); + row.put("waiting", di.waiting); + rows.add(row); + } + result.put("rows", rows); + result.put("totalRows", info.dataSources.size()); + Integer sel = tableState.selected(); + result.put("selectedIndex", sel != null ? sel : -1); + return result; + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java index e51122f1e4c2..a4009ab251c4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java @@ -82,6 +82,7 @@ class IntegrationInfo { final List<MicrometerMeterInfo> meters = new ArrayList<>(); final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>(); final List<ConfigurationTab.ConfigProperty> configProperties = new ArrayList<>(); + final List<DataSourceInfo> dataSources = new ArrayList<>(); String httpServer; String readmeFiles; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java index 930a966f75ae..515c080554fb 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java @@ -497,6 +497,31 @@ final class StatusParser { } } + // Parse dataSources + JsonObject dsObj = (JsonObject) root.get("dataSources"); + if (dsObj != null) { + JsonArray dsList = (JsonArray) dsObj.get("dataSources"); + if (dsList != null) { + for (Object d : dsList) { + JsonObject dj = (JsonObject) d; + DataSourceInfo di = new DataSourceInfo(); + di.name = dj.getString("name"); + di.type = dj.getString("type"); + di.poolType = dj.getString("poolType"); + di.poolName = dj.getString("poolName"); + di.active = dj.getIntegerOrDefault("active", 0); + di.idle = dj.getIntegerOrDefault("idle", 0); + di.total = dj.getIntegerOrDefault("total", 0); + di.waiting = dj.getIntegerOrDefault("waiting", 0); + di.maxPoolSize = dj.getIntegerOrDefault("maxPoolSize", 0); + di.maxUsed = dj.getIntegerOrDefault("maxUsed", 0); + di.leakDetection = dj.getIntegerOrDefault("leakDetection", 0); + di.created = dj.getIntegerOrDefault("created", 0); + info.dataSources.add(di); + } + } + } + return info; }
