Skip to main content

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

RequirementMinimumRecommended
Eclipse Jetty9.412.0
JMX Scraper1.48.0-alpha1.57.0-alpha
Java (scraper)1117
OTel Collector Contrib0.90.00.153.0
base14 ScoutAny-

Before starting:

  • Jetty must be reachable from the host running the JMX Scraper (JMX port, default 1099).
  • Jetty's jmx module 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

MetricWhat it tells you
jetty.thread.busy.countThreads actively handling requests - the closest proxy for whether Jetty is doing work.
jvm.memory.usedJVM 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

MetricWhat it tells you
jetty.thread.queue.sizeJobs queued waiting for a free thread - backpressure / saturation.
jetty.thread.limitMax threads in the pool; the saturation denominator for busy.count.
jvm.memory.limitJVM memory ceiling; the saturation denominator for jvm.memory.used.
jvm.cpu.recent_utilizationRecent process CPU utilization.
jvm.thread.countTotal 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.

GroupMetricsWhen you reach for it
Thread-pool detailjetty.thread.count, jetty.thread.idle.countPool composition - how the busy/idle split moves under load.
Connector I/Ojetty.select.countNIO selector select calls; connector-level I/O pressure.
JVM memory detailjvm.memory.committed, jvm.memory.init, jvm.memory.used_after_last_gcHeap sizing and post-GC live-set growth.
JVM class loadingjvm.class.count, jvm.class.loaded, jvm.class.unloadedClass-loader churn / leaks during redeploys.
JVM CPU / systemjvm.cpu.count, jvm.cpu.time, jvm.system.cpu.load_1m, jvm.system.cpu.utilizationHost-level CPU context behind recent_utilization.
JVM buffers / descriptorsjvm.buffer.count, jvm.buffer.memory.limit, jvm.buffer.memory.used, jvm.file_descriptor.count, jvm.file_descriptor.limitDirect-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.

MetricWarningCriticalWhy it matters
jetty.thread.busy.count / jetty.thread.limitApproaching the limitApproaching 1.0All worker threads in use; new requests queue. Raise maxThreads or scale out.
jetty.thread.queue.size> 0 sustainedGrowingRequests waiting for a free thread - the pool is saturated. Investigate slow handlers or scale.
jvm.memory.used / jvm.memory.limitApproaching the limitApproaching 1.0GC churn / OOM risk. Raise heap or reduce allocation.
jvm.cpu.recent_utilizationSustained highPinnedProcess is CPU-bound. Scale out or profile hot paths.
jvm.thread.countClimbing vs baselineUnbounded growthThreads 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 module and remote access
# 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:

jetty/Dockerfile
FROM jetty:12.0-jdk17

USER root
RUN java -jar "$JETTY_HOME/start.jar" --add-module=jmx
USER jetty
docker-compose.yaml (Jetty service)
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:

start.d/jmx-remote.ini (authenticated)
--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:

Run the JMX Scraper
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:

/etc/systemd/system/otel-jmx-scraper.service
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:

jmx-scraper/Dockerfile
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:

config/otel-collector.yaml
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.name is the current OTel attribute (semantic conventions v1.27+, stable in v1.40.0). The legacy deployment.environment is still accepted by Scout for backward compatibility, but new configs should emit the dotted form.

Environment Variables

.env
# 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:

docker-compose.yaml
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:

Verify metrics collection
# 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:

  1. Verify Jetty is running: docker ps | grep jetty.
  2. Confirm remote JMX is enabled - check that -Dcom.sun.management.jmxremote.port=1099 is in JAVA_OPTIONS or start.d/jmx-remote.ini.
  3. Verify the port matches between Jetty and the scraper's OTEL_JMX_SERVICE_URL.
  4. In Docker, ensure hostname is 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:

  1. Enable the JMX module: java -jar start.jar --add-module=jmx.
  2. In Docker, use the custom Dockerfile that runs --add-module=jmx at build time.
  3. Verify OTEL_JMX_TARGET_SYSTEM includes jetty.

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:

  1. Raise maxThreads or add Jetty capacity if the queue is sustained.
  2. 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:

  1. Raise heap or reduce allocation if used approaches the limit.
  2. Inspect thread dumps if jvm.thread.count climbs 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:

  1. 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:

  1. Check Collector logs for export errors: docker logs otel-collector.
  2. Verify OTEL_EXPORTER_OTLP_ENDPOINT is set correctly.
  3. 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:

docker-compose.yaml (multiple instances)
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.

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 filter processor to control volume; keep it available for incident investigation.
Was this page helpful?