From 17481d422c97e764d61def65b2592427dbb9b5e3 Mon Sep 17 00:00:00 2001 From: Hailey <132700658+hai-ben@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:06:31 +0000 Subject: [PATCH] Add a Prometheus /metrics endpoint --- activemq-prometheus/README.md | 92 ++++ activemq-prometheus/pom.xml | 58 +++ .../prometheus/PrometheusMetricsServlet.java | 195 ++++++++ .../resources/example-grafana-dashboard.json | 216 +++++++++ .../src/main/webapp/META-INF/LICENSE | 202 ++++++++ .../src/main/webapp/META-INF/NOTICE | 6 + .../src/main/webapp/WEB-INF/web.xml | 36 ++ .../PrometheusMetricsServletTest.java | 445 ++++++++++++++++++ assembly/pom.xml | 5 + assembly/src/main/descriptors/common-bin.xml | 17 + .../src/release/conf/jetty/jetty-security.xml | 10 + .../src/release/conf/jetty/jetty-webapps.xml | 12 + bom/pom.xml | 6 + pom.xml | 7 + 14 files changed, 1307 insertions(+) create mode 100644 activemq-prometheus/README.md create mode 100644 activemq-prometheus/pom.xml create mode 100644 activemq-prometheus/src/main/java/org/apache/activemq/prometheus/PrometheusMetricsServlet.java create mode 100644 activemq-prometheus/src/main/resources/example-grafana-dashboard.json create mode 100644 activemq-prometheus/src/main/webapp/META-INF/LICENSE create mode 100644 activemq-prometheus/src/main/webapp/META-INF/NOTICE create mode 100644 activemq-prometheus/src/main/webapp/WEB-INF/web.xml create mode 100644 activemq-prometheus/src/test/java/org/apache/activemq/prometheus/PrometheusMetricsServletTest.java 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