Skip to content

feat(prometheus): Add native /metrics endpoint - #2402

Open
hai-ben wants to merge 2 commits into
apache:mainfrom
hai-ben:prometheus-poc
Open

feat(prometheus): Add native /metrics endpoint#2402
hai-ben wants to merge 2 commits into
apache:mainfrom
hai-ben:prometheus-poc

Conversation

@hai-ben

@hai-ben hai-ben commented Aug 5, 2026

Copy link
Copy Markdown

New activemq-prometheus module that adds a /metrics endpoint to the broker that vends metrics in the Prometheus format without requiring any sort of open JMX port. The plugin relies on the existing Jetty auth systems and only scrapes MBean during a request, so it doesn't put any additional load on the broker unless someone is actually using it and users define their own Prometheus scrapers and monitoring setup.

It depends only on the jakarta stuff jetty already does.

I've included an example grafana dashboard in this PR just as a way to get up an running quickly (./activemq-prometheus/src/main/resources/example-grafana-dashboard.json). This wouldn't be in the final PR, instead something like it would be uploaded to grafana.com/dashboards.

See Discussion: #2226

Questions for the community:

  • What permissions should be required to reach this endpoint? admins? everyone?
  • What do you think about the chosen metrics and names?
  • Because it's no load without use, what are your thoughts about having it on by default?

@graben

graben commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Maybe an idea for improvement for broker with many queues an topics. Collect metrics periodically in the background and answer http request with the last collected result. This avoids a lot of preasure on the broker and keep getting results back to prometheus "quickly" available. In my experience is better to not not the most actual data but have at most actual data by providing broker stability.

@hai-ben

hai-ben commented Aug 5, 2026

Copy link
Copy Markdown
Author

I like the idea @graben. I know for RabbitMQ, there's an internal metrics store that both their console and their prometheus plugin scrape from, so the plugin still has 0 load on idle, does that pattern fix what you're getting at?

A couple of other parallel additions I might also just add in this vein:

  • A simple cache with some configurable TTL, additional callers inside the TTL would represent only marginal load
  • Adding support to stream the response back to callers

@hai-ben

hai-ben commented Aug 5, 2026

Copy link
Copy Markdown
Author

Looking into the source a little more, it looks like everything is already just describing MBeans for metrics anyways. The reason RabbitMQ has that pattern is that those metrics need to be actually aggregated, ActiveMQ MBeans are already ready to use. So I'm not sure I see the savings that pre-caching metrics has over lazily updating a cache in response to a caller.

I'll still add the streaming response and the lazy TTL cache, as those are definitely worthwhile (imo).

@sergio-d-lemos sergio-d-lemos left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! I left some simple comments, I understand this is WIP.


@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
boolean perObject = request != null && "true".equalsIgnoreCase(request.getParameter("per_object"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to use final as much as possible

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated a bunch of stuff to use final, lmk if you see anything that could be but isn't.

ObjectName pattern = new ObjectName("org.apache.activemq:type=Broker,brokerName=*");
Set<ObjectName> brokers = mBeanServer.queryNames(pattern, null);

for (MetricDefinition metric : BROKER_METRICS) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as a curiosity, is there any practical difference between iterating the brokers inside the metrics loop or doing the other way around ?

for (ObjectName broker : brokers) {  
   for (MetricDefinition metric : BROKER_METRICS) {
   ...
   }
}

Do we need to have the metrics grouped together ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, not really. Though if we're implementing streaming responses then this makes it so metrics are grouped per object, which seems intuitive to me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LICENSE file should be added to the archive from the central file, not stored redundantly

@mattrpav

Copy link
Copy Markdown
Contributor

Excited to get a modern metrics endpoint into the broker. I think there are a couple architecture points that need to be sorted out.

Use of hand-crafted JSON output-- to avoid a security attack vector his needs serious character escape handling, or (my preference) leverage Jackson JSON processing library.

@hai-ben

hai-ben commented Aug 19, 2026

Copy link
Copy Markdown
Author

Excited to get a modern metrics endpoint into the broker. I think there are a couple architecture points that need to be sorted out.

Use of hand-crafted JSON output-- to avoid a security attack vector his needs serious character escape handling, or (my preference) leverage Jackson JSON processing library.

@mattrpav I'm not sure I understand the security risk JSON output has or the kind of attack you're foreseeing here. Could you tell me more? Maybe link some previous CVEs on this?

@hai-ben

hai-ben commented Aug 19, 2026

Copy link
Copy Markdown
Author

@graben on the lazy cache I did some reading and some tests. Prometheus recommends:

Metrics should only be pulled from the application when Prometheus scrapes them, exporters should not perform scrapes based on their own timers. That is, all scrapes should be synchronous.

The logic being that it's whoever's scraping Prometheus's responsibility to manage time-stamps.

I also did some perf testing to see what affect this would actually have on a real broker using worst-practices. I spun up an m5.xlarge broker and put some queues on it (1_000, 5_000, and 25_0000). Callers would scrape Prometheus every 15s. I tried with 1, 5, and 20 callers. I also ran this under no-load and a load of ~2k msg/s. The best practice is to only touch this endpoint from one caller once every 30-60s and only touch the per-objects when you really need them. Here are the results:

PrometheusTest

The CPU load is noticeable, but even with 20 concurrent scrapers on 25k queues with load, scrape times were well under 10s.

Importantly, the CPU spikes on 20 concurrent scrapers weren't much worse than 1 or 5 scrapers. There are also a lot of edge cases to handle with a lazy cache (deleted queues, race conditions with concurrent scrapes, etc) and it would not provide much benefit against worst practices. And a scheduled cache also puts this non-zero load on the broker, even if it's not being used.

@mattrpav

Copy link
Copy Markdown
Contributor

@mattrpav I'm not sure I understand the security risk JSON output has or the kind of attack you're foreseeing here. Could you tell me more? Maybe link some previous CVEs on this?

The write take data from the broker unsanitized. So things like brokerName, queue/topic names, and clientIds are all user-defined strings that create attack surface for malformed json.

@Jerald Jerald left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long-time ActiveMQ lurker here. I have some interest in Prometheus support myself, very excited to see it happening!

I have a few opportunities for improvement I noticed, not sure what others think, but none of them seem to impact correctness, just future improvements.

Comment on lines +42 to +43
// Metrics can be easily extended by adding them here
private static final MetricDefinition[] BROKER_METRICS = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an opportunity for this metric list to be automatically generated from the registered MBeans on the broker? I can see how a system could deterministically turn the PascalCase name into a snake_case name for Prometheus (although the names would not be the same as right now).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind one of the great things about Prometheus is the ability for people to standardize on dashboards. I'm hesitant to do this because it might create churn for dashboards if MBean names ever change.

Though maybe I'm not understanding something. What's the use case you envision for this?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My mental model (but contributors please chime in) is that the MBean names (and their attributes) are part of the public API, in the sense that ActiveMQ wouldn't break them without documenting it and a Semver change.

Which is to say, MBeans changing is already a breaking change for anyone using them for monitoring, there's no difference in the impact here for Prometheus.

The use-case I see is that this prevents the MBean metrics and Prometheus metrics from diverging over time. Right now, you took some editorial liberties with translating the MBean attribute names to Prometheus metric names. While I don't have an issue with this per se, it does mean the two ways of monitoring an ActiveMQ broker will necessarily diverge a bit. (This also reduces the effort to expose new metrics to Prometheus, because there simply would be no extra effort.)

Comment on lines +87 to +88
final Set<ObjectName> queues;
final Set<ObjectName> topics;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason to split queue and topic information like this? Could this be a combined "destinations" set without much impact?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they must be split. Queues and Topics are separate namespace regions, so it is legit (and spec compliant) to have a queue and topic with the same name.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! That's a good point, the destination name is not globally unique. I believe it would be the tuple of (destination_type, destination_name) that's unique, does that sound right? (Effectively encoding the same info as the destination URI.)

Comment on lines +93 to +96
queues = mBeanServer.queryNames(new ObjectName(
"org.apache.activemq:type=Broker,brokerName=*,destinationType=Queue,destinationName=*"), null);
topics = mBeanServer.queryNames(new ObjectName(
"org.apache.activemq:type=Broker,brokerName=*,destinationType=Topic,destinationName=*"), null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any other destinationType values used in ActiveMQ? (Notably I'm unsure what composite/virtual destinations look like)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still relatively new to ActiveMQ so I'm not totally sure. Maybe this @sergio-d-lemos could answer this one?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did a little digging around, but struggled to find the exact spot defining this destinationType field. I think this might be the transitive source though, showing that the temp destination types are also valid: https://github.com/apache/activemq/blob/main/activemq-client/src/main/java/org/apache/activemq/command/ActiveMQDestination.java#L382-L395

Either way, this leads me to believe that you should assume temp destinations have a different type unless you can prove that to not be true. Missing them here would lead to incomplete metric scraping, possibly being very confusing to some people.

}

for (final MetricDefinition metric : BROKER_METRICS) {
final String metricName = "activemq_broker_" + metric.name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is prefixes like this the conventional pattern for "namespacing" in the prometheus metrics world?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the standard is namespace_subsystem_name. So you have activemq_broker_connections and activemq_queue_messages.

Comment on lines +156 to +167
private String resolveStringAttribute(final MBeanServer mBeanServer, final ObjectName name, final String attribute) {
// Partial results are better than no results if something goes wrong
try {
final Object value = mBeanServer.getAttribute(name, attribute);
if (value instanceof String) {
return (String) value;
}
} catch (final Exception exception) {
LOG.debug("Skipping object {}: identity attribute {} unavailable", name, attribute, exception);
}
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an opportunity to downcast the MBeans to their concrete class as a way to remove some of these string attribute checks?

I'm usually weary of those being brittle in the face of attribute changes, though it's very unlikely to be a concern here.

@Jerald

Jerald commented Aug 19, 2026

Copy link
Copy Markdown

@hai-ben would you have time/interest in running another load test in a different scenario?

I know that in some CPU constrained environments with lots of thread churn, ActiveMQ can quickly buckle due to the impact of starving threads of CPU cycles. Given that Prometheus scrapes look to create a relatively-large single-threaded workload, I'm very interested to understand how (if at all) Prometheus scrapes impact ActiveMQ when everything is competing for CPU cycles.

As I saw you're using EC2 instances for testing (m5.xlarge), a comparable instance type with CPU constraints would be something like t3.micro. Running a load test using short-lived clients that open/close dedicated connections should easily induce the sort of issues I'm talking about.

Comment on lines +145 to +152
final String metricName = "activemq_" + typeLower + "_" + metric.name;
writeMetadata(writer, metricName, metric.withFormattedHelp(typeLower));
for (final ObjectName destination : destinations) {
final String brokerName = sanitizeLabel(destination.getKeyProperty("brokerName"));
final String destinationName = sanitizeLabel(destination.getKeyProperty("destinationName"));
final String labels = String.format("broker=\"%s\",destination=\"%s\"", brokerName, destinationName);
writeSample(writer, metricName, labels, getNumber(mBeanServer, destination, metric.attribute));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm realizing there's an alternate way to encode these metrics that could be used. Right now, the destination type (queue/topic) is encoded in the metric name itself, despite all destinations sharing a set of emitted metrics (though queues and topics do have a few metrics unique to themselves). But because the underlying metrics are mostly overlapping, the destination type can also be disambiguated by a label instead of a unique metric name.

For example, the current format for a destination metric name is something like this: activemq_$destType_$metricName (activemq_queue_connections). And it would have labels like this: broker=$brokerName, destination=$destName (broker="foo",destination="FOO.BAR").

But it should be near equivalent to use a destination name like activemq_destination_$metricName and then have labels broker=$brokerName, destinationType=$destType, and destination=$destName.

Both these approaches are valid (as far as I'm aware), so it's mostly a matter of taste. One upside that I see is having destination type as a label more closely aligns with how the MBeans look. If someone were to convert a tool from using MBeans for metrics and move to Prometheus, I'd expect this approach would be an easier transition (but whether or not that matters is a separate question).

return "unknown";
}
// See: https://prometheus.io/docs/instrumenting/exposition_formats/
return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just reviewing the prometheus docs on this format, but I realized it doesn't state anything about the CR (\r) needing escaping, only the LF (\n).

Do you know if this is an undocumented assumption in Prometheus? (I've not run into it before myself.) Or should this escaping be removed?

Either way, it would be great (but I totally understand if impractical) to have a real Prometheus parser used as a control in some sort of test here. That will help validate that ActiveMQ is not just emitting the metrics that are expected, but also emitting them in a format that can be consumed by Prometheus correctly. An adversarial test with \r would be a perfect starting point if there's a test suite like this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No clue. I think we want to shift this to a library anyways which will handle all of this.

@mattrpav

mattrpav commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

ActiveMQ can quickly buckle due to the impact of starving threads of CPU cycles.

@Jerald I suggest you run your scenarios against ActiveMQ v6.3.1+ with Virtual Threads enabled. In addition to Virtual Threads, a number of hotspots have been removed.

I'd be interested if you find hot spots and I'll remove them.

blog: https://medium.com/javarevisited/how-activemq-v6-3-0-boosts-topic-throughput-up-to-93-6b00006f3b82?sharedUserId=mattrpav

@hai-ben

hai-ben commented Aug 19, 2026

Copy link
Copy Markdown
Author

@mattrpav

The write take data from the broker unsanitized. So things like brokerName, queue/topic names, and clientIds are all user-defined strings that create attack surface for malformed json.

What I was missing was that users that create destinations on the broker would be different than users consuming metrics. And a bad sanitizer could let the former break the latter.

Prometheus output isn't JSON, but I'll swap to the official Prometheus parser/output library to address the concern.

@hai-ben

hai-ben commented Aug 19, 2026

Copy link
Copy Markdown
Author

@hai-ben would you have time/interest in running another load test in a different scenario?

I know that in some CPU constrained environments with lots of thread churn, ActiveMQ can quickly buckle due to the impact of starving threads of CPU cycles. Given that Prometheus scrapes look to create a relatively-large single-threaded workload, I'm very interested to understand how (if at all) Prometheus scrapes impact ActiveMQ when everything is competing for CPU cycles.

As I saw you're using EC2 instances for testing (m5.xlarge), a comparable instance type with CPU constraints would be something like t3.micro. Running a load test using short-lived clients that open/close dedicated connections should easily induce the sort of issues I'm talking about.

Sure @Jerald. I'll try those tests with @mattrpav's suggestion of using virtual threads.

@Jerald

Jerald commented Aug 19, 2026

Copy link
Copy Markdown

I suggest you run your scenarios against ActiveMQ v6.3.1+ with Virtual Threads enabled. In addition to Virtual Threads, a number of hotspots have been removed.

Oh that's great to hear! I'm still stuck using older versions of ActiveMQ in production, with virtual threads likely a ways out, so that's where my thoughts are coming from. I'm happy for this to be a worry of the past!

Is there an opportunity for this (EDIT: Prometheus) to be backported to the 5.19 branch? Or would this only be expected to be available in 6.x? (Feel free to leave these questions until later if they detract from the review here.)

@mattrpav

Copy link
Copy Markdown
Contributor

Oh that's great to hear! I'm still stuck using older versions of ActiveMQ in production, with virtual threads likely a ways out, so that's where my thoughts are coming from. I'm happy for this to be a worry of the past!

Keep in mind-- the broker and clients are wire-compatible across versions. You can run 6.3.x broker and 5.19.x clients no problem.

Is there an opportunity for this (EDIT: Prometheus) to be backported to the 5.19 branch? Or would this only be expected to be available in 6.x? (Feel free to leave these questions until later if they detract from the review here.)

Unfortunately, no. Virtual Threads need JDK 25 support and that is only technically possible in v6.3.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants