JVM and connection-pool metric names: what each producer emits
- JVM and connection-pool metric names: what each producer emits
- Where the numbers came from
- Three producer spellings, not two
- What this already costs us, measured
- Canonical names
- Deliberately not mapped
- Consequence for the plan
- Left on producer names on purpose (task 4b) — mostly closed, 19.09.2026
- Labelled canonical names (task 30, closed)
- The other half of the contract: consumers (task 27, closed)
- One dashboard set, two destinations (task 31, closed)
Version 1.7.1 | Updated: October 06, 2026
JVM and connection-pool metric names: what each producer emits
Everything here was measured on 05.08.2026 against running systems; nothing is quoted from Micrometer or JMX exporter documentation. The hosts are named by role rather than by address: this page is published, and the numbers are what matters, not which machine produced them.
Where the numbers came from
-
JMX, WildFly/ACS 6.4 — an ACS application server, exporters
:5556(Hikari) and:5557(JVM). 110 distinct names. -
JMX, Hazelcast — same host,
:9101,exporter_type=hazelcast. 208java_lang_*lines plus 7176com_hazelcast_*. -
Micrometer, ACS 7.1.1 — an ACS 7.1 test container,
:8080/actuator/prometheus. 98 names. -
Micrometer, ACS 6.5.2 — the
acsOKD cluster, viaft-prometheus-acs,job="ftacs". 167 series names.
6.5 and 7.1 are the same producer. The two sets are identical: 48 shared JVM/pool names, zero on either side alone. Two capture artefacts had to be removed before that was visible, and both are worth knowing if these numbers are ever re-taken:
-
# HELPnames a summary once;/api/v1/serieslists its_count/_sum/_bucketexpansions. Comparing one against the other invents differences. Normalise those three suffixes. -
_maxmust not be normalised away. In Micrometer it is a metric in its own right —hikaricp_connections_maxis what the pool-exhaustion alert queries, and stripping it made this table briefly claim the metric does not exist, when it has 16 series in the cluster and 10 on-prem.
Three producer spellings, not two
This is the part that matters, and it is not what the plan assumed.
-
JMX exporter, lowercase —
java_lang_runtime_uptime,java_lang_classloading_loadedclasscount,java_lang_threading_threadcount. Emitted by the WildFly/ACS JVM exporter (:5557,exporter_type=jvm) on both production ACS hosts. 208 lines, 0 CamelCase. -
JMX exporter, CamelCase —
java_lang_Runtime_Uptime,java_lang_Memory_HeapMemoryUsage_used. Emitted by the Hazelcast exporter (:9101,exporter_type=hazelcast). 208 lines, 0 lowercase. -
Micrometer —
process_uptime_seconds,jvm_memory_used_bytes,hikaricp_connections_active. ACS 6.5+, every Spring Boot service, and Kubernetes.
The two JMX spellings are the same exporter with different config (lowercaseOutputName), and
both are present in production Prometheus at the same time under the same instance label,
separated only by exporter_type.
What this already costs us, measured
-
jmx-dashboardis empty for the JVM it is named after. Its expressions query CamelCase and are scoped byexporter_type=~"$app". On production:count(java_lang_Runtime_Uptime{exporter_type="jvm"})= 0;count(java_lang_Runtime_Uptime{exporter_type="hazelcast"})= 2;count(java_lang_runtime_uptime{exporter_type="jvm"})= 2. So the dashboard shows something only when pointed at Hazelcast. The WildFly JVM data has been collected all along and read by nothing. This matches the note already in thetest-monitoringskill — “the lowercase half has never been read by anything” — and is a defect in its own right, independent of this plan. -
The ACS pool alert is blind on-prem and works in Kubernetes.
hikaricp_connections_active(Micrometer) — on-prem 10 series, none of them ACS; in the cluster it coversftacsitself (4 series).hikaricp_active_connections(JMX) — on-prem 6 series, all ACS; in the cluster 0. The alert queries the Micrometer spelling. -
hikari.jsonis entirely empty in Kubernetes. Every expression in it queries the JMX spelling, which no producer there emits.
Canonical names
ft: prefix. One entry per concept: the canonical name, then the expression per producer.
— means the producer does not carry it and the row is served by the other side (or by
node-exporter, noted explicitly).
-
ft:jvm_uptime_seconds-
jmx/lowercase:
java_lang_runtime_uptime / 1000 -
jmx/CamelCase:
java_lang_Runtime_Uptime / 1000 -
micrometer:
process_uptime_seconds
-
-
ft:jvm_heap_used_bytes-
jmx/lowercase:
java_lang_memory_heapmemoryusage_used -
jmx/CamelCase:
java_lang_Memory_HeapMemoryUsage_used -
micrometer:
jvm_memory_used_bytes{area="heap"}
-
-
ft:jvm_heap_max_bytes-
jmx/lowercase:
java_lang_memory_heapmemoryusage_max -
jmx/CamelCase:
java_lang_Memory_HeapMemoryUsage_max -
micrometer:
jvm_memory_max_bytes{area="heap"}
-
-
ft:jvm_nonheap_used_bytes-
jmx:
java_lang_memory_nonheapmemoryusage_used/java_lang_Memory_NonHeapMemoryUsage_used -
micrometer:
jvm_memory_used_bytes{area="nonheap"}
-
-
ft:jvm_threads_live-
jmx:
java_lang_threading_threadcount/java_lang_Threading_ThreadCount -
micrometer:
jvm_threads_live_threads
-
-
ft:jvm_threads_daemon-
jmx:
java_lang_threading_daemonthreadcount/java_lang_Threading_DaemonThreadCount -
micrometer:
jvm_threads_daemon_threads
-
-
ft:jvm_threads_peak-
jmx:
java_lang_threading_peakthreadcount/java_lang_Threading_PeakThreadCount -
micrometer:
jvm_threads_peak_threads
-
-
ft:jvm_classes_loaded-
jmx:
java_lang_classloading_loadedclasscount/java_lang_ClassLoading_LoadedClassCount -
micrometer:
jvm_classes_loaded_classes
-
-
ft:jvm_gc_time_seconds_total-
jmx:
java_lang_garbagecollector_collectiontime / 1000(both spellings) -
micrometer:
jvm_gc_pause_seconds_sum
-
-
ft:jvm_gc_count_total-
jmx:
java_lang_garbagecollector_collectioncount -
micrometer:
jvm_gc_pause_seconds_count
-
-
ft:jvm_cpu_process_ratio-
jmx:
java_lang_operatingsystem_processcpuload/java_lang_OperatingSystem_ProcessCpuLoad -
micrometer:
process_cpu_usage
-
-
ft:jvm_open_file_descriptors-
jmx:
java_lang_operatingsystem_openfiledescriptorcount -
micrometer:
process_files_open_files
-
-
ft:pool_connections_active-
jmx:
hikaricp_active_connections -
micrometer:
hikaricp_connections_active
-
-
ft:pool_connections_idle-
jmx:
hikaricp_idle_connections -
micrometer:
hikaricp_connections_idle
-
-
ft:pool_connections_total-
jmx:
hikaricp_total_connections -
micrometer:
hikaricp_connections
-
-
ft:pool_connections_max-
jmx:
hikaricp_maximum_pool_size -
micrometer:
hikaricp_connections_max
-
-
ft:pool_connections_min-
jmx:
hikaricp_minimum_idle -
micrometer:
hikaricp_connections_min
-
-
ft:pool_threads_awaiting-
jmx:
hikaricp_threads_awaiting_connection -
micrometer:
hikaricp_connections_pending
-
-
ft:jvm_threads_started_total-
jmx:
java_lang_threading_totalstartedthreadcount/java_lang_Threading_TotalStartedThreadCount -
micrometer:
jvm_threads_started_threads_total
-
-
ft:jvm_classes_unloaded_total-
jmx:
java_lang_classloading_unloadedclasscount/java_lang_ClassLoading_UnloadedClassCount -
micrometer:
jvm_classes_unloaded_classes_total
-
Deliberately not mapped
These are queried by jmx-dashboard today and get no canonical name. Each is a host-level
reading that the JVM happens to report; node-exporter already scrapes the same host and is both
more accurate and unaffected by which JVM is running.
-
java_lang_OperatingSystem_TotalPhysicalMemorySize→node_memory_MemTotal_bytes -
java_lang_OperatingSystem_FreePhysicalMemorySize→node_memory_MemAvailable_bytes -
java_lang_OperatingSystem_TotalSwapSpaceSize→node_memory_SwapTotal_bytes -
java_lang_OperatingSystem_FreeSwapSpaceSize→node_memory_SwapFree_bytes -
java_lang_OperatingSystem_CommittedVirtualMemorySize→ drop (JVM address space, not useful next to the RSS that node-exporter and cAdvisor already show) -
java_lang_OperatingSystem_SystemCpuLoad→node_cpu_seconds_totalbased panel -
java_lang_OperatingSystem_AvailableProcessors→count(node_cpu_seconds_total) by (instance) -
java_lang_OperatingSystem_ProcessCpuTime→ covered byft:jvm_cpu_process_ratio -
java_lang_Runtime_StartTime→ covered byft:jvm_uptime_seconds -
java_lang_Threading_CurrentThreadCpuTime→ drop (per-thread sampling, no consumer) -
java_lang_Threading_TotalStartedThreadCount→ft:jvm_threads_started_total(was “drop, no panel reads it meaningfully” —acs.jsonandjmx-dashboardboth read it, and on a Micrometer-only installation both were blind) -
jvm_threads_deadlocked,jvm_threads_deadlocked_monitor→ these come from the JMX exporter’s own JVM collector, which is not enabled on the production exporters (0jvm_*names in the 110 captured). They have no producer today on either side; drop the panels rather than pretend.
Consequence for the plan
Task 2 must emit each canonical name from three groups, not two — lowercase JMX, CamelCase
JMX, Micrometer. The alternative (normalising the two JMX spellings at the exporter by fixing
lowercaseOutputName on one side) changes a producer on a production host and would silently
break whatever currently reads the CamelCase Hazelcast series, so it is not part of this plan.
It is worth a separate ticket.
Left on producer names on purpose (task 4b) — mostly closed, 19.09.2026
The deferral below said per-pool canonicalisation was “not free and no consumer
has asked for it”. A consumer asked: measured 19.09.2026 on the OpenShift acs
namespace and on grafrd, these panels are blind on every installation whose ACS
is a container.
-
jvm_memory_pool_bytes_used/max/_committed→ft:jvm_memory_used_bytes_by_pool,ft:jvm_memory_max_bytes_by_pool,ft:jvm_memory_committed_bytes_by_pool. Three producers, three spellings of the pool label, normalised ontopool: java-clientpool=, WildFly JMXjava_lang_memorypool_usage*{name=}, Micrometerjvm_memory_*_bytes{id=}. -
jvm_memory_bytes_committed→ft:jvm_memory_committed_bytes_by_area, the same shape as the used-memory name beside it. -
java_lang_ClassLoading_UnloadedClassCount,java_lang_Threading_TotalStartedThreadCount→ canonical, see the list above.
Still on producer names, and still deliberately:
-
jvm_info— a label-only series driving the JDK panel. Both the JMX exporter and Micrometer emit it with the samevendor/runtime/versionlabels (8 series in Kubernetes, measured), solabel_joinalready works on both and a canonical name would add nothing. -
java_lang_OperatingSystem_CommittedVirtualMemorySize— the “Physical memory” panel. Micrometer publishes no equivalent in any spelling, so the panel is JMX-only and now says so in its description.
What made this worth doing, stated once: the canonical rules are
producer-agnostic, but the dashboards were not. acs.json selected instances
with label_values(ft:jvm_heap_used_bytes{job="JmxExporter"}, instance) and
filtered every panel by the same job, and jmx-dashboard built its job list
from jvm_info{job="JmxExporter"} — so on the OpenShift cluster, where the ACS
publishes Micrometer under job="ftacs", the ACS dashboard offered 24 instances
and not one of them was an ACS. Measured through that cluster’s own Grafana on
19.09.2026, with the data present all along:
count(ft:jvm_heap_used_bytes{job="ftacs"}) = 2.
jmx-dashboard: the panels that never had data (DEV-3042, 02.10.2026)
Once the job list stopped excluding Micrometer, the panels that had always been
empty became visible. None of them lost data; their queries and the rules behind
them were the same in 1.6.9. Measured with every panel run through Grafana’s
/api/ds/query, before and after:
-
grafrd, ACS 6.4 (JMX exporter): 8 empty panels → 0.
-
grafrd, Spring services: 9 → 1 (Process Memory).
-
acs (OpenShift), ftacs and Spring services: 20 → 2 (Process Memory; SWAP Utilization, because the nodes have no swap).
-
acs, Hazelcast: 21 → 5 (JDK, Process Memory, JVM Memory committed, Memory pool, which its exporter does not publish on Kubernetes; and SWAP Utilization).
The causes, and what each panel reads now:
-
Host memory and CPU (Total RAM, Total SWAP, Memory and SWAP Utilization, CPU Cores, both System Load panels, and the host lines of Physical memory) asked for
node_memory_*{job="$job"}andnode_cpu_seconds_total{instance="$server"}. The first exists only underjob="node-exporter", and on Kubernetes the second never matches, because$serveris a pod address. They now read node-exporter on the JVM’s host: the one with the sameinstance(bare metal, Deployment Manager), or on Kubernetes the pod’s node throughkube_pod_info. This is the same join the ACS dashboard uses. -
JVM Memory Used [heap]/[nonheap] filtered
ft:jvm_heap_used_byteson anarealabel that no producer sets on that name. It now readsft:jvm_memory_used_bytes_by_area. Where an exporter has no per-area breakdown (Hazelcast on Kubernetes), it falls back toft:jvm_heap_used_bytes/ft:jvm_nonheap_used_bytes.$memareais a fixed list (heap, nonheap), and the Max line is drawn for heap only. -
JVM Memory Usage is now one panel, JVM Heap Usage: heap used as a share of max heap. Non-heap has no meaningful maximum (Micrometer reports -1).
-
Open File Descriptors read the JMX exporter’s
process_open_fds. It now readsft:jvm_open_file_descriptors, which all three producers feed. -
Every rate/increase panel used
[$interval]or[$rate_interval]. Theft:series are recording rules evaluated every 60 s, so on a 30 s scrape those windows held one sample and drew nothing. Every such panel now uses a fixed[5m], as the ACS dashboard already does. -
JVM Threading Threads no longer plots Started threads. Since the panel moved to the canonical name, that cumulative counter (1.46 M on an ftacs pod) is present for every producer, and it flattened Current/Daemon/Peak (~1.6 K) onto the floor. Started stays in the rate panel.
-
Process Memory stays JMX-only and says so: Micrometer publishes no resident or virtual memory.
Labelled canonical names (task 30, closed)
Two dashboards needed a label the aggregate rules deliberately drop, so the set gained three names that keep exactly one label each and nothing more:
-
ft:jvm_gc_time_seconds_by_collector/ft:jvm_gc_count_by_collector— keep the collector. Every producer spells it differently, measured on production: WildFly puts it inname, the Hazelcast exporter and Micrometer ingc, and Micrometer addsactionandcausewhich multiply the series. All three are normalised ontogc. -
ft:jvm_memory_used_bytes_by_area— keepsarea. Micrometer and the Hazelcast exporter carry that label; the WildFly JMX side has no such label at all, since heap and nonheap are separate metric names there, so it is synthesised.
The original reasoning, kept because it is why these names are narrow:
-
hazelcast-overviewasks for GC per collector —jvm_gc_collection_seconds_*withgc="G1 Old Generation". Both canonical GC rules aggregate that away (sum without (action, cause, gc)on the Micrometer side), so the selector would match nothing. Needsft:jvm_gc_time_seconds_by_collectoror the panels rewritten to the aggregate. -
ft-ui-servicesasks for memory byarea—sum by (area) (jvm_memory_used_bytes{…}). The canonical names split heap from nonheap by NAME rather than by label, so there is noareato group on. Needs either a labelled canonical name or two explicit series per panel.
Migrated in this pass: acs and system-dashboard-basic.
acs also selects the ACS JVM and nothing else, and that is the substantive
change. Its panels queried CamelCase java_lang_* filtered only by
job="JmxExporter", which on most hosts is the Hazelcast exporter: the ACS
dashboard showed Hazelcast’s JVM. The canonical series span every producer —
the ACS, Hazelcast and every Spring service — so the dashboard now requires
the ACS job (ftacs, acs-metrics or JmxExporter) with exporter_type
jvm, acs-metrics or none (Kubernetes sets none), and leaves out the
Hazelcast pods. CamelCase is not Hazelcast’s alone: some 6.4 hosts’ ACS
exporter emits it too, which is why ft:jvm_gc_*by_collector has a CamelCase
source guarded by unless against Hazelcast’s jvm_gc_collection_seconds*.
The other half of the contract: consumers (task 27, closed)
Everything above is the producer side — what the exporters emit. The mapping is only worth something if the queries actually name those metrics, and until this pass nothing checked that they did.
misc/testing/fixtures/metric-inventory.txt is the producer side written down:
10322 metric names captured 05.08.2026 from the live Prometheus of all three
environments, each prefixed by which of them emits it. Regenerate it with
misc/testing/scripts/capture-inventory.sh — never by hand, because a name
added to silence a finding removes the only reason the check means anything.
misc/testing/scripts/expr-coverage.sh extracts the metric names from every
dashboard expr and every alert expr (1127 expressions, including the Helm
chart’s rules when that repository is checked out alongside) and reports any
name with no producer. validate-all Test 40 runs it.
What it found
Five live queries named metrics that do not exist anywhere:
-
process_resident_memory_max_bytes, innodes-monitoringandsystem-dashboard. Producers emitprocess_resident_memory_bytesandprocess_virtual_memory_max_bytes; the crossed name has never existed. Both panels also carried a duplicated target pair, and the broken query was the mangled half of one — the working copy sat right beside it, so the panel drew a line and looked fine. The duplicate pair is removed. -
windows_cs_physical_memory_bytes,windows_os_physical_memory_free_bytesandwindows_os_virtual_memory_bytes. windows_exporter dropped thecsandoscollector prefixes in 0.20; the monitored Windows host emitswindows_memory_physical_total_bytes,windows_memory_physical_free_bytesandwindows_memory_commit_limit. Verified against on-prem: each replacement returns 1 series, each old name returns 0.
And two alert families that could never fire, both left in place because fixing them is a separate change:
-
hz_cluster_size— no producer, and no recording rule defines it either. Bothprometheus-grafana-stack/prometheus/hazelcast_rules.ymland the chart’shazelcast.ymlalert on it. -
probe_ssl_earliest_cert_expiry— the blackbox exporter is not deployed in any environment, so the certificate-expiry alerts inprometheus_rules.ymland the chart’splatform.ymlare decorative.
Why Hazelcast could not be fixed by renaming a query (task 32, closed)
Hazelcast embeds its randomly generated instance name in the metric name rather
than a label. The same three metrics appear under six different names across the
three environments — com_hazelcast_thirsty_margulis_memberCount,
com_hazelcast_laughing_lamport_memberCount,
com_hazelcast_compassionate_wilbur_memberCount and so on — and the name changes
on every restart, so no query could be written that survives one.
The canonical_hazelcast group in canonical_rules.yml extracts the instance
from name into an hz_instance label:
-
ft:hazelcast_cluster_members -
ft:hazelcast_running -
ft:hazelcast_cluster_safe -
ft:hazelcast_local_member_safe -
ft:hazelcast_executor_queue_size
Two details in those rules are load-bearing. The type matcher is what keeps
com_hazelcast_Metrics_queueSize out — an unrelated per-executor metric that
matches the same name regex, with 23 series on the QA stand and hundreds
in Kubernetes, every one of which would arrive labelled
hz_instance="Metrics". And the instance is extracted from the metric name
rather than read from the name label, because name means the instance on
HazelcastInstance and HazelcastInstance.PartitionServiceMBean but the
EXECUTOR on HazelcastInstance.ManagedExecutorService.
All four Hazelcast alerts were inert before this, each differently, and each convincing to read:
-
HazelcastClusterSizeChangedreadhz_cluster_size, which nothing defines. -
HazelcastNodeDownusedcount(…) < 1. On an empty selectorcount()returns no samples rather than 0, so the comparison filtered nothing in exactly the situation the alert existed for. It now usesabsent(). -
HazelcastHighPendingOpsreadcom_hazelcast_Metrics_completedPacketCount, which counts completed packets rather than pending ones and is flat at zero on production (98 series, max 0 over seven days). Had it ever moved, being cumulative, it would have fired permanently. It now reads the managed-executor queue depth. The chart had already spotted the first problem and replaced the metric withcom_hazelcast_Metrics_operationExecutorQueueSize, which no producer emits either. -
isClusterSafe— the reason this stack scrapes JMX over the native endpoint — was present on every instance and readable by nothing.HazelcastClusterUnsafenow alerts on it.
hazelcast_canonical_test.yml and hazelcast_alert_test.yml cover both halves,
feeding the instance names measured in each environment. The chart’s
hazelcast.yml is now byte-identical to hazelcast_rules.yml and the same
tests pass against it, which matters because the two copies had already drifted
into different broken states.
The remaining 24 absences
misc/testing/fixtures/metric-inventory-exceptions.txt lists every name a query
asks for that no producer emits, each with the reason. The largest group is
mysql.json: eleven query-cache and InnoDB-memory counters that MySQL 8 removed
outright, inherited from the upstream Percona dashboard written for MySQL 5.x.
Those panels are permanently empty on every database we monitor.
One dashboard set, two destinations (task 31, closed)
The dashboards shipped twice — from this repo for on-prem, and from
ft-charts-library/charts/ft-monitoring/dashboards for Kubernetes — and nothing
kept the two in step. Measured 05.08.2026: seven files with different queries,
drifting in both directions. Neither copy was simply the older one.
-
The chart was behind on the canonical names.
acs,hikari,hazelcast-overviewandjmx-dashboardstill queried one producer’s spelling, so the Kubernetes dashboards were blind to exactly what theft:names exist to fix. -
The chart was ahead on
mysql: six panels this repo lacked (InnoDB buffer pool, query throughput, full-scan and expensive-query signals, aperformance_schemadigest table) and the two Query Cache panels correctly deleted, MySQL 8 having removed the query cache. That version is now the one here, which also retired six entries from the coverage exceptions file.
misc/testing/scripts/sync-dashboards.sh makes this repo the source and copies
into the chart; --check reports drift and exits non-zero. validate-all Test 42
runs the check when an ft-charts-library checkout is present and only ever
warns — a suite that fails on someone else’s branch state is worse than the
drift it catches.
There was no environment reason for the copies to differ at all. Both sides
provision a Prometheus datasource with uid prometheus_uid, so the files can be
byte-identical; the ${datasource} references the chart had accumulated were
dangling in the eight dashboards that define no such variable.
The two that stay forked
Extended/ft-ui-services.json and system-dashboard-basic.json are skipped by
the sync. Their panels do not query different spellings of one metric — they
query different producers. On-prem reads host processes from process-exporter
(namedprocess_namegroup_*); the chart reads containers from cAdvisor
(container_*{namespace,pod}). A canonical name cannot reconcile those, because
a process on a host and a container in a pod are not the same measurement.
Logs/logs-overview.json exists only in the chart: it reads Loki, which on-prem
does not run.
Not taken back
jmx-dashboard has eight panels in the chart that this repo does not: Threads
current and Classes Loaded (three each — value, increase, rate), plus Start time
and Cpu Time. The first six duplicate panels already here on canonical names,
and all eight read a single producer spelling. Importing them would reintroduce
the blindness, and validate-all Test 35 would reject them.
branch release/1.7.1 · commit e70021b6498caa7f94c439e9d43703e4496f41e2 · page generated October 06, 2026