Jetty
The OpenTelemetry JMX Scraper collects 6 Jetty-specific metrics and 19
JVM metrics from Eclipse Jetty 9.4+ - thread-pool busy/idle/queue counts,
NIO selector activity, heap and non-heap memory, CPU utilization, class
loading, and buffer pools. Jetty keeps these in JMX MBeans with no native
metrics endpoint, and unlike Tomcat it does not register them by default,
so the jmx module must be enabled. The scraper connects over JMX/RMI,
converts the MBeans into OpenTelemetry metrics, and pushes them over OTLP
to the Collector. This guide enables JMX on Jetty, configures the scraper,
and ships metrics to base14 Scout.
Prerequisites
| Requirement | Minimum | Recommended |
|---|---|---|
| Eclipse Jetty | 9.4 | 12.0 |
| JMX Scraper | 1.48.0-alpha | 1.57.0-alpha |
| Java (scraper) | 11 | 17 |
| OTel Collector Contrib | 0.90.0 | 0.153.0 |
| base14 Scout | Any | - |
Before starting:
- Jetty must be reachable from the host running the JMX Scraper (JMX port, default 1099).
- Jetty's
jmxmodule must be enabled so the server registers its MBeans; without it you get only JVM metrics. - The JMX Scraper runs as a standalone Java process and needs its own JRE.
- A Scout account and OTLP endpoint.
- OTel Collector installed - see Docker Compose Setup.
The jetty rules are bundled in JMX Scraper 1.48.0-alpha and later; on
earlier builds only the JVM metrics surface. The metric names come from
the scraper's jetty rules, not from Jetty itself, so a Jetty version
change cannot rename or drop them - it can only leave a source MBean
absent.
What You'll Monitor
Metrics are grouped into three tiers by how you use them. Scrape Core always, alert on Operational, and reach for Diagnostic during an incident or capacity review.
The jetty target exposes no request counter, so there is no built-in
throughput or per-request latency metric. Use jetty.thread.busy.count
as the work proxy; per-request timing lives in your access logs or trace
path, not in these metrics.
Core - is it up and serving
| Metric | What it tells you |
|---|---|
jetty.thread.busy.count | Threads actively handling requests - the closest proxy for whether Jetty is doing work. |
jvm.memory.used | JVM memory in use. JMX exposes no up metric, so heap-in-use doubles as the process-alive and heap-health anchor. |
Operational - what to alert on
| Metric | What it tells you |
|---|---|
jetty.thread.queue.size | Jobs queued waiting for a free thread - backpressure / saturation. |
jetty.thread.limit | Max threads in the pool; the saturation denominator for busy.count. |
jvm.memory.limit | JVM memory ceiling; the saturation denominator for jvm.memory.used. |
jvm.cpu.recent_utilization | Recent process CPU utilization. |
jvm.thread.count | Total live JVM threads - a leak signal when it climbs. |
Diagnostic - for investigation and tuning
Higher cardinality; reach for these during an incident or capacity
review. In production you can drop this tier with a filter processor
and keep Core + Operational.
| Group | Metrics | When you reach for it |
|---|---|---|
| Thread-pool detail | jetty.thread.count, jetty.thread.idle.count | Pool composition - how the busy/idle split moves under load. |
| Connector I/O | jetty.select.count | NIO selector select calls; connector-level I/O pressure. |
| JVM memory detail | jvm.memory.committed, jvm.memory.init, jvm.memory.used_after_last_gc | Heap sizing and post-GC live-set growth. |
| JVM class loading | jvm.class.count, jvm.class.loaded, jvm.class.unloaded | Class-loader churn / leaks during redeploys. |
| JVM CPU / system | jvm.cpu.count, jvm.cpu.time, jvm.system.cpu.load_1m, jvm.system.cpu.utilization | Host-level CPU context behind recent_utilization. |
| JVM buffers / descriptors | jvm.buffer.count, jvm.buffer.memory.limit, jvm.buffer.memory.used, jvm.file_descriptor.count, jvm.file_descriptor.limit | Direct-buffer and file-descriptor exhaustion. |
Session metrics (jetty.session.count, jetty.session.created.count,
jetty.session.duration.sum) are defined by the scraper's jetty target
but stay silent on a bare server - their MBeans only register once a web
application with an active session cache is deployed. They surface
automatically as soon as a session-bearing context runs; nothing in the
config withholds them.
Full metric reference: OTel JMX Scraper Jetty rules.
Key Alerts to Configure
Threshold guidance for the most useful Core and Operational series. These are starting points; tune them to your workload.
| Metric | Warning | Critical | Why it matters |
|---|---|---|---|
jetty.thread.busy.count / jetty.thread.limit | Approaching the limit | Approaching 1.0 | All worker threads in use; new requests queue. Raise maxThreads or scale out. |
jetty.thread.queue.size | > 0 sustained | Growing | Requests waiting for a free thread - the pool is saturated. Investigate slow handlers or scale. |
jvm.memory.used / jvm.memory.limit | Approaching the limit | Approaching 1.0 | GC churn / OOM risk. Raise heap or reduce allocation. |
jvm.cpu.recent_utilization | Sustained high | Pinned | Process is CPU-bound. Scale out or profile hot paths. |
jvm.thread.count | Climbing vs baseline | Unbounded growth | Threads not being released; inspect thread dumps. |
Access Setup
Jetty JMX is disabled by default. Two steps are required: enable the
jmx module (registers Jetty MBeans) and configure remote JMX access
(opens a port for the scraper).
Enable JMX on Jetty
For standalone Jetty, enable the JMX module and add remote-access flags:
# Enable Jetty JMX MBean registration
java -jar $JETTY_HOME/start.jar --add-module=jmx
# Add remote JMX access flags to start.d
cat >> $JETTY_BASE/start.d/jmx-remote.ini << 'EOF'
--exec
-Dcom.sun.management.jmxremote.port=1099
-Dcom.sun.management.jmxremote.rmi.port=1099
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=<jetty-host>
EOF
Setting rmi.port to the same value as port keeps RMI from picking a
random second port, which simplifies firewall and Docker networking.
For Docker, the Jetty image needs a custom Dockerfile to enable the jmx
module at build time; the remote-access flags are passed via
JAVA_OPTIONS:
FROM jetty:12.0-jdk17
USER root
RUN java -jar "$JETTY_HOME/start.jar" --add-module=jmx
USER jetty
jetty:
build: ./jetty
hostname: jetty
environment:
JAVA_OPTIONS: >-
-Dcom.sun.management.jmxremote.port=1099
-Dcom.sun.management.jmxremote.rmi.port=1099
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=jetty
With authentication (production)
Unauthenticated JMX is fine on a trusted private network or inside a pod; expose it across hosts only with SSL and authentication enabled:
--exec
-Dcom.sun.management.jmxremote.port=1099
-Dcom.sun.management.jmxremote.rmi.port=1099
-Dcom.sun.management.jmxremote.ssl=true
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.password.file=/path/to/jmxremote.password
-Dcom.sun.management.jmxremote.access.file=/path/to/jmxremote.access
-Djava.rmi.server.hostname=<jetty-host>
The JMX Scraper connects to an authenticated server via the
OTEL_JMX_USERNAME and OTEL_JMX_PASSWORD environment variables. The
account needs read-only MBean access; no write operations are used.
Configuration
Jetty monitoring uses two components: the JMX Scraper (connects to Jetty, exports OTLP) and the OTel Collector (receives OTLP, ships to Scout).
Jetty (JMX:1099) ← JMX/RMI → JMX Scraper → OTLP → OTel Collector → Scout
JMX Scraper
Download the scraper JAR from
Maven Central
and run it against the jvm,jetty target systems:
OTEL_JMX_SERVICE_URL=service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi \
OTEL_JMX_TARGET_SYSTEM=jvm,jetty \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
OTEL_METRIC_EXPORT_INTERVAL=10000 \
java -jar opentelemetry-jmx-scraper-1.57.0-alpha.jar
For a managed install, move the JAR to a permanent location and run it under systemd:
sudo tee /etc/systemd/system/otel-jmx-scraper.service > /dev/null <<'EOF'
[Unit]
Description=OpenTelemetry JMX Scraper for Jetty
After=network.target jetty.service
[Service]
Type=simple
Environment=OTEL_JMX_SERVICE_URL=service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi
Environment=OTEL_JMX_TARGET_SYSTEM=jvm,jetty
Environment=OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
Environment=OTEL_METRIC_EXPORT_INTERVAL=10000
ExecStart=/usr/bin/java -jar /opt/otel/opentelemetry-jmx-scraper-1.57.0-alpha.jar
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now otel-jmx-scraper
For Docker, build a small image with the scraper JAR:
FROM eclipse-temurin:17-jre
ARG SCRAPER_VERSION=1.57.0-alpha # Update to match your target version
ADD https://repo1.maven.org/maven2/io/opentelemetry/contrib/opentelemetry-jmx-scraper/${SCRAPER_VERSION}/opentelemetry-jmx-scraper-${SCRAPER_VERSION}.jar /opt/scraper.jar
ENTRYPOINT ["java", "-jar", "/opt/scraper.jar"]
OTel Collector
The Collector receives metrics from the JMX Scraper over OTLP/gRPC:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
resource:
attributes:
- key: deployment.environment.name
value: ${env:ENVIRONMENT}
action: upsert
- key: service.name
value: ${env:SERVICE_NAME}
action: upsert
batch:
timeout: 10s
send_batch_size: 1024
exporters:
otlphttp/b14:
endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT}
tls:
insecure_skip_verify: true
service:
pipelines:
metrics:
receivers: [otlp]
processors: [resource, batch]
exporters: [otlphttp/b14]
To control metric volume in production, drop the Diagnostic tier with a
filter processor on the metrics pipeline while keeping the Core and
Operational series.
Semconv version note:
deployment.environment.nameis the current OTel attribute (semantic conventions v1.27+, stable in v1.40.0). The legacydeployment.environmentis still accepted by Scout for backward compatibility, but new configs should emit the dotted form.
Environment Variables
# JMX Scraper
OTEL_JMX_SERVICE_URL=service:jmx:rmi:///jndi/rmi://jetty:1099/jmxrmi
OTEL_JMX_TARGET_SYSTEM=jvm,jetty
OTEL_METRIC_EXPORT_INTERVAL=10000
# OTEL_JMX_USERNAME=monitor # Uncomment for authenticated JMX
# OTEL_JMX_PASSWORD=your_password # Uncomment for authenticated JMX
# OTel Collector
ENVIRONMENT=your_environment
SERVICE_NAME=your_service_name
OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-tenant>.base14.io
Docker Compose
Full working example with all three components:
services:
jetty:
build: ./jetty
hostname: jetty
ports:
- "8080:8080"
- "1099:1099"
environment:
JAVA_OPTIONS: >-
-Dcom.sun.management.jmxremote.port=1099
-Dcom.sun.management.jmxremote.rmi.port=1099
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=jetty
healthcheck:
test: ["CMD-SHELL", "curl -so /dev/null http://localhost:8080/ || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
jmx-scraper:
build: ./jmx-scraper
environment:
OTEL_JMX_SERVICE_URL: ${OTEL_JMX_SERVICE_URL}
OTEL_JMX_TARGET_SYSTEM: ${OTEL_JMX_TARGET_SYSTEM}
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_METRIC_EXPORT_INTERVAL: ${OTEL_METRIC_EXPORT_INTERVAL}
depends_on:
jetty:
condition: service_healthy
otel-collector:
image: otel/opentelemetry-collector-contrib:0.153.0
container_name: otel-collector
volumes:
- ./config/otel-collector.yaml:/etc/otelcol-contrib/config.yaml:ro
depends_on:
- jetty
Verify the Setup
Start the Collector and check for metrics within 60 seconds:
# Check JMX Scraper logs for a successful connection
docker logs jetty-telemetry-jmx-scraper-1 2>&1 | head -10
# Confirm Jetty started with the jmx module enabled
docker logs jetty 2>&1 | grep "jmx"
# Check Collector logs for Jetty metrics
docker logs otel-collector 2>&1 | grep "jetty"
# Generate traffic so the thread-pool counters move
curl -s http://localhost:8080/ > /dev/null
You should see the jvm.* and jetty.* metrics in the Collector debug
output and, shortly after, in Scout.
Troubleshooting
JMX connection refused
Cause: The JMX Scraper cannot reach Jetty's JMX port.
Fix:
- Verify Jetty is running:
docker ps | grep jetty. - Confirm remote JMX is enabled - check that
-Dcom.sun.management.jmxremote.port=1099is inJAVA_OPTIONSorstart.d/jmx-remote.ini. - Verify the port matches between Jetty and the scraper's
OTEL_JMX_SERVICE_URL. - In Docker, ensure
hostnameis set on the Jetty container and matches-Djava.rmi.server.hostname.
Only JVM metrics, no Jetty metrics
Cause: Jetty's jmx module is not enabled, so its components are not
registered as MBeans.
Fix:
- Enable the JMX module:
java -jar start.jar --add-module=jmx. - In Docker, use the custom Dockerfile that runs
--add-module=jmxat build time. - Verify
OTEL_JMX_TARGET_SYSTEMincludesjetty.
Requests are slow or piling up
Cause: The thread pool is saturated, or handlers are slow.
Look at: jetty.thread.queue.size (requests waiting for a thread)
against jetty.thread.busy.count / jetty.thread.limit. A queue above
zero with busy near the limit means the pool is full. The Diagnostic
jetty.thread.idle.count confirms there are no spare threads, and
jetty.select.count surfaces connector-level I/O pressure.
Fix:
- Raise
maxThreadsor add Jetty capacity if the queue is sustained. - Profile slow handlers if busy threads stay high without queue relief.
Heap pressure or rising CPU
Cause: Allocation churn, a memory leak, or CPU-bound work.
Look at: jvm.memory.used against jvm.memory.limit, plus the
Diagnostic jvm.memory.used_after_last_gc - a climbing post-GC live set
points to a leak rather than transient churn. jvm.system.cpu.utilization
and jvm.cpu.time give the host-level CPU context behind
jvm.cpu.recent_utilization.
Fix:
- Raise heap or reduce allocation if used approaches the limit.
- Inspect thread dumps if
jvm.thread.countclimbs without bound.
Session metrics missing
Cause: jetty.session.count, jetty.session.created.count, and
jetty.session.duration.sum come from session-cache MBeans that only
register once a web application with active sessions is deployed.
Fix:
- Deploy a web application that uses sessions. The MBeans register per context and the metrics surface automatically.
No metrics appearing in Scout
Cause: Metrics are collected but not exported.
Fix:
- Check Collector logs for export errors:
docker logs otel-collector. - Verify
OTEL_EXPORTER_OTLP_ENDPOINTis set correctly. - Confirm the pipeline includes both the OTLP receiver and the exporter.
FAQ
Why do I need the JMX Scraper instead of a Jetty receiver?
Jetty has no native metrics endpoint and the Collector has no Jetty
receiver. Jetty publishes its statistics as JMX MBeans, and the
OpenTelemetry JMX Scraper reads them over JMX/RMI, maps them to OTel
metrics with the jvm,jetty target rules, and pushes OTLP to the
Collector.
Does this work with embedded Jetty (Spring Boot / Dropwizard)?
Yes. Spring Boot and Dropwizard embed Jetty and register its MBeans when
JMX is enabled (spring.jmx.enabled=true on Spring Boot). Add the same
-Dcom.sun.management.jmxremote.* flags to the application JVM and point
the scraper at it.
Why is there no request-rate or latency metric?
The scraper's jetty target exposes no request counter, so throughput
and per-request timing are not in this metric surface. Use
jetty.thread.busy.count as a work proxy; per-request timing lives in
your access logs or trace path.
How do I monitor multiple Jetty instances?
Run one JMX Scraper per instance, each with a different
OTEL_JMX_SERVICE_URL, all exporting to the same Collector:
jmx-scraper-primary:
environment:
OTEL_JMX_SERVICE_URL: service:jmx:rmi:///jndi/rmi://jetty-1:1099/jmxrmi
OTEL_JMX_TARGET_SYSTEM: jvm,jetty
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
jmx-scraper-replica:
environment:
OTEL_JMX_SERVICE_URL: service:jmx:rmi:///jndi/rmi://jetty-2:1099/jmxrmi
OTEL_JMX_TARGET_SYSTEM: jvm,jetty
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
Does this work with Jetty in Kubernetes?
Yes. Run the JMX Scraper as a sidecar in the same pod and set
OTEL_JMX_SERVICE_URL to
service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi - both containers
share the pod network, so no firewall rules are needed for intra-pod
communication.
Related Guides
- JMX Metrics Guide - Compare the JMX Scraper and the JMX Exporter.
- OTel Collector Configuration - Advanced collector configuration.
- Docker Compose Setup - Run the Collector locally.
- Kubernetes Helm Setup - Production deployment.
- Tomcat Monitoring - Another Java application server.
- Creating Alerts - Alert on Jetty metrics.
What's Next?
- Create Dashboards: Explore pre-built dashboards or build your own. See Create Your First Dashboard.
- Monitor More Components: Add monitoring for Tomcat, Nginx, and other web servers.
- Fine-tune Collection: Drop the Diagnostic tier in production with a
filterprocessor to control volume; keep it available for incident investigation.