Two unrelated things: a Dockerfile that packages ERSAP into a runnable image,
and a docker-compose.yml that runs the Prometheus + Grafana monitoring stack.
docker/
├── Dockerfile packages ERSAP itself into a runnable image
├── hooks/ Docker Hub automated-build hooks (build the image above)
└── observability/ Prometheus + Grafana stack that visualizes ERSAP's metrics
They do not depend on each other. observability/ doesn't run any ERSAP code —
it consumes metrics published by a PrometheusExporter process running somewhere
— and you don't need the ERSAP image built to run it.
Builds a runnable ERSAP container using a multi-stage build:
- Stage 1 (
build) —eclipse-temurin:17-jdk-jammy, copies the source in, runs./gradlew build checkthen./gradlew deploy: the same build/deploy path the rootREADME.mdassumes (ERSAP_HOME,./gradlew deploy). - Stage 2 (final image) —
eclipse-temurin:17-jre-jammy, just the JRE, no build toolchain. Only the built${ERSAP_HOME}tree is copied out of stage 1, so the JDK/Gradle/source used to build it never end up in the shipped image.
It exposes:
7771-7775— the DPE ports each pipeline node binds.9095— the PrometheusExporter's/metricsport, only relevant if this image is used to run the exporter.
Volumes are declared for data/input, data/output, and log, so a container's
I/O isn't trapped inside the container filesystem.
hooks/build and hooks/post_push are Docker Hub automated-build hooks —
Docker Hub calls these scripts (not you, manually) when it auto-builds and pushes
an image on a repo push. build runs the two-stage docker build (tagging the
build stage as a dev/debug image, and the final image separately); post_push
pushes that dev-stage image under a derived tag. This is legacy Docker Hub CI
plumbing — unrelated to anything you'd run by hand.
The Dockerfile does COPY . ., so the build context must be the repo root,
not docker/ — run this from the top of the repository:
docker build -t ersap-java -f docker/Dockerfile .To build only the intermediate JDK/build stage (useful for debugging the build itself, or as a dev image with the full toolchain still present):
docker build --target build -t ersap-java-dev -f docker/Dockerfile .The image has no ENTRYPOINT/CMD — you supply the command, exactly as you
would on a bare-metal install with ERSAP_HOME already on PATH. For example,
to start a DPE:
docker run --rm -it \
-p 7771-7775:7771-7775 \
-v "$PWD/data/input:/usr/local/ersap/data/input" \
-v "$PWD/data/output:/usr/local/ersap/data/output" \
-v "$PWD/log:/usr/local/ersap/log" \
ersap-java \
j_dpe --host 0.0.0.0 --port 7771 --session mydemo-p 7771-7775:7771-7775publishes the DPE port range the imageEXPOSEs.- The three
-vmounts map the declared volumes to host directories, so results and logs survive after the container exits. - Swap the trailing command for whatever ERSAP entry point you need —
ersap-shell,j_dpe, or thePrometheusExporter(add-p 9095:9095if you run the exporter this way).
To open a shell in the container instead of running ERSAP directly:
docker run --rm -it --entrypoint bash ersap-javaDPEs → Monitor FE (:9000) → PrometheusExporter (:9095/metrics) → Prometheus (:9090) → Grafana (:3000)
PrometheusExporter subscribes to the Monitor FE over xMsg/ZeroMQ, converts
every DPE report and engine user-metric into Prometheus gauges/counters, and
serves them on /metrics. Prometheus scrapes that endpoint on a schedule.
Grafana queries Prometheus and renders the dashboards.
| Path | What it does |
|---|---|
observability/docker-compose.yml |
runs prom/prometheus:v2.53.0 and grafana/grafana:11.1.0 with named volumes |
observability/prometheus/prometheus.yml |
scrape config — tells Prometheus which hosts to poll and how often |
observability/grafana/provisioning/datasources/datasource.yml |
auto-registers the Prometheus data source (UID prometheus, URL http://prometheus:9090) |
observability/grafana/provisioning/dashboards/dashboards.yml |
tells Grafana to load any .json from the dashboards folder, re-checked every 30 s |
observability/grafana/dashboards/ersap-overview.json |
the ERSAP Overview dashboard (12 panels) |
- Docker and Docker Compose on the host running this stack.
PrometheusExporteralready running somewhere reachable — seeREADME.mdfor how to start it against a Monitor FE. Note its host and--prometheus-port(default9095).- Network path from this host to
<exporter-host>:<prometheus-port>. If they're on different networks, that's a firewall/VPN problem to solve first — nothing in this stack can work around unreachable targets. - Start the exporter with
--export-timestampsfor the "DPEs seen in last 30s" panel to work (see What's on the dashboard).
-
Copy
observability/to the monitoring host (scp -r,rsync, or a sparsegit clone). -
Edit
prometheus/prometheus.ymland replace the placeholder target with the real<exporter-host>:<prometheus-port>:static_configs: - targets: ["<exporter-host>:9095"] labels: session: "prod" # match --session the DPEs were started with, or remove
-
Start the stack — nothing to build, both images are pulled from Docker Hub:
cd observability docker compose up -d -
Confirm Prometheus is scraping the exporter:
http://<host>:9090/targets # ersap-monitor job should show UPIf it shows DOWN, run
curl http://<exporter-host>:9095/metricsdirectly from the monitoring host first — that isolates a Prometheus config problem from a network reachability problem. -
Open Grafana — the data source and ERSAP Overview dashboard are auto-provisioned on startup, nothing to import:
http://<host>:3000 # admin / changeme — change the password on first login
Any component that exposes a Prometheus-format /metrics endpoint can be
scraped alongside the ERSAP exporter — no code changes to ERSAP required.
Edit prometheus/prometheus.yml and append a job under scrape_configs. The
file ships with one job:
scrape_configs:
- job_name: ersap-monitor
metrics_path: /metrics
scrape_interval: 15s
static_configs:
- targets: ["monitoring-host.example.org:9095"]
labels:
session: "prod"Minimal entry — only job_name and targets are required:
- job_name: my-exporter
static_configs:
- targets: ["<host>:<port>"] # e.g. "10.0.0.5:8000" or "localhost:9200"metrics_path defaults to /metrics and scrape_interval inherits the global
15s, so nothing else is needed for a plain HTTP endpoint on any port.
Full annotated entry:
- job_name: my-exporter
metrics_path: /metrics # default; omit if unchanged
scrape_interval: 15s # omit to inherit the global 15s
scrape_timeout: 10s
static_configs:
- targets:
- "<host>:<port>"
labels:
pipeline: "mypipe" # any labels added to every series from this target
# Only needed for Basic Auth endpoints:
# basic_auth:
# username: "user"
# password: "secret"
# Only needed for HTTPS endpoints:
# scheme: https
# tls_config:
# insecure_skip_verify: truejob_name must be unique across all entries.
Reload without restarting the stack:
curl -X POST http://localhost:9090/-/reloadIf the reload is rejected, check docker compose logs prometheus for a parse
error. Alternatively, docker compose restart prometheus also picks up the
change.
Verify the new target is UP at http://localhost:9090/targets. The new
job's metrics are immediately queryable in Grafana — see
§ 3 Grafana configuration for how to add panels.
- Total processed events, failure rate, DPEs seen in the last 30 s, average execution time (top stat row)
- Processing rate by service, successful vs. failed events
- Error rate by service, average execution time by service
- DPE CPU usage, DPE memory usage
- Network bytes sent/received, shared-memory reads/writes
All panels are filterable by $session and $dpe dropdowns at the top.
"DPEs seen in last 30 s" needs --export-timestamps. That panel queries
ersap_metric_last_update_timestamp_seconds, which the exporter only emits when
started with --export-timestamps. Without that flag, a dead DPE's last known
values sit there indefinitely with no way to tell it's stale.
Not available yet: p50/p95/p99 latency. The exporter only sees the periodic DPE-level report, which carries a cumulative execution-time sum — enough for an average, not a distribution.
docker compose down # stop, keep Prometheus TSDB and Grafana state volumes
docker compose down -v # stop and delete volumesPrometheus retains data for 30 days (--storage.tsdb.retention.time=30d in
docker-compose.yml). Adjust before running long-term.
prometheus/prometheus.yml — tells Prometheus where to scrape. Editing
this changes what data exists to graph; it does not change what a dashboard
looks like. See § Adding an external /metrics endpoint
for how to add a new scrape target.
grafana/provisioning/ — Grafana bootstrap plumbing:
| File | What it does |
|---|---|
datasources/datasource.yml |
registers the Prometheus data source (UID prometheus, URL http://prometheus:9090) so dashboards can reference it without a hard-coded numeric ID |
dashboards/dashboards.yml |
tells Grafana to load any .json it finds in the dashboards folder and re-check every 30 s |
You should not need to edit either file unless you add a second data source or change the dashboard folder.
grafana/dashboards/ersap-overview.json — the actual dashboard: every
panel, every PromQL query, every title, every layout position. This is the file
to edit when you want to change what is graphed or add new panels.
The dashboard has 12 panels on a 24-unit-wide grid. Row 0 is the top.
Top row — stat tiles (y=0, h=4)
| id | Title | x | w | PromQL | Unit |
|---|---|---|---|---|---|
| 1 | Total processed events | 0 | 6 | sum(ersap_service_requests_total{session=~"$session", dpe=~"$dpe"}) |
count |
| 2 | Failure rate | 6 | 6 | 100 * sum(rate(ersap_service_failures_total[5m])) / clamp_min(sum(rate(ersap_service_requests_total[5m])), 1e-9) |
percent |
| 3 | DPEs seen in last 30 s | 12 | 6 | count(time() - ersap_metric_last_update_timestamp_seconds{metric="ersap_dpe_cpu_usage_percent",...} < 30) |
count |
| 4 | Average execution time | 18 | 6 | sum(rate(ersap_service_execution_time_seconds_total[5m])) / clamp_min(sum(rate(ersap_service_requests_total[5m])), 1e-9) |
seconds |
Panel 2 has colour thresholds: green → orange at 1 % → red at 5 %.
Panel 3 requires --export-timestamps.
Row 1 — throughput and success/failure (y=4, h=8)
| id | Title | x | w | PromQL |
|---|---|---|---|---|
| 5 | Processing rate by service | 0 | 12 | sum by (dpe, container, service) (rate(ersap_service_requests_total[5m])) |
| 6 | Successful vs. failed events | 12 | 12 | success: sum(rate(...requests...)) - sum(rate(...failures...)) / failure: sum(rate(...failures...)) |
Row 2 — error rate and execution time per service (y=12, h=8)
| id | Title | x | w | PromQL |
|---|---|---|---|---|
| 7 | Error rate by service | 0 | 12 | 100 * sum by (...) (rate(failures[5m])) / clamp_min(sum by (...) (rate(requests[5m])), 1e-9) |
| 8 | Average execution time by service | 12 | 12 | sum by (...) (rate(exec_time[5m])) / clamp_min(sum by (...) (rate(requests[5m])), 1e-9) |
Row 3 — DPE system metrics (y=20, h=8)
| id | Title | x | w | PromQL | Unit |
|---|---|---|---|---|---|
| 9 | DPE CPU usage | 0 | 12 | ersap_dpe_cpu_usage_percent{session=~"$session", dpe=~"$dpe"} |
percent |
| 10 | DPE memory usage | 12 | 12 | ersap_dpe_memory_usage_bytes{session=~"$session", dpe=~"$dpe"} |
bytes |
Row 4 — network and shared memory (y=28, h=8)
| id | Title | x | w | PromQL | Unit |
|---|---|---|---|---|---|
| 11 | Network bytes sent / received | 0 | 12 | tx: sum by (dpe,service) (rate(bytes_sent[5m])) / rx: sum by (...) (rate(bytes_received[5m])) |
bytes/s |
| 12 | Shared memory reads / writes | 12 | 12 | reads: sum by (...) (rate(shm_reads[5m])) / writes: sum by (...) (rate(shm_writes[5m])) |
ops/s |
Both variables are populated by querying Prometheus at dashboard load time and support multi-select and an All option.
| Variable | PromQL | What it filters |
|---|---|---|
$session |
label_values(ersap_dpe_cpu_usage_percent, session) |
every session label seen in Prometheus |
$dpe |
label_values(ersap_dpe_cpu_usage_percent{session=~"$session"}, dpe) |
DPEs active in the selected session |
Every panel query uses session=~"$session", dpe=~"$dpe" as label matchers, so
the dropdowns at the top filter every panel simultaneously.
Way 1 (recommended): edit visually in Grafana, then export
- Click a panel title → Edit.
- Change the PromQL in the Query tab, or the visualization in the Panel tab.
- Click Apply, then Save dashboard.
- To persist to the repo: Dashboard settings (gear) → JSON Model →
copy, then overwrite
grafana/dashboards/ersap-overview.json. Or: Share → Export → Save to file.
Grafana re-reads the dashboards folder every 30 s automatically — no restart needed.
Way 2: hand-edit the JSON
A panel block (example from the "DPE CPU usage" panel):
{
"id": 9,
"title": "DPE CPU usage",
"type": "timeseries",
"gridPos": {"x": 0, "y": 20, "w": 12, "h": 8},
"targets": [
{
"expr": "ersap_dpe_cpu_usage_percent{session=~\"$session\", dpe=~\"$dpe\"}",
"legendFormat": "{{dpe}}"
}
],
"fieldConfig": {
"defaults": {"unit": "percent", "min": 0, "max": 100}
}
}Key fields:
| Field | Meaning |
|---|---|
id |
unique integer; pick the next available number |
type |
timeseries (line chart), stat (single big number), bargauge, table, etc. |
gridPos |
position and size on the 24-unit-wide grid; panels must not overlap |
targets[].expr |
PromQL query driving the panel |
targets[].legendFormat |
legend label; {{label_name}} pulls a label value |
fieldConfig.defaults.unit |
Grafana unit ID: short, percent, bytes, s, reqps, Bps, ops |
fieldConfig.defaults.thresholds |
colour thresholds for stat panels |
Step 1 — pick the next id and grid position. Current panels occupy y=0–35. Full-width row at the bottom:
"gridPos": {"x": 0, "y": 36, "w": 24, "h": 8}Half-width side by side:
"gridPos": {"x": 0, "y": 36, "w": 12, "h": 8}
"gridPos": {"x": 12, "y": 36, "w": 12, "h": 8}Step 2 — write the PromQL query.
Use the Prometheus expression browser (http://localhost:9090/graph) to develop
and test before pasting. Include $session and $dpe matchers for ERSAP metrics:
rate(your_counter_total{session=~"$session", dpe=~"$dpe"}[5m])
Step 3 — add the panel block to the JSON. Copy an existing panel of the right
type, update id, title, gridPos, expr, legendFormat, and unit, then
add it to the panels array. Example — event rate from a user engine metric:
{
"id": 13,
"title": "Event rate (user engine)",
"type": "timeseries",
"gridPos": {"x": 0, "y": 36, "w": 24, "h": 8},
"targets": [
{
"expr": "ersap_user_event_rate_hz{session=~\"$session\", dpe=~\"$dpe\"}",
"legendFormat": "{{engine}} @ {{dpe}}"
}
],
"fieldConfig": {"defaults": {"unit": "hertz"}}
}Step 4 — save. Grafana picks it up within 30 s. To reload immediately: Dashboard settings → JSON Model → paste updated JSON → Save.
If you added an external scrape target (see
§ Adding an external /metrics endpoint),
its metrics are immediately available in Prometheus. Wire them into a new panel
exactly as above. External metrics typically do not carry session or dpe
labels — omit those matchers:
{
"id": 14,
"title": "SAGIPS event rate",
"type": "timeseries",
"gridPos": {"x": 0, "y": 36, "w": 24, "h": 8},
"targets": [
{
"expr": "sagips_event_rate_hz",
"legendFormat": "{{instance}}"
}
],
"fieldConfig": {"defaults": {"unit": "hertz"}}
}If the external job carries labels you want to filter by, add a template variable (see below).
Via the UI: Dashboard settings → Variables → Add variable →
type Query, data source Prometheus, query:
label_values(your_metric_name, pipeline)
Enable Multi-value and Include All option. Click Apply, then export the JSON to persist.
Via JSON: add an entry to templating.list:
{
"name": "pipeline",
"type": "query",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"query": "label_values(your_metric_name, pipeline)",
"refresh": 2,
"includeAll": true,
"multi": true,
"allValue": ".*"
}Then use pipeline=~"$pipeline" in any panel expr.
| Pattern | When to use it |
|---|---|
metric{label=~"$var"} |
filter by a template variable; =~ handles multi-select and All |
rate(counter[5m]) |
per-second rate of a counter over a 5-minute window |
sum by (label) (...) |
aggregate across series, keeping one label as a dimension |
sum(...) / clamp_min(sum(...), 1e-9) |
safe division — avoids divide-by-zero when denominator is 0 |
100 * failures / requests |
percentage |
time() - timestamp_metric < 30 |
"seen in the last 30 s" liveness check |
count(...) |
number of series matching a selector |
| ID | Displayed as |
|---|---|
short |
plain number with SI suffix |
percent |
% |
bytes |
B, KiB, MiB, … |
s |
seconds |
ms |
milliseconds |
reqps |
req/s |
Bps |
bytes/s |
ops |
ops/s |
hertz |
Hz |
Full list: Panel edit → Field tab → Standard options → Unit dropdown.
../README.md— starting the Monitor FE and the PrometheusExporter.../HOWTO-perlmutter.md— Perlmutter Slurm deployment and adding external targets toersap.yml.../src/main/java/org/jlab/epsci/ersap/util/prometheus/README.md— PrometheusExporter reference: every option, the full metric catalogue, filters, reconnection, alert rules.