This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 0f4679e666a2 CAMEL-23837: Add DataSource connection pool dev-console
and TUI tab
0f4679e666a2 is described below
commit 0f4679e666a209d15c399377430c7637eb9b95ca
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jun 28 08:49:59 2026 +0200
CAMEL-23837: Add DataSource connection pool dev-console and TUI tab
Adds a datasource dev-console that discovers javax.sql.DataSource beans
from the Camel registry and extracts pool metrics via reflection for
HikariCP and Agroal. Includes a TUI DataSource tab with pool exhaustion
warnings, a `camel get datasource` CLI command, dynamic infra panel
sizing in the overview tab, and synced jbang examples catalog.
Closes #24278
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../apache/camel/catalog/dev-consoles.properties | 1 +
.../camel/catalog/dev-consoles/datasource.json | 15 ++
.../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 ++++++++++++++++
.../jbang-commands/camel-jbang-get-datasource.adoc | 28 ++
.../ROOT/pages/jbang-commands/camel-jbang-get.adoc | 1 +
.../camel/cli/connector/LocalCliConnector.java | 7 +
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../dsl/jbang/core/commands/CamelJBangMain.java | 1 +
.../core/commands/process/ListDataSource.java | 190 ++++++++++++++
.../examples/camel-jbang-example-catalog.json | 4 -
.../src/main/resources/examples/sql/sql.camel.yaml | 3 +-
.../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/OverviewTab.java | 18 +-
.../dsl/jbang/core/commands/tui/StatusParser.java | 25 ++
22 files changed, 943 insertions(+), 34 deletions(-)
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 b993922f1751..536dbe0e6b98 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
@@ -8,6 +8,7 @@ browse
circuit-breaker
consumer
context
+datasource
debug
endpoint
errors
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/datasource.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/datasource.json
new file mode 100644
index 000000000000..8889232ffa1a
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/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/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/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-datasource.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-datasource.adoc
new file mode 100644
index 000000000000..aed7954ba37a
--- /dev/null
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-datasource.adoc
@@ -0,0 +1,28 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel get datasource
+
+Get status of DataSource connection pools
+
+
+== Usage
+
+[source,bash]
+----
+camel get datasource [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--json` | Output in JSON Format | | boolean
+| `--sort` | Sort by pid, name or age | pid | String
+| `--watch` | Execute periodically and showing output fullscreen | | boolean
+| `-h,--help` | Display the help and sub-commands | | boolean
+|===
+
+
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc
index 3384abd60ecb..8d2eb511b28e 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc
@@ -24,6 +24,7 @@ camel get [options]
| xref:jbang-commands/camel-jbang-get-consumer.adoc[consumer] | Get status of
Camel consumers
| xref:jbang-commands/camel-jbang-get-context.adoc[context] | Get status of
Camel integrations
| xref:jbang-commands/camel-jbang-get-count.adoc[count] | Get total and failed
exchanges
+| xref:jbang-commands/camel-jbang-get-datasource.adoc[datasource] | Get status
of DataSource connection pools
| xref:jbang-commands/camel-jbang-get-endpoint.adoc[endpoint] | Get usage of
Camel endpoints
| xref:jbang-commands/camel-jbang-get-error.adoc[error] | Get captured routing
errors of Camel integrations
| xref:jbang-commands/camel-jbang-get-event.adoc[event] | Get latest events of
Camel integrations
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-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 791f8dd75b40..19c7b6505642 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -15,7 +15,7 @@
{ "name": "eval", "fullName": "eval", "description": "Evaluate Camel
expressions and scripts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression",
"fullName": "eval expression", "description": "Evaluates Camel expression",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
{ "name": "explain", "fullName": "explain", "description": "Explain what a
Camel route does using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType":
"java.lang.String", "type": "string" }, { "names": "--api-type", "description":
"API type: 'ollama', 'openai' (OpenAI-compatible), or 'anthropic' (A [...]
{ "name": "export", "fullName": "export", "description": "Export to other
runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names":
"--build-property", "description": "Maven build properties, ex.
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, {
"names": "--camel-spring-boot-version", "description": "Camel version to use
with Spring Boot", "javaType": "java.lang.String", "ty [...]
- { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean",
"fullName": "get [...]
+ { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean",
"fullName": "get [...]
{ "name": "harden", "fullName": "harden", "description": "Suggest security
hardening for Camel routes using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String",
"type": "string" }, { "names": "--api-type", "description": "API type: 'ollama'
or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", [...]
{ "name": "hawtio", "fullName": "hawtio", "description": "Launch Hawtio
web console", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.Hawtio", "options": [ {
"names": "--host", "description": "Hostname to bind the Hawtio web console to",
"defaultValue": "127.0.0.1", "javaType": "java.lang.String", "type": "string"
}, { "names": "--openUrl", "description": "To automatic open Hawtio web console
in the web browser", "defaultValue": "true", "javaType": "boolean", "type":
[...]
{ "name": "infra", "fullName": "infra", "description": "List and Run
external services for testing and prototyping", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ {
"names": "--json", "description": "Output in JSON Format", "javaType":
"boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display
the help and sub-commands", "javaType": "boolean", "type": "boolean" } ],
"subcommands": [ { "name": "get", "fullName": "infra [...]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 1b3f9de7dc29..5113540c571f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -158,6 +158,7 @@ public class CamelJBangMain implements Callable<Integer> {
.addSubcommand("consumer", new CommandLine(new
ListConsumer(this)))
.addSubcommand("context", new CommandLine(new
CamelContextStatus(this)))
.addSubcommand("count", new CommandLine(new
CamelCount(this)))
+ .addSubcommand("datasource", new CommandLine(new
ListDataSource(this)))
.addSubcommand("endpoint", new CommandLine(new
ListEndpoint(this)))
.addSubcommand("error", new CommandLine(new
ListError(this)))
.addSubcommand("event", new CommandLine(new
ListEvent(this)))
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListDataSource.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListDataSource.java
new file mode 100644
index 000000000000..0e78ed4f6cf4
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListDataSource.java
@@ -0,0 +1,190 @@
+/*
+ * 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.process;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import com.github.freva.asciitable.OverflowBehaviour;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
+import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "datasource", description = "Get status of DataSource
connection pools", sortOptions = false,
+ showDefaultValues = true,
+ footer = {
+ "%nExamples:",
+ " camel get datasource",
+ " camel get datasource --watch" })
+public class ListDataSource extends ProcessWatchCommand {
+
+ @CommandLine.Parameters(description = "Name or pid of running Camel
integration", arity = "0..1")
+ String name = "*";
+
+ @CommandLine.Option(names = { "--sort" }, completionCandidates =
PidNameAgeCompletionCandidates.class,
+ description = "Sort by pid, name or age", defaultValue
= "pid")
+ String sort;
+
+ public ListDataSource(CamelJBangMain main) {
+ super(main);
+ }
+
+ @Override
+ public Integer doProcessWatchCall() throws Exception {
+ List<Row> rows = new ArrayList<>();
+
+ List<Long> pids = findPids(name);
+ ProcessHandle.allProcesses()
+ .filter(ph -> pids.contains(ph.pid()))
+ .forEach(ph -> {
+ JsonObject root = loadStatus(ph.pid());
+ if (root != null) {
+ JsonObject context = (JsonObject) root.get("context");
+ JsonObject ds = (JsonObject) root.get("dataSources");
+ if (context != null && ds != null) {
+ JsonArray array = (JsonArray)
ds.get("dataSources");
+ if (array != null) {
+ for (int i = 0; i < array.size(); i++) {
+ JsonObject o = (JsonObject) array.get(i);
+ Row row = new Row();
+ row.name = context.getString("name");
+ if ("CamelJBang".equals(row.name)) {
+ row.name =
ProcessHelper.extractName(root, ph);
+ }
+ row.pid = Long.toString(ph.pid());
+ row.uptime = extractSince(ph);
+ row.age = TimeUtils.printSince(row.uptime);
+ row.dsName = o.getString("name");
+ row.type = o.getString("type");
+ row.poolType = o.getString("poolType");
+ row.poolName = o.getString("poolName");
+ row.active =
o.getIntegerOrDefault("active", 0);
+ row.idle = o.getIntegerOrDefault("idle",
0);
+ row.total = o.getIntegerOrDefault("total",
0);
+ row.maxPoolSize =
o.getIntegerOrDefault("maxPoolSize", 0);
+ row.waiting =
o.getIntegerOrDefault("waiting", 0);
+ rows.add(row);
+ }
+ }
+ }
+ }
+ });
+
+ // sort rows
+ rows.sort(this::sortRow);
+
+ if (!rows.isEmpty()) {
+ printTable(rows);
+ }
+
+ return 0;
+ }
+
+ protected void printTable(List<Row> rows) {
+ if (jsonOutput) {
+ printer().println(Jsoner.serialize(rows.stream().map(r -> {
+ JsonObject jo = new JsonObject();
+ jo.put("pid", r.pid);
+ jo.put("name", r.name);
+ jo.put("age", r.age);
+ jo.put("dsName", r.dsName);
+ jo.put("poolType", r.poolType);
+ if (r.poolName != null) {
+ jo.put("poolName", r.poolName);
+ }
+ jo.put("active", r.active);
+ jo.put("idle", r.idle);
+ jo.put("total", r.total);
+ jo.put("maxPoolSize", r.maxPoolSize);
+ jo.put("waiting", r.waiting);
+ jo.put("type", r.type);
+ return jo;
+ }).collect(Collectors.toList())));
+ return;
+ }
+ int tw = terminalWidth();
+ int typeW = Math.max(20, Math.min(60, tw - 100));
+ printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows,
Arrays.asList(
+ new
Column().header("PID").headerAlign(HorizontalAlign.CENTER).with(r -> r.pid),
+ new
Column().header("NAME").dataAlign(HorizontalAlign.LEFT).maxWidth(30,
OverflowBehaviour.ELLIPSIS_RIGHT)
+ .with(r -> r.name),
+ new
Column().header("AGE").headerAlign(HorizontalAlign.CENTER).with(r -> r.age),
+ new
Column().header("DS-NAME").dataAlign(HorizontalAlign.LEFT).maxWidth(25,
OverflowBehaviour.ELLIPSIS_RIGHT)
+ .with(r -> r.dsName),
+ new Column().header("POOL").dataAlign(HorizontalAlign.LEFT)
+ .with(r -> r.poolName != null ? r.poolName :
(r.poolType != null ? r.poolType : "")),
+ new
Column().header("ACTIVE").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> Integer.toString(r.active)),
+ new
Column().header("IDLE").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> Integer.toString(r.idle)),
+ new
Column().header("TOTAL").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> Integer.toString(r.total)),
+ new
Column().header("MAX").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> r.maxPoolSize > 0 ?
Integer.toString(r.maxPoolSize) : ""),
+ new
Column().header("WAITING").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> Integer.toString(r.waiting)),
+ new Column().header("TYPE").dataAlign(HorizontalAlign.LEFT)
+ .maxWidth(typeW, OverflowBehaviour.ELLIPSIS_RIGHT)
+ .with(r -> r.type))));
+ }
+
+ protected int sortRow(Row o1, Row o2) {
+ String s = sort;
+ int negate = 1;
+ if (s.startsWith("-")) {
+ s = s.substring(1);
+ negate = -1;
+ }
+ switch (s) {
+ case "pid":
+ return Long.compare(Long.parseLong(o1.pid),
Long.parseLong(o2.pid)) * negate;
+ case "name":
+ return o1.name.compareToIgnoreCase(o2.name) * negate;
+ case "age":
+ return Long.compare(o1.uptime, o2.uptime) * negate;
+ default:
+ return 0;
+ }
+ }
+
+ static class Row {
+ String pid;
+ String name;
+ long uptime;
+ String age;
+ String dsName;
+ String type;
+ String poolType;
+ String poolName;
+ int active;
+ int idle;
+ int total;
+ int maxPoolSize;
+ int waiting;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
index 91941c4cdfa8..33129c74ff89 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
@@ -144,7 +144,6 @@
"files": [
"README.md",
"application.properties",
- "compose.yaml",
"docling-langchain4j-rag.yaml",
"sample.md"
],
@@ -197,7 +196,6 @@
"files": [
"README.md",
"application.properties",
- "compose.yaml",
"ftp.camel.yaml",
"jbang.properties"
],
@@ -305,7 +303,6 @@
"files": [
"README.md",
"application.properties",
- "compose.yaml",
"infra/mosquitto.conf",
"mqtt.camel.yaml",
"start.sh"
@@ -460,7 +457,6 @@
"analyzer/error-analyzer.camel.yaml",
"containers/caches/infinispan-events-config.json",
"containers/caches/infinispan-events-to-process-config.json",
- "containers/docker-compose.yaml",
"containers/otel-collector-config.yaml",
"correlator/application-dev.properties",
"correlator/correlated-log-schema.json",
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml
index e7a18a2ab2bf..3153d25c1917 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml
@@ -29,8 +29,7 @@
uri: timer
parameters:
timerName: select
- repeatCount: 1
- delay: 2000
+ delay: 5000
steps:
- to:
uri: sql
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 e9113704af78..f9985158d512 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) {
@@ -938,6 +941,7 @@ public class CamelMonitor extends CamelCommand {
startupTab.onIntegrationChanged();
configurationTab.onIntegrationChanged();
consumersTab.onIntegrationChanged();
+ dataSourceTab.onIntegrationChanged();
circuitBreakerTab.onIntegrationChanged();
inflightTab.onIntegrationChanged();
spansTab.onIntegrationChanged();
@@ -1240,7 +1244,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;
@@ -1268,6 +1272,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"))),
@@ -1370,27 +1375,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;
}
@@ -2470,7 +2478,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) {
@@ -2492,13 +2500,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/OverviewTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index 805458e3ad30..70e454488d9c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -160,13 +160,15 @@ class OverviewTab implements MonitorTab {
boolean hasSparkline = chartMode != CHART_OFF &&
!throughputHistory.isEmpty() && !ctx.isInfraSelected()
&& ctx.shellPercent < 50;
- boolean showInfoPanel = ctx.isInfraSelected() &&
ctx.findSelectedInfra() != null && !hasSparkline;
+ InfraInfo infraSel = ctx.isInfraSelected() ? ctx.findSelectedInfra() :
null;
+ boolean showInfoPanel = infraSel != null && !hasSparkline;
List<Constraint> constraints = new ArrayList<>();
constraints.add(Constraint.fill());
if (hasSparkline) {
constraints.add(Constraint.length(14));
} else if (showInfoPanel) {
- constraints.add(Constraint.length(10));
+ int panelH = countInfraLines(infraSel) + 2;
+ constraints.add(Constraint.length(Math.min(panelH, area.height() /
2)));
}
List<Rect> chunks = Layout.vertical()
.constraints(constraints)
@@ -575,6 +577,18 @@ class OverviewTab implements MonitorTab {
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(),
inner);
}
+ private static int countInfraLines(InfraInfo infra) {
+ int count = 2; // "Service: ..." + blank line
+ for (Map.Entry<String, Object> e : infra.properties.entrySet()) {
+ if (e.getValue() instanceof Map<?, ?> map) {
+ count += 1 + map.size();
+ } else {
+ count++;
+ }
+ }
+ return count;
+ }
+
private void renderInfraInfoPanel(Frame frame, Rect area, InfraInfo infra)
{
Block infoBlock =
Block.builder().borderType(BorderType.ROUNDED).build();
frame.renderWidget(infoBlock, area);
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;
}