diff --git a/activemq-prometheus/README.md b/activemq-prometheus/README.md
new file mode 100644
index 00000000000..05511b88b36
--- /dev/null
+++ b/activemq-prometheus/README.md
@@ -0,0 +1,92 @@
+
+
+# ActiveMQ Prometheus Metrics
+
+## Activation
+
+1. Uncomment the `Prometheus Metrics Web Application` block from `conf/jetty/jetty-webapps.xml`.
+2. Restart the broker
+
+The endpoint uses the existing Jetty management listener, TLS configuration,
+IP allowlist, and JAAS realm. Its path is restricted to the `admins` role
+but can be changed in `conf/jetty/jetty-security.xml`.
+
+## Endpoints
+
+Two endpoints because brokers with many destinations might produce large responses:
+- `GET /metrics`: broker-level metrics only.
+- `GET /metrics?per_object=true`: per-queue, per-topic, and broker-level metrics
+
+## Metrics
+
+### Broker metrics (`activemq_broker_*`)
+
+| Metric | Type | Description |
+|--------|------|-------------|
+| `connections_count` | gauge | Current number of connections |
+| `connections_total` | counter | Total connections since last start |
+| `messages_enqueued_total` | counter | Total messages enqueued since last start |
+| `messages_dequeued_total` | counter | Total messages dequeued since last start |
+| `consumers_count` | gauge | Current number of consumers |
+| `producers_count` | gauge | Current number of producers |
+| `message_count` | gauge | Current number of messages across all destinations |
+| `memory_percent_usage` | gauge | Percent of memory limit used |
+| `memory_limit_bytes` | gauge | Memory limit in bytes |
+| `store_percent_usage` | gauge | Percent of store limit used |
+| `store_limit_bytes` | gauge | Store limit in bytes |
+| `temp_percent_usage` | gauge | Percent of temp limit used |
+| `temp_limit_bytes` | gauge | Temp limit in bytes |
+| `uptime_milliseconds` | gauge | Broker uptime in milliseconds |
+
+### Destination metrics (`activemq_queue_*` / `activemq_topic_*`)
+
+Returned only when `?per_object=true` is set.
+
+| Metric | Type | Description |
+|--------|------|-------------|
+| `message_count` | gauge | Number of messages in destination |
+| `enqueue_count_total` | counter | Total messages enqueued since last start |
+| `dequeue_count_total` | counter | Total messages dequeued since last start |
+| `dispatch_count_total` | counter | Total messages dispatched since last start |
+| `message_inflight_count` | gauge | Messages dispatched but not acknowledged |
+| `expired_count_total` | counter | Total messages expired since last start |
+| `consumer_count` | gauge | Number of consumers |
+| `producer_count` | gauge | Number of producers |
+| `memory_percent_usage` | gauge | Percent of destination memory limit used |
+| `memory_limit_bytes` | gauge | Memory limit for destination in bytes |
+| `memory_usage_bytes` | gauge | Memory used by destination in bytes |
+| `store_message_size_bytes` | gauge | Store message size in bytes |
+| `average_enqueue_time_milliseconds` | gauge | Average time (since last start) messages waited before dispatch |
+
+## Prometheus configuration
+
+Example yaml configuration for running a Prometheus scraper on the same machine as the broker
+```yaml
+scrape_configs:
+ - job_name: activemq
+ metrics_path: /metrics
+ params:
+ per_object: ['true'] # omit for broker-only
+ basic_auth:
+ username: admin
+ password: admin
+ static_configs:
+ - targets: ['localhost:8161']
+```
diff --git a/activemq-prometheus/pom.xml b/activemq-prometheus/pom.xml
new file mode 100644
index 00000000000..2dd7e7154f8
--- /dev/null
+++ b/activemq-prometheus/pom.xml
@@ -0,0 +1,58 @@
+
+
+
+
+ 4.0.0
+
+
+ org.apache.activemq
+ activemq-parent
+ 6.4.0-SNAPSHOT
+
+
+ activemq-prometheus
+ war
+ ActiveMQ :: Prometheus
+ ActiveMQ Prometheus metrics endpoint
+
+
+
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+
+
+
+ junit
+ junit
+ test
+
+
+
+
+ metrics
+
+
+
diff --git a/activemq-prometheus/src/main/java/org/apache/activemq/prometheus/PrometheusMetricsServlet.java b/activemq-prometheus/src/main/java/org/apache/activemq/prometheus/PrometheusMetricsServlet.java
new file mode 100644
index 00000000000..390b392f7cc
--- /dev/null
+++ b/activemq-prometheus/src/main/java/org/apache/activemq/prometheus/PrometheusMetricsServlet.java
@@ -0,0 +1,195 @@
+/**
+ * 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.activemq.prometheus;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.management.ManagementFactory;
+import java.util.Set;
+
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+public class PrometheusMetricsServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+ private static final String CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
+
+ // Metrics can be easily extended by adding them here
+ private static final MetricDefinition[] BROKER_METRICS = {
+ new MetricDefinition("connections", "Current number of connections", "CurrentConnectionsCount", MetricType.GAUGE),
+ new MetricDefinition("connections_total", "Total connections since last start", "TotalConnectionsCount", MetricType.COUNTER),
+ new MetricDefinition("messages_enqueued_total", "Total messages enqueued since last start", "TotalEnqueueCount", MetricType.COUNTER),
+ new MetricDefinition("messages_dequeued_total", "Total messages dequeued since last start", "TotalDequeueCount", MetricType.COUNTER),
+ new MetricDefinition("consumers", "Current number of consumers", "TotalConsumerCount", MetricType.GAUGE),
+ new MetricDefinition("producers", "Current number of producers", "TotalProducerCount", MetricType.GAUGE),
+ new MetricDefinition("messages", "Current number of messages across all destinations", "TotalMessageCount", MetricType.GAUGE),
+ new MetricDefinition("memory_percent_usage", "Percent (0-100) of memory limit used", "MemoryPercentUsage", MetricType.GAUGE),
+ new MetricDefinition("memory_limit_bytes", "Memory limit in bytes", "MemoryLimit", MetricType.GAUGE),
+ new MetricDefinition("store_percent_usage", "Percent (0-100) of store limit used", "StorePercentUsage", MetricType.GAUGE),
+ new MetricDefinition("store_limit_bytes", "Store limit in bytes", "StoreLimit", MetricType.GAUGE),
+ new MetricDefinition("temp_percent_usage", "Percent (0-100) of temp limit used", "TempPercentUsage", MetricType.GAUGE),
+ new MetricDefinition("temp_limit_bytes", "Temp limit in bytes", "TempLimit", MetricType.GAUGE),
+ new MetricDefinition("uptime_milliseconds", "Broker uptime in milliseconds", "UptimeMillis", MetricType.GAUGE)
+ };
+
+ private static final MetricDefinition[] DESTINATION_METRICS = {
+ new MetricDefinition("messages", "Number of messages in this destination", "QueueSize", MetricType.GAUGE),
+ new MetricDefinition("enqueued_total", "Total messages enqueued to this destination since last start", "EnqueueCount", MetricType.COUNTER),
+ new MetricDefinition("dequeued_total", "Total messages dequeued from destination since last start", "DequeueCount", MetricType.COUNTER),
+ new MetricDefinition("dispatched_total", "Total messages dispatched from destination since last start", "DispatchCount", MetricType.COUNTER),
+ new MetricDefinition("message_inflight_count", "Messages dispatched but not acknowledged", "InFlightCount", MetricType.GAUGE),
+ new MetricDefinition("expired_total", "Total messages expired since last start", "ExpiredCount", MetricType.COUNTER),
+ new MetricDefinition("consumers", "Number of consumers", "ConsumerCount", MetricType.GAUGE),
+ new MetricDefinition("producers", "Number of producers", "ProducerCount", MetricType.GAUGE),
+ new MetricDefinition("memory_percent_usage", "Percent (0-100) of destination memory limit used", "MemoryPercentUsage", MetricType.GAUGE),
+ new MetricDefinition("memory_limit_bytes", "Memory limit for this destination in bytes", "MemoryLimit", MetricType.GAUGE),
+ new MetricDefinition("memory_usage_bytes", "Memory used by this destination in bytes", "MemoryUsageByteCount", MetricType.GAUGE),
+ new MetricDefinition("store_message_size_bytes", "Store message size in bytes", "StoreMessageSize", MetricType.GAUGE),
+ new MetricDefinition("average_enqueue_time_milliseconds", "Average time (since last start) messages waited before dispatch", "AverageEnqueueTime", MetricType.GAUGE)
+ };
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ boolean perObject = request != null && "true".equalsIgnoreCase(request.getParameter("per_object"));
+
+ StringWriter output = new StringWriter();
+ PrintWriter writer = new PrintWriter(output);
+
+ try {
+ writeMetrics(ManagementFactory.getPlatformMBeanServer(), writer, perObject);
+ } catch (Exception exception) {
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Metrics collection failed");
+ return;
+ }
+
+ response.setContentType(CONTENT_TYPE);
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().write(output.toString());
+ }
+
+ void writeMetrics(MBeanServer mBeanServer, PrintWriter writer, boolean perObject) throws Exception {
+ writeBrokerMetrics(mBeanServer, writer);
+ // Scraping this by default on brokers with lots of queues or topics might be expensive
+ if (perObject) {
+ writeDestinationMetrics(mBeanServer, writer, "Queue");
+ writeDestinationMetrics(mBeanServer, writer, "Topic");
+ }
+ writer.flush();
+ }
+
+ private void writeBrokerMetrics(MBeanServer mBeanServer, PrintWriter writer) throws Exception {
+ ObjectName pattern = new ObjectName("org.apache.activemq:type=Broker,brokerName=*");
+ Set brokers = mBeanServer.queryNames(pattern, null);
+
+ for (MetricDefinition metric : BROKER_METRICS) {
+ String metricName = "activemq_broker_" + metric.name;
+ writeMetadata(writer, metricName, metric);
+ for (ObjectName broker : brokers) {
+ String brokerName = sanitizeLabel((String) mBeanServer.getAttribute(broker, "BrokerName"));
+ String labels = "broker=\"" + brokerName + "\"";
+ writeSample(writer, metricName, labels, getNumber(mBeanServer, broker, metric.attribute));
+ }
+ }
+ }
+
+ private void writeDestinationMetrics(MBeanServer mBeanServer, PrintWriter writer, String type) throws Exception {
+ String queryPattern = "org.apache.activemq:type=Broker,brokerName=*,destinationType=" + type + ",destinationName=*";
+ Set destinations = mBeanServer.queryNames(new ObjectName(queryPattern), null);
+ String typeLower = type.toLowerCase();
+
+ for (MetricDefinition metric : DESTINATION_METRICS) {
+ String metricName = "activemq_" + typeLower + "_" + metric.name;
+ writeMetadata(writer, metricName, metric.withFormattedHelp(typeLower));
+ for (ObjectName destination : destinations) {
+ String brokerName = sanitizeLabel(destination.getKeyProperty("brokerName"));
+ String destinationName = sanitizeLabel(destination.getKeyProperty("destinationName"));
+ String labels = String.format("broker=\"%s\",destination=\"%s\"", brokerName, destinationName);
+ writeSample(writer, metricName, labels, getNumber(mBeanServer, destination, metric.attribute));
+ }
+ }
+ }
+
+ private double getNumber(MBeanServer mBeanServer, ObjectName name, String attribute) {
+ try {
+ Object value = mBeanServer.getAttribute(name, attribute);
+ if (value instanceof Number) {
+ return ((Number) value).doubleValue();
+ }
+ } catch (Exception ignored) {
+ // Some attributes are not available on every ActiveMQ deployment (eg: bridge metrics)
+ }
+ return 0;
+ }
+
+ private void writeMetadata(PrintWriter writer, String metric, MetricDefinition def) {
+ writer.println("# HELP " + metric + " " + def.help);
+ writer.println("# TYPE " + metric + " " + def.type.prometheusName());
+ }
+
+ private void writeSample(PrintWriter writer, String metric, String labels, double value) {
+ if (value == (long) value) {
+ writer.println(metric + "{" + labels + "} " + (long) value);
+ } else {
+ writer.println(metric + "{" + labels + "} " + value);
+ }
+ }
+
+ static String sanitizeLabel(String value) {
+ if (value == null) {
+ return "unknown";
+ }
+ return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
+ }
+
+ private static final class MetricDefinition {
+ private final String name;
+ private final String help;
+ private final String attribute;
+ private final MetricType type;
+
+ private MetricDefinition(String name, String help, String attribute, MetricType type) {
+ this.name = name;
+ this.help = help;
+ this.attribute = attribute;
+ this.type = type;
+ }
+
+ private MetricDefinition withFormattedHelp(String arg) {
+ return new MetricDefinition(name, String.format(help, arg), attribute, type);
+ }
+ }
+
+ enum MetricType {
+ GAUGE("gauge"),
+ COUNTER("counter");
+
+ private final String prometheusName;
+
+ MetricType(String prometheusName) {
+ this.prometheusName = prometheusName;
+ }
+
+ String prometheusName() {
+ return prometheusName;
+ }
+ }
+}
diff --git a/activemq-prometheus/src/main/resources/example-grafana-dashboard.json b/activemq-prometheus/src/main/resources/example-grafana-dashboard.json
new file mode 100644
index 00000000000..cd1c9b01059
--- /dev/null
+++ b/activemq-prometheus/src/main/resources/example-grafana-dashboard.json
@@ -0,0 +1,216 @@
+{
+ "annotations": { "list": [] },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 1,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
+ "id": 100,
+ "title": "Broker Overview",
+ "type": "row"
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 100 }, { "color": "red", "value": 500 }] } } },
+ "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 },
+ "id": 1,
+ "options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
+ "title": "Connections",
+ "type": "stat",
+ "targets": [{ "expr": "activemq_broker_connections_count", "legendFormat": "{{broker}}" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 50 }, { "color": "red", "value": 80 }] } } },
+ "gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 },
+ "id": 2,
+ "options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
+ "title": "Memory %",
+ "type": "stat",
+ "targets": [{ "expr": "activemq_broker_memory_percent_usage", "legendFormat": "{{broker}}" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 50 }, { "color": "red", "value": 80 }] } } },
+ "gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 },
+ "id": 3,
+ "options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
+ "title": "Store %",
+ "type": "stat",
+ "targets": [{ "expr": "activemq_broker_store_percent_usage", "legendFormat": "{{broker}}" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 50 }, { "color": "red", "value": 80 }] } } },
+ "gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 },
+ "id": 4,
+ "options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
+ "title": "Temp %",
+ "type": "stat",
+ "targets": [{ "expr": "activemq_broker_temp_percent_usage", "legendFormat": "{{broker}}" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "steps": [{ "color": "green", "value": null }] } } },
+ "gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 },
+ "id": 5,
+ "options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
+ "title": "Consumers",
+ "type": "stat",
+ "targets": [{ "expr": "activemq_broker_consumers_count", "legendFormat": "{{broker}}" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "steps": [{ "color": "green", "value": null }] } } },
+ "gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 },
+ "id": 6,
+ "options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
+ "title": "Producers",
+ "type": "stat",
+ "targets": [{ "expr": "activemq_broker_producers_count", "legendFormat": "{{broker}}" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "short" } },
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 5 },
+ "id": 10,
+ "options": { "legend": { "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
+ "title": "Enqueue / Dequeue Rate",
+ "type": "timeseries",
+ "targets": [
+ { "expr": "rate(activemq_broker_messages_enqueued_total[1m])", "legendFormat": "enqueue/s" },
+ { "expr": "rate(activemq_broker_messages_dequeued_total[1m])", "legendFormat": "dequeue/s" }
+ ]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "short" } },
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 5 },
+ "id": 11,
+ "options": { "legend": { "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
+ "title": "Connections Over Time",
+ "type": "timeseries",
+ "targets": [
+ { "expr": "activemq_broker_connections_count", "legendFormat": "current" },
+ { "expr": "rate(activemq_broker_connections_total[5m])", "legendFormat": "new/s (5m)" }
+ ]
+ },
+ {
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 13 },
+ "id": 200,
+ "title": "Queues (requires ?per_object=true)",
+ "type": "row"
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "short", "custom": { "fillOpacity": 15 } } },
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 14 },
+ "id": 20,
+ "options": { "legend": { "displayMode": "table", "placement": "right", "sortBy": "Last", "sortDesc": true }, "tooltip": { "mode": "multi" } },
+ "title": "Queue Depth",
+ "type": "timeseries",
+ "targets": [{ "expr": "activemq_queue_message_count", "legendFormat": "{{destination}} depth" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "short" } },
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 14 },
+ "id": 21,
+ "options": { "legend": { "displayMode": "table", "placement": "right", "sortBy": "Last", "sortDesc": true }, "tooltip": { "mode": "multi" } },
+ "title": "Queue Enqueue Rate",
+ "type": "timeseries",
+ "targets": [{ "expr": "rate(activemq_queue_enqueue_count_total[1m])", "legendFormat": "{{destination}} enqueue/s" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "short" } },
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 22 },
+ "id": 22,
+ "options": { "legend": { "displayMode": "table", "placement": "right", "sortBy": "Last", "sortDesc": true }, "tooltip": { "mode": "multi" } },
+ "title": "Queue In-Flight",
+ "type": "timeseries",
+ "targets": [{ "expr": "activemq_queue_message_inflight_count", "legendFormat": "{{destination}} inflight" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "short" } },
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 22 },
+ "id": 23,
+ "options": { "legend": { "displayMode": "table", "placement": "right", "sortBy": "Last", "sortDesc": true }, "tooltip": { "mode": "multi" } },
+ "title": "Queue Consumers",
+ "type": "timeseries",
+ "targets": [{ "expr": "activemq_queue_consumer_count", "legendFormat": "{{destination}} consumers" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "percent" } },
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 30 },
+ "id": 24,
+ "options": { "legend": { "displayMode": "table", "placement": "right" }, "tooltip": { "mode": "multi" } },
+ "title": "Queue Memory %",
+ "type": "timeseries",
+ "targets": [{ "expr": "activemq_queue_memory_percent_usage", "legendFormat": "{{destination}} mem%" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "ms" } },
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 30 },
+ "id": 25,
+ "options": { "legend": { "displayMode": "table", "placement": "right", "sortBy": "Last", "sortDesc": true }, "tooltip": { "mode": "multi" } },
+ "title": "Queue Avg Enqueue Time",
+ "type": "timeseries",
+ "targets": [{ "expr": "activemq_queue_average_enqueue_time_milliseconds", "legendFormat": "{{destination}} avg ms" }]
+ },
+ {
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 38 },
+ "id": 300,
+ "title": "Topics (requires ?per_object=true)",
+ "type": "row"
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "short", "custom": { "fillOpacity": 15 } } },
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 39 },
+ "id": 30,
+ "options": { "legend": { "displayMode": "table", "placement": "right", "sortBy": "Last", "sortDesc": true }, "tooltip": { "mode": "multi" } },
+ "title": "Topic Enqueue Rate",
+ "type": "timeseries",
+ "targets": [{ "expr": "rate(activemq_topic_enqueue_count_total[1m])", "legendFormat": "{{destination}} enqueue/s" }]
+ },
+ {
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "fieldConfig": { "defaults": { "unit": "short" } },
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 39 },
+ "id": 31,
+ "options": { "legend": { "displayMode": "table", "placement": "right", "sortBy": "Last", "sortDesc": true }, "tooltip": { "mode": "multi" } },
+ "title": "Topic Consumers",
+ "type": "timeseries",
+ "targets": [{ "expr": "activemq_topic_consumer_count", "legendFormat": "{{destination}} consumers" }]
+ }
+ ],
+ "schemaVersion": 39,
+ "tags": ["activemq", "prometheus", "native-plugin"],
+ "templating": {
+ "list": [
+ {
+ "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" },
+ "hide": 0,
+ "includeAll": false,
+ "name": "DS_PROMETHEUS",
+ "options": [],
+ "query": "prometheus",
+ "type": "datasource"
+ }
+ ]
+ },
+ "time": { "from": "now-30m", "to": "now" },
+ "title": "ActiveMQ Native Prometheus Plugin",
+ "uid": "activemq-native-prom",
+ "version": 1
+}
diff --git a/activemq-prometheus/src/main/webapp/META-INF/LICENSE b/activemq-prometheus/src/main/webapp/META-INF/LICENSE
new file mode 100644
index 00000000000..d6456956733
--- /dev/null
+++ b/activemq-prometheus/src/main/webapp/META-INF/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
diff --git a/activemq-prometheus/src/main/webapp/META-INF/NOTICE b/activemq-prometheus/src/main/webapp/META-INF/NOTICE
new file mode 100644
index 00000000000..6023dae2e59
--- /dev/null
+++ b/activemq-prometheus/src/main/webapp/META-INF/NOTICE
@@ -0,0 +1,6 @@
+ActiveMQ :: Prometheus
+Copyright 2005-2026 The Apache Software Foundation
+
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/activemq-prometheus/src/main/webapp/WEB-INF/web.xml b/activemq-prometheus/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..f4fde4db6f6
--- /dev/null
+++ b/activemq-prometheus/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ Apache ActiveMQ Prometheus Metrics
+
+
+ PrometheusMetrics
+ org.apache.activemq.prometheus.PrometheusMetricsServlet
+ 1
+
+
+
+ PrometheusMetrics
+ /*
+
+
+
diff --git a/activemq-prometheus/src/test/java/org/apache/activemq/prometheus/PrometheusMetricsServletTest.java b/activemq-prometheus/src/test/java/org/apache/activemq/prometheus/PrometheusMetricsServletTest.java
new file mode 100644
index 00000000000..f5b3afaf464
--- /dev/null
+++ b/activemq-prometheus/src/test/java/org/apache/activemq/prometheus/PrometheusMetricsServletTest.java
@@ -0,0 +1,445 @@
+/**
+ * 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.activemq.prometheus;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PrometheusMetricsServletTest {
+
+ private static final String CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
+ private static final ObjectName BROKER_NAME;
+ private static final ObjectName QUEUE_NAME;
+ private static final ObjectName SECOND_QUEUE_NAME;
+ private static final ObjectName TOPIC_NAME;
+ private static final ObjectName INVALID_BROKER_NAME;
+
+ static {
+ try {
+ BROKER_NAME = new ObjectName("org.apache.activemq:type=Broker,brokerName=TestBroker");
+ QUEUE_NAME = new ObjectName("org.apache.activemq:type=Broker,brokerName=TestBroker,"
+ + "destinationType=Queue,destinationName=test.queue");
+ SECOND_QUEUE_NAME = new ObjectName("org.apache.activemq:type=Broker,brokerName=TestBroker,"
+ + "destinationType=Queue,destinationName=orders.queue");
+ TOPIC_NAME = new ObjectName("org.apache.activemq:type=Broker,brokerName=TestBroker,"
+ + "destinationType=Topic,destinationName=events.topic");
+ INVALID_BROKER_NAME = new ObjectName("org.apache.activemq:type=Broker,brokerName=InvalidBroker");
+ } catch (Exception exception) {
+ throw new ExceptionInInitializerError(exception);
+ }
+ }
+
+ private MBeanServer mBeanServer;
+
+ @Before
+ public void setUp() throws Exception {
+ mBeanServer = ManagementFactory.getPlatformMBeanServer();
+ mBeanServer.registerMBean(new FakeBroker(), BROKER_NAME);
+ mBeanServer.registerMBean(new FakeDestination(), QUEUE_NAME);
+ mBeanServer.registerMBean(new FakeDestination(), SECOND_QUEUE_NAME);
+ mBeanServer.registerMBean(new FakeDestination(), TOPIC_NAME);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ unregister(BROKER_NAME);
+ unregister(QUEUE_NAME);
+ unregister(SECOND_QUEUE_NAME);
+ unregister(TOPIC_NAME);
+ unregister(INVALID_BROKER_NAME);
+ }
+
+ @Test
+ public void testDefaultResponseReturnsBrokerMetricsOnly() throws Exception {
+ CapturedResponse response = invokeServlet(null);
+ String output = response.output.toString();
+
+ assertEquals(HttpServletResponse.SC_OK, response.status);
+ assertEquals(CONTENT_TYPE, response.contentType);
+ assertTrue(output.endsWith("\n"));
+
+ // Broker metrics present
+ assertTrue(output.contains("activemq_broker_connections_count{broker=\"TestBroker\"} 42"));
+ assertTrue(output.contains("activemq_broker_messages_enqueued_total{broker=\"TestBroker\"} 50000"));
+
+ // Percent usage reported as raw integer from MBean (no conversion)
+ assertTrue(output.contains("activemq_broker_memory_percent_usage{broker=\"TestBroker\"} 25"));
+ assertTrue(output.contains("activemq_broker_store_percent_usage{broker=\"TestBroker\"} 10"));
+ assertTrue(output.contains("activemq_broker_temp_percent_usage{broker=\"TestBroker\"} 5"));
+
+ // Destination metrics absent by default
+ assertFalse(output.contains("activemq_queue_"));
+ assertFalse(output.contains("activemq_topic_"));
+
+ assertMetadataAppearsOncePerMetric(output);
+ assertSamplesHavePrometheusSyntax(output);
+ }
+
+ @Test
+ public void testPerObjectResponseIncludesDestinationMetrics() throws Exception {
+ Map params = new HashMap<>();
+ params.put("per_object", "true");
+ CapturedResponse response = invokeServlet(params);
+ String output = response.output.toString();
+
+ assertEquals(HttpServletResponse.SC_OK, response.status);
+
+ // Broker metrics still present
+ assertTrue(output.contains("activemq_broker_connections_count{broker=\"TestBroker\"} 42"));
+
+ // Destination metrics now present
+ assertTrue(output.contains("activemq_queue_message_count{broker=\"TestBroker\",destination=\"test.queue\"} 100"));
+ assertTrue(output.contains("activemq_queue_message_count{broker=\"TestBroker\",destination=\"orders.queue\"} 100"));
+ assertTrue(output.contains("activemq_topic_message_count{broker=\"TestBroker\",destination=\"events.topic\"} 100"));
+
+ // AverageEnqueueTime present (fractional value preserved)
+ assertTrue(output.contains("activemq_queue_average_enqueue_time_milliseconds{broker=\"TestBroker\",destination=\"test.queue\"} 3.7"));
+
+ // Percent usage reported as raw integer from MBean
+ assertTrue(output.contains("activemq_queue_memory_percent_usage{broker=\"TestBroker\",destination=\"test.queue\"} 15"));
+
+ assertMetadataAppearsOncePerMetric(output);
+ assertSamplesHavePrometheusSyntax(output);
+ }
+
+ @Test
+ public void testCollectionFailureReturnsServerErrorWithoutPartialMetrics() throws Exception {
+ mBeanServer.registerMBean(new InvalidBroker(), INVALID_BROKER_NAME);
+
+ CapturedResponse response = invokeServlet(null);
+
+ assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.status);
+ assertEquals("Metrics collection failed", response.errorMessage);
+ assertEquals("", response.output.toString());
+ }
+
+ @Test
+ public void testLabelSanitization() {
+ assertEquals("hello", PrometheusMetricsServlet.sanitizeLabel("hello"));
+ assertEquals("a\\\"b", PrometheusMetricsServlet.sanitizeLabel("a\"b"));
+ assertEquals("a\\\\b", PrometheusMetricsServlet.sanitizeLabel("a\\b"));
+ assertEquals("a\\nb", PrometheusMetricsServlet.sanitizeLabel("a\nb"));
+ assertEquals("unknown", PrometheusMetricsServlet.sanitizeLabel(null));
+ }
+
+ private CapturedResponse invokeServlet(Map params) throws Exception {
+ CapturedResponse captured = new CapturedResponse();
+
+ HttpServletRequest request = (HttpServletRequest) Proxy.newProxyInstance(
+ HttpServletRequest.class.getClassLoader(), new Class>[] {HttpServletRequest.class},
+ (proxy, method, arguments) -> {
+ if ("getParameter".equals(method.getName())) {
+ return params != null ? params.get(arguments[0]) : null;
+ }
+ return null;
+ });
+
+ HttpServletResponse response = (HttpServletResponse) Proxy.newProxyInstance(
+ HttpServletResponse.class.getClassLoader(), new Class>[] {HttpServletResponse.class},
+ (proxy, method, arguments) -> {
+ switch (method.getName()) {
+ case "getWriter":
+ return captured.writer;
+ case "setContentType":
+ captured.contentType = (String) arguments[0];
+ return null;
+ case "setStatus":
+ captured.status = (Integer) arguments[0];
+ return null;
+ case "sendError":
+ captured.status = (Integer) arguments[0];
+ captured.errorMessage = (String) arguments[1];
+ return null;
+ default:
+ throw new UnsupportedOperationException(method.getName());
+ }
+ });
+
+ new PrometheusMetricsServlet().doGet(request, response);
+ captured.writer.flush();
+ return captured;
+ }
+
+ private void assertMetadataAppearsOncePerMetric(String output) {
+ for (String line : output.split("\\n")) {
+ if (line.startsWith("# HELP ")) {
+ String metric = line.substring("# HELP ".length(), line.indexOf(' ', "# HELP ".length()));
+ assertEquals(1, countOccurrences(output, "# HELP " + metric + " "));
+ assertTrue("Missing TYPE for " + metric,
+ output.contains("# TYPE " + metric + " "));
+ }
+ }
+ }
+
+ private void assertSamplesHavePrometheusSyntax(String output) {
+ for (String line : output.split("\\n")) {
+ if (!line.isEmpty() && !line.startsWith("#")) {
+ assertTrue("Invalid Prometheus sample: " + line,
+ line.matches("[a-zA-Z_:][a-zA-Z0-9_:]*\\{[^}]+} -?[0-9]+(\\.[0-9]+)?"));
+ }
+ }
+ }
+
+ private int countOccurrences(String value, String search) {
+ int count = 0;
+ int index = 0;
+ while ((index = value.indexOf(search, index)) >= 0) {
+ count++;
+ index += search.length();
+ }
+ return count;
+ }
+
+ private void unregister(ObjectName name) throws Exception {
+ if (mBeanServer.isRegistered(name)) {
+ mBeanServer.unregisterMBean(name);
+ }
+ }
+
+ private static final class CapturedResponse {
+ private final StringWriter output = new StringWriter();
+ private final PrintWriter writer = new PrintWriter(output);
+ private int status;
+ private String contentType;
+ private String errorMessage;
+ }
+
+ public interface InvalidBrokerMBean {
+ int getBrokerName();
+ }
+
+ public static class InvalidBroker implements InvalidBrokerMBean {
+ @Override
+ public int getBrokerName() {
+ return 1;
+ }
+ }
+
+ public interface FakeBrokerMBean {
+ String getBrokerName();
+
+ int getCurrentConnectionsCount();
+
+ long getTotalConnectionsCount();
+
+ long getTotalEnqueueCount();
+
+ long getTotalDequeueCount();
+
+ long getTotalConsumerCount();
+
+ long getTotalProducerCount();
+
+ long getTotalMessageCount();
+
+ int getMemoryPercentUsage();
+
+ long getMemoryLimit();
+
+ int getStorePercentUsage();
+
+ long getStoreLimit();
+
+ int getTempPercentUsage();
+
+ long getTempLimit();
+
+ long getUptimeMillis();
+ }
+
+ public static class FakeBroker implements FakeBrokerMBean {
+ @Override
+ public String getBrokerName() {
+ return "TestBroker";
+ }
+
+ @Override
+ public int getCurrentConnectionsCount() {
+ return 42;
+ }
+
+ @Override
+ public long getTotalConnectionsCount() {
+ return 1000;
+ }
+
+ @Override
+ public long getTotalEnqueueCount() {
+ return 50000;
+ }
+
+ @Override
+ public long getTotalDequeueCount() {
+ return 49000;
+ }
+
+ @Override
+ public long getTotalConsumerCount() {
+ return 10;
+ }
+
+ @Override
+ public long getTotalProducerCount() {
+ return 5;
+ }
+
+ @Override
+ public long getTotalMessageCount() {
+ return 1000;
+ }
+
+ @Override
+ public int getMemoryPercentUsage() {
+ return 25;
+ }
+
+ @Override
+ public long getMemoryLimit() {
+ return 1073741824L;
+ }
+
+ @Override
+ public int getStorePercentUsage() {
+ return 10;
+ }
+
+ @Override
+ public long getStoreLimit() {
+ return 107374182400L;
+ }
+
+ @Override
+ public int getTempPercentUsage() {
+ return 5;
+ }
+
+ @Override
+ public long getTempLimit() {
+ return 53687091200L;
+ }
+
+ @Override
+ public long getUptimeMillis() {
+ return 86400000L;
+ }
+ }
+
+ public interface FakeDestinationMBean {
+ long getQueueSize();
+
+ long getEnqueueCount();
+
+ long getDequeueCount();
+
+ long getDispatchCount();
+
+ long getInFlightCount();
+
+ long getExpiredCount();
+
+ long getConsumerCount();
+
+ long getProducerCount();
+
+ int getMemoryPercentUsage();
+
+ long getMemoryUsageByteCount();
+
+ long getStoreMessageSize();
+
+ double getAverageEnqueueTime();
+ }
+
+ public static class FakeDestination implements FakeDestinationMBean {
+ @Override
+ public long getQueueSize() {
+ return 100;
+ }
+
+ @Override
+ public long getEnqueueCount() {
+ return 5000;
+ }
+
+ @Override
+ public long getDequeueCount() {
+ return 4900;
+ }
+
+ @Override
+ public long getDispatchCount() {
+ return 4950;
+ }
+
+ @Override
+ public long getInFlightCount() {
+ return 50;
+ }
+
+ @Override
+ public long getExpiredCount() {
+ return 10;
+ }
+
+ @Override
+ public long getConsumerCount() {
+ return 3;
+ }
+
+ @Override
+ public long getProducerCount() {
+ return 2;
+ }
+
+ @Override
+ public int getMemoryPercentUsage() {
+ return 15;
+ }
+
+ @Override
+ public long getMemoryUsageByteCount() {
+ return 161061273L;
+ }
+
+ @Override
+ public long getStoreMessageSize() {
+ return 524288000L;
+ }
+
+ @Override
+ public double getAverageEnqueueTime() {
+ return 3.7;
+ }
+ }
+}
diff --git a/assembly/pom.xml b/assembly/pom.xml
index 30ffa9e0071..29540e6a91b 100644
--- a/assembly/pom.xml
+++ b/assembly/pom.xml
@@ -150,6 +150,11 @@
activemq-web-console
war
+
+ ${project.groupId}
+ activemq-prometheus
+ war
+
${project.groupId}
activemq-rar
diff --git a/assembly/src/main/descriptors/common-bin.xml b/assembly/src/main/descriptors/common-bin.xml
index d6a945e4bad..b0dbbd6557e 100644
--- a/assembly/src/main/descriptors/common-bin.xml
+++ b/assembly/src/main/descriptors/common-bin.xml
@@ -73,6 +73,23 @@
0755
+
+
+ ../activemq-prometheus/src/main/webapp
+ webapps/metrics
+ 0644
+ 0755
+
+
+ ../activemq-prometheus/target/classes
+ webapps/metrics/WEB-INF/classes
+
+ **/*.class
+
+ 0644
+ 0755
+
+
../activemq-web-demo/src/main/webapp
diff --git a/assembly/src/release/conf/jetty/jetty-security.xml b/assembly/src/release/conf/jetty/jetty-security.xml
index c6e3eceb11f..169db1a91c1 100644
--- a/assembly/src/release/conf/jetty/jetty-security.xml
+++ b/assembly/src/release/conf/jetty/jetty-security.xml
@@ -81,6 +81,16 @@
+
+
+ /metrics/*
+
+
+ - admins
+
+
+
+
/*
diff --git a/assembly/src/release/conf/jetty/jetty-webapps.xml b/assembly/src/release/conf/jetty/jetty-webapps.xml
index f4bd9603d3b..939ad01e35c 100644
--- a/assembly/src/release/conf/jetty/jetty-webapps.xml
+++ b/assembly/src/release/conf/jetty/jetty-webapps.xml
@@ -176,6 +176,18 @@
+
+
diff --git a/bom/pom.xml b/bom/pom.xml
index 321244473f2..13be22a6374 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -147,6 +147,12 @@
activemq-shiro
${project.version}
+
+ org.apache.activemq
+ activemq-prometheus
+ ${project.version}
+ war
+
org.apache.activemq
activemq-spring
diff --git a/pom.xml b/pom.xml
index df322396fd4..5e2eba0544e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -217,6 +217,7 @@
activemq-rar
activemq-run
activemq-shiro
+ activemq-prometheus
activemq-spring
activemq-runtime-config
activemq-tooling
@@ -349,6 +350,12 @@
activemq-shiro
${project.version}
+
+ org.apache.activemq
+ activemq-prometheus
+ ${project.version}
+ war
+
org.apache.activemq
activemq-spring