Exporting GeoServer Metrics to Prometheus
This guide shows how to expose GeoServer’s JVM, servlet-container, and OGC request metrics to Prometheus by running the JMX exporter as a Java agent, so heap pressure and slow GetMap renders become scrapeable time series.
It is a hands-on companion to Monitoring and Observability for Geospatial Portals and sits within the broader Infrastructure Orchestration & Configuration Management practice; read the parent guide first if you need the rationale for which GeoServer signals matter and why the RED method frames the request path. Here the focus is purely mechanical: attach the exporter, whitelist the right beans, expose a port, and confirm the series land in Prometheus.
Prerequisites
- GeoServer 2.22+ running in Tomcat or the official
docker.osgeo.org/geoserverimage, with access to setJAVA_OPTSorCATALINA_OPTS. - The
jmx_prometheus_javaagentJAR (0.20.0 or newer) from the Prometheus JMX exporter project, placed on a volume the GeoServer container can read. - Prometheus 2.45+, or the Prometheus Operator (
kube-prometheus-stack) if you scrape via aServiceMonitor. - Permission to edit the GeoServer Deployment/Pod spec and to add a scrape target or
ServiceMonitorin the monitoring namespace. - Network reachability from Prometheus to the GeoServer metrics port (default
9404), not blocked by aNetworkPolicy.
The topology below shows the agent living inside the GeoServer JVM, translating JMX beans into a Prometheus exposition endpoint that Prometheus scrapes and Grafana reads.
Step-by-step implementation
1. Place the agent and write its config
The JMX exporter runs in-process as a -javaagent, so it needs no separate JVM and sees every MBean GeoServer publishes. Mount the JAR and a config file into the container. The config below whitelists JVM, Tomcat thread-pool, and GeoServer request beans and drops everything else, which keeps cardinality bounded.
# jmx-exporter-config.yaml — mounted at /opt/jmx/config.yaml
startDelaySeconds: 0
lowercaseOutputName: true
lowercaseOutputLabelNames: true
# Only export the beans we actually chart; everything else is ignored.
whitelistObjectNames:
- "java.lang:type=Memory,*"
- "java.lang:type=GarbageCollector,*"
- "java.lang:type=Threading,*"
- "Catalina:type=ThreadPool,*"
- "Catalina:type=GlobalRequestProcessor,*"
- "org.geoserver:*"
rules:
# Tomcat request processor -> request rate + latency labels
- pattern: 'Catalina<type=GlobalRequestProcessor, name="(.+)"><>(\w+):'
name: geoserver_tomcat_$2
labels:
processor: "$1"
type: COUNTER
# Catch-all for remaining whitelisted beans
- pattern: ".*"
Keeping the config as a file rather than inline flags means the whitelist is version-controlled with the rest of the deployment, the same parity discipline described in Environment Parity in Geospatial CI Pipelines.
2. Attach the agent to the GeoServer JVM
Add the agent to JAVA_OPTS. The argument is port:configfile — the port the exporter listens on and the path to the config from step 1.
# Appended to CATALINA_OPTS / JAVA_OPTS for the GeoServer process
export JAVA_OPTS="$JAVA_OPTS \
-javaagent:/opt/jmx/jmx_prometheus_javaagent-0.20.0.jar=9404:/opt/jmx/config.yaml"
In Kubernetes, set this through the pod’s environment and mount the JAR and config from a ConfigMap and an init-container-populated volume:
env:
- name: EXTRA_JAVA_OPTS
value: "-javaagent:/opt/jmx/jmx_prometheus_javaagent-0.20.0.jar=9404:/opt/jmx/config.yaml"
3. Expose the metrics port
Declare the port on the container and on the Service so Prometheus has a stable target. Give it a named port (metrics) — the ServiceMonitor in step 4 selects it by name.
apiVersion: v1
kind: Service
metadata:
name: geoserver
namespace: geoportal
labels:
app: geoserver
spec:
selector:
app: geoserver
ports:
- name: http
port: 8080
targetPort: 8080
- name: metrics
port: 9404
targetPort: 9404
4. Add a Prometheus scrape target
If you run plain Prometheus, add a scrape_config:
scrape_configs:
- job_name: geoserver
metrics_path: /metrics
scrape_interval: 15s
static_configs:
- targets: ["geoserver.geoportal.svc.cluster.local:9404"]
labels:
component: geoserver
If you run the Prometheus Operator, ship a ServiceMonitor instead — it selects the Service by label and the port by name:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: geoserver
namespace: geoportal
labels:
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: geoserver
endpoints:
- port: metrics
interval: 15s
path: /metrics
5. Import a Grafana panel
With the series flowing, add a panel that reads GeoServer heap headroom and a companion that reads request latency. Grafana panels are JSON; the fragment below defines a single time-series panel targeting the exporter’s heap metric.
{
"title": "GeoServer JVM heap used",
"type": "timeseries",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [
{
"expr": "jvm_memory_bytes_used{area=\"heap\",component=\"geoserver\"}",
"legendFormat": "heap used"
}
],
"fieldConfig": { "defaults": { "unit": "bytes" } }
}
Pair it with a latency panel using histogram_quantile(0.99, sum by (le) (rate(geoserver_tomcat_requestprocessingtime_bucket[5m]))) so heap pressure and slow renders sit side by side on the GeoServer board described in the parent guide.
Verification
Confirm each hop before trusting the dashboard.
# 1. The exporter is serving metrics from inside the pod
kubectl exec -n geoportal deploy/geoserver -- \
curl -s localhost:9404/metrics | grep -c jvm_memory_bytes_used
# expect: a non-zero count (heap gauges present)
# 2. Key GeoServer/Tomcat series are exposed, not just JVM defaults
kubectl exec -n geoportal deploy/geoserver -- \
curl -s localhost:9404/metrics | grep geoserver_tomcat_
# 3. Prometheus considers the target up
curl -s 'http://prometheus.geoportal:9090/api/v1/query?query=up{job="geoserver"}' \
| grep -o '"value":\[[^]]*\]'
# expect: ... ,"1"] (1 = target healthy)
# 4. A real request metric increments after driving traffic
curl -s 'http://prometheus.geoportal:9090/api/v1/query?query=jvm_threads_current{component="geoserver"}'
A non-zero heap gauge and an up value of 1 confirm the agent is attached and Prometheus is scraping. If geoserver_tomcat_ series are missing while JVM series appear, the whitelist or the rule pattern is the culprit, not the scrape.
Troubleshooting matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| GeoServer fails to start after adding the agent | Wrong JAR path or Java version mismatch in -javaagent |
Verify the path exists in the container and the JAR matches the JVM major version; check the container startup log |
/metrics returns connection refused |
Agent argument malformed — missing port:configfile form |
Confirm the flag is =9404:/opt/jmx/config.yaml, not two separate args |
Only jvm_* series appear, no geoserver_* |
whitelistObjectNames omits the GeoServer/Catalina beans |
Add the Catalina:* and org.geoserver:* object names and reload |
Prometheus target shows down |
NetworkPolicy blocks port 9404, or wrong Service port name |
Allow ingress from the monitoring namespace to 9404; confirm the metrics port name matches the ServiceMonitor |
| Metrics present but cardinality explodes, Prometheus RAM climbs | A rule maps a high-cardinality label (per-layer or per-URL) | Tighten the pattern rules; drop labels that carry unbounded values |
histogram_quantile returns empty |
The latency metric is a gauge, not a histogram with _bucket series |
Confirm the bean exposes buckets; otherwise chart the gauge directly |
Related
- Monitoring and Observability for Geospatial Portals — the full signal model and where GeoServer metrics fit the RED view.
- Alerting on OGC Endpoint SLA Breaches — turning these request series into availability and latency alerts.
- Environment Parity in Geospatial CI Pipelines — keeping the exporter config version-controlled across environments.
Up one level: Monitoring and Observability for Geospatial Portals.