Skip to content

Commit 139106b

Browse files
committed
feat: v2.7 — EFA fabric metrics and Cluster Logs dashboard
Closes the two main gaps identified against the AWS HPC blog's observability_for_pcs solution. Validated end-to-end on a live ParallelCluster (hpc8a.96xlarge + g6e.16xlarge, both EFA-enabled). EFA fabric metrics: - New efa-metrics.sh textfile collector reads the EFA hw_counters/ directory (RDMA read/write, SRD retransmits, WR errors) that node_exporter's infiniband collector does not, exposing them as node_amazonefa_* (upstream-compatible names) via a systemd timer on compute and head/login nodes. - Compute Node Details: four new EFA panels, plus the existing bandwidth/packet panels migrated from node_infiniband_* (which emit nothing on real EFA hardware) to node_amazonefa_*. Cluster Logs dashboard (ParallelCluster): - New searchable CloudWatch Logs dashboard (slurmctld, slurmd, clustermgtd, computemgtd, cfn-init, cloud-init) with a text filter, replacing the old logs.json.disabled stub. The installer pins the log group name + ARN from /etc/chef/dna.json; Grafana mounts the host-refreshed AWS creds so the CloudWatch datasource works under Imds.Secured=true. Default window 12h so bootstrap logs are visible. Fixes surfaced during hardware validation: - Dedup dashboard provisioning: remove BOTH platform subdirs after copying to root (duplicate UIDs were blocking all provisioned dashboards). - Restart cloudwatch-exporter + grafana when IMDS rotates creds, so the long-running CloudWatch consumers don't fail with expired tokens. - Correct EFS metric names (iobytes/iolimit) in storage.json; document that FSx CPU/throughput-utilization panels require Persistent-2 SSD. - Substitute the CloudWatch datasource region (was invalid us-east).
1 parent 8601292 commit 139106b

17 files changed

Lines changed: 752 additions & 785 deletions

File tree

CHANGELOG.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,71 @@
11
# Changelog
22

3+
## v2.7 — 2026-06-03
4+
5+
EFA fabric metrics and a searchable CloudWatch Logs dashboard. No breaking
6+
changes. Closes the main gaps identified against the AWS HPC blog's
7+
`observability_for_pcs` solution.
8+
9+
### Added
10+
- **EFA hardware counters** via a new textfile collector
11+
(`custom-metrics/efa-metrics.sh` + `systemd/efa-metrics.{service,timer}`).
12+
node_exporter's `--collector.infiniband` only reads the standard IB
13+
`counters/` directory; the EFA-specific counters
14+
(RDMA read/write bytes, SRD retransmits, work-request errors) live in
15+
`/sys/class/infiniband/<dev>/ports/<port>/hw_counters/`, which it does
16+
not read. The collector surfaces them as `node_amazonefa_*` metrics
17+
(matching the upstream awsome-distributed-training EFA exporter names),
18+
scraped via node_exporter's textfile collector on compute and head/login
19+
nodes. Emits an empty file on non-EFA instances (no-op).
20+
- **Compute Node Details**: four new EFA panels — RDMA Read Throughput,
21+
RDMA Write Throughput, SRD Retransmitted Packets, and Work-Request
22+
Errors — alongside the existing bandwidth/packet-rate panels. The
23+
existing bandwidth/packet panels were also migrated from the stock
24+
`node_infiniband_*` metrics to `node_amazonefa_*`: on real EFA hardware
25+
node_exporter's infiniband collector emits no `node_infiniband_*`
26+
series, so those panels previously stayed empty on EFA instances.
27+
- New **Cluster Logs** dashboard (`grafana/dashboards/pcluster/logs.json`,
28+
ParallelCluster only): searchable CloudWatch Logs for slurmctld, slurmd,
29+
clustermgtd, computemgtd, cfn-init, and cloud-init, plus an all-streams
30+
panel. Includes a `log_group` picker (CloudWatch `logGroups` template
31+
variable, auto-discovers `/aws/parallelcluster/*`) and a free-text
32+
filter — no per-cluster token substitution needed. Replaces the old
33+
`logs.json.disabled` stub.
34+
35+
### Changed
36+
- `compose/compute.yml` and `compose/compute.gpu.yml`: node_exporter now
37+
runs the textfile collector (`--collector.textfile`) so EFA metrics are
38+
scraped on compute nodes (head node already had it).
39+
- `compose/head.yml`: the Grafana container now mounts the host-refreshed
40+
AWS credentials file (`/run/prometheus-ec2-creds`) so the CloudWatch
41+
datasource can read Logs under `Imds.Secured=true`.
42+
- `custom-metrics/refresh-ec2-credentials.sh`: the credentials file is now
43+
world-readable (0644, tmpfs-only) so all three consumer containers can
44+
read it — prometheus (uid 65534), cloudwatch-exporter (uid 0), and
45+
grafana (uid 472/gid 0). The previous `root:65534 0640` blocked Grafana.
46+
- `grafana/datasources/datasource.yml`: CloudWatch `defaultRegion` is now
47+
substituted from the cluster region (was hardcoded to an invalid
48+
`us-east`); the installer also `sed`s `__AWS_REGION__` here.
49+
- `installer/install.sh`: on ParallelCluster, wires the Cluster Logs
50+
dashboard to the cluster's CloudWatch log group by substituting the log
51+
group name (from `/etc/chef/dna.json`) and ARN (built from region +
52+
account). Drops the dashboard if logging is disabled.
53+
54+
### Notes
55+
- EFA metrics require EFA-capable instances (p4/p5/hpc6a/c5n/...). The
56+
required CloudWatch Logs IAM permissions are already in
57+
`iam/monitoring-head-node-policy.json`.
58+
- `node_amazonefa_*` counters intentionally omit the Prometheus `_total`
59+
suffix to stay name-compatible with the upstream EFA node exporter.
60+
- The Logs dashboard pins the log group ARN at install time rather than
61+
using a template variable: Grafana's CloudWatch logs query requires the
62+
ARN, and the PC log group name carries a creation-timestamp suffix that
63+
can't be hardcoded.
64+
- Validated end-to-end on a live PC cluster (hpc8a.96xlarge + g6e.16xlarge,
65+
both EFA-enabled): EFA `node_amazonefa_*` metrics scraped with correct
66+
`instance_id` labels, and the Logs dashboard panels return slurmctld /
67+
slurmd / computemgtd rows.
68+
369
## v2.6.5 — 2026-06-03
470

571
GPU monitoring fixes for AWS PCS on Docker 29.x. No breaking changes.

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ Slurm metrics as containers — no manual configuration required.
1616
- **GPU health monitoring**: XID errors, throttle reasons, ECC counters, NVLink errors, retired pages
1717
- **Secure by default**: per-cluster random password in SSM, optional Cognito SSO
1818
- **GPU-ready**: NVIDIA DCGM exporter with custom counters auto-deploys on GPU instances
19-
- **EFA-ready**: InfiniBand/EFA metrics collected automatically
19+
- **EFA fabric metrics**: bandwidth, packet rate, RDMA read/write throughput, SRD retransmits and work-request errors — collected automatically on EFA hardware
2020
- **Cost tracking**: real-time cost/hour estimates + accumulated total (includes PCS controller cost)
2121
- **Job mapping**: see which Slurm jobs run on which nodes
22+
- **Log search in Grafana**: CloudWatch Logs (slurmctld, slurmd, cluster daemons, bootstrap) browsable from a dashboard (ParallelCluster)
2223
- **Works with `Imds.Secured=true`**: no IMDS workarounds needed
2324

2425
## Dashboards
@@ -28,14 +29,15 @@ Slurm metrics as containers — no manual configuration required.
2829
| **Cluster Summary** | Both | Cluster overview: Slurm states, CPU/memory aggregates, idle node-hours, top users / partitions |
2930
| **Slurm Detail** | Both | Per-partition / per-user / per-account breakdown, queue health, scheduler RPC stats, license usage |
3031
| **Compute Node List** | Both | Fleet table with CPU/Mem/Disk/Network gauges, Queue column, job info, click-through |
31-
| **Compute Node Details** | Both | Per-node deep-dive (CPU, memory, disk, network, EFA) |
32+
| **Compute Node Details** | Both | Per-node deep-dive (CPU, memory, disk, network, EFA bandwidth/RDMA/retransmits/errors) |
3233
| **GPU Node List** | Both | GPU fleet table: model, utilization, temp, power, memory — click-through by hostname |
3334
| **GPU Node Details** | Both | Per-GPU workload metrics + Compute Pipeline activity (SM, tensor, FP64/32/16) + system metrics (CPU/mem/disk/net) |
3435
| **GPU Health** | Both | Cluster-wide GPU faults: XID errors, throttle reasons, ECC, NVLink + PCIe errors, retired pages |
3536
| **HeadNode Details** | ParallelCluster | Head node metrics |
3637
| **Login Node List** | PCS | Login nodes table with click-through to node details |
3738
| **Cluster Costs** | Both | Cost/hour breakdown (headnode/login, compute, EBS, PCS controller) + accumulated total |
3839
| **Storage (FSx + EFS)** | Both | FSx Lustre throughput, IOPS, capacity, CPU/disk utilization; EFS throughput, IO limit, connections, storage |
40+
| **Cluster Logs** | ParallelCluster | Searchable CloudWatch Logs (slurmctld, slurmd, clustermgtd, computemgtd, cfn-init, cloud-init) with log-group picker and text filter |
3941

4042

4143
## Screenshots

compose/compute.gpu.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,16 @@ services:
1313
restart: unless-stopped
1414
volumes:
1515
- '/:/host:ro,rslave'
16+
- '/var/lib/prometheus/node-exporter:/textfile:ro'
1617
image: quay.io/prometheus/node-exporter:v1.9.0
1718
command:
1819
- '--path.rootfs=/host'
1920
- '--collector.infiniband'
21+
# EFA hw_counters (RDMA, SRD retransmits, WR errors) are published
22+
# via the textfile collector by efa-metrics.sh — node_exporter's
23+
# infiniband collector does not read the hw_counters/ directory.
24+
- '--collector.textfile'
25+
- '--collector.textfile.directory=/textfile'
2026

2127
dcgm-exporter:
2228
container_name: nvidia-dcgm

compose/compute.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ services:
1212
restart: unless-stopped
1313
volumes:
1414
- '/:/host:ro,rslave'
15+
- '/var/lib/prometheus/node-exporter:/textfile:ro'
1516
image: quay.io/prometheus/node-exporter:v1.9.0
1617
command:
1718
- '--path.rootfs=/host'
1819
- '--collector.infiniband'
20+
# EFA hw_counters (RDMA, SRD retransmits, WR errors) are published
21+
# via the textfile collector by efa-metrics.sh — node_exporter's
22+
# infiniband collector does not read the hw_counters/ directory.
23+
- '--collector.textfile'
24+
- '--collector.textfile.directory=/textfile'

compose/head.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ services:
4848
- 'GF_SERVER_ROOT_URL=http://%(domain)s/grafana/'
4949
# Land every user on ParallelCluster Summary after login.
5050
- 'GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/ParallelCluster.json'
51+
# AWS creds for the CloudWatch datasource (Logs dashboard). Same
52+
# host-refreshed credentials file Prometheus/cloudwatch-exporter use;
53+
# required because Imds.Secured=true blocks IMDS from containers.
54+
- 'AWS_SHARED_CREDENTIALS_FILE=/run/prometheus-ec2-creds/credentials'
5155
env_file:
5256
# Cognito OAuth2 config, materialized by installer from SSM.
5357
# Empty file when Cognito is not configured (no-op).
@@ -56,6 +60,7 @@ services:
5660
- '__MONITORING_HOME__/grafana:/etc/grafana/provisioning'
5761
- 'grafana-data:/var/lib/grafana'
5862
- '/run/grafana-secrets:/run/grafana-secrets:ro'
63+
- '/run/prometheus-ec2-creds:/run/prometheus-ec2-creds:ro'
5964
image: grafana/grafana:11.2.2
6065

6166
prometheus-node-exporter:

custom-metrics/efa-metrics.sh

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/bin/bash
2+
#
3+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
# SPDX-License-Identifier: MIT-0
5+
#
6+
# Textfile collector: exports Amazon EFA hardware counters for Prometheus.
7+
#
8+
# Why this exists:
9+
# node_exporter's --collector.infiniband only reads the standard IB
10+
# counters in /sys/class/infiniband/<dev>/ports/<port>/counters/
11+
# (port_rcv_data, port_xmit_data, ...). The EFA-specific counters that
12+
# matter for HPC/ML fabric health — RDMA read/write bytes, SRD
13+
# retransmits, work-request errors — live in a SEPARATE directory:
14+
# /sys/class/infiniband/<dev>/ports/<port>/hw_counters/
15+
# which that collector does not read. Rather than ship a forked
16+
# node_exporter binary (as some solutions do), we surface the same
17+
# counters via the textfile collector.
18+
#
19+
# Metric naming mirrors the upstream awsome-distributed-training EFA node
20+
# exporter (node_amazonefa_<counter>), so dashboards/queries are portable
21+
# to/from that exporter.
22+
#
23+
# Output: /var/lib/prometheus/node-exporter/efa.prom
24+
# node_amazonefa_rdma_read_bytes{device="rdmap16s27",port="1"} 12345
25+
# node_amazonefa_retrans_pkts{device="rdmap16s27",port="1"} 0
26+
# ...
27+
#
28+
# The instance_id / instance_type labels are added by Prometheus at scrape
29+
# time (ec2_sd relabeling on the :9100 node_exporter target), so this
30+
# script only emits device/port labels — exactly like the other panels.
31+
#
32+
# Emits nothing (zero-length file) on instances without EFA, so the panels
33+
# simply stay empty there. Runs as root via a systemd timer.
34+
#
35+
set -euo pipefail
36+
37+
# Paths are overridable for testing (EFA_SYSFS_PATH / EFA_OUTPUT). In
38+
# production they default to the real sysfs and node_exporter textfile dir.
39+
IB_PATH="${EFA_SYSFS_PATH:-/sys/class/infiniband}"
40+
TEXTFILE_DIR="/var/lib/prometheus/node-exporter"
41+
OUTPUT="${EFA_OUTPUT:-${TEXTFILE_DIR}/efa.prom}"
42+
TMP="${OUTPUT}.$$"
43+
mkdir -p "$(dirname "${OUTPUT}")"
44+
45+
# No IB/EFA class at all → write an empty file and exit so any stale
46+
# metrics are cleared.
47+
if [[ ! -d "${IB_PATH}" ]]; then
48+
: > "${TMP}"
49+
mv -f "${TMP}" "${OUTPUT}"
50+
exit 0
51+
fi
52+
53+
# Decide whether a device is EFA. EFA devices are bound to the 'efa'
54+
# kernel driver; fall back to the presence of an EFA-specific counter
55+
# file (rdma_read_bytes) for robustness across kernels.
56+
is_efa_device() {
57+
local dev="$1"
58+
local drv
59+
drv=$(readlink -f "${IB_PATH}/${dev}/device/driver" 2>/dev/null || true)
60+
[[ "${drv##*/}" == "efa" ]] && return 0
61+
# Fallback: any port exposing the EFA-specific hw_counter.
62+
compgen -G "${IB_PATH}/${dev}/ports/*/hw_counters/rdma_read_bytes" >/dev/null 2>&1
63+
}
64+
65+
# First pass: collect EFA (device, port, hw_counters-dir) tuples and the
66+
# union of counter file names, so we can group samples by metric name
67+
# (the Prometheus text format requires each metric's TYPE line to precede
68+
# its samples and all samples to be contiguous).
69+
#
70+
# Uses indexed arrays + a newline-delimited string (deduped via sort -u)
71+
# instead of associative arrays, so it runs on bash 3.2+ (macOS dev hosts,
72+
# minimal images) as well as the bash 4+ on the HPC AMIs.
73+
HWDIRS=()
74+
DEVS=()
75+
PORTS=()
76+
counter_names=""
77+
78+
for devpath in "${IB_PATH}"/*; do
79+
[[ -d "${devpath}" ]] || continue
80+
dev="$(basename "${devpath}")"
81+
is_efa_device "${dev}" || continue
82+
for portpath in "${devpath}"/ports/*; do
83+
[[ -d "${portpath}/hw_counters" ]] || continue
84+
port="$(basename "${portpath}")"
85+
hwdir="${portpath}/hw_counters"
86+
HWDIRS+=("${hwdir}")
87+
DEVS+=("${dev}")
88+
PORTS+=("${port}")
89+
for cfile in "${hwdir}"/*; do
90+
[[ -f "${cfile}" ]] || continue
91+
counter_names="${counter_names}$(basename "${cfile}")
92+
"
93+
done
94+
done
95+
done
96+
97+
# Sorted, de-duplicated list of counter names across all EFA ports.
98+
counters_sorted="$(printf '%s' "${counter_names}" | sort -u | sed '/^$/d')"
99+
100+
# Emit grouped by metric name.
101+
{
102+
while IFS= read -r counter; do
103+
[[ -n "${counter}" ]] || continue
104+
metric="node_amazonefa_${counter}"
105+
echo "# HELP ${metric} EFA hw_counter ${counter} from /sys/class/infiniband/*/ports/*/hw_counters"
106+
echo "# TYPE ${metric} counter"
107+
for i in "${!HWDIRS[@]}"; do
108+
cfile="${HWDIRS[$i]}/${counter}"
109+
[[ -r "${cfile}" ]] || continue
110+
value="$(cat "${cfile}" 2>/dev/null || echo "")"
111+
# Only emit clean non-negative integers; skip N/A or blank.
112+
[[ "${value}" =~ ^[0-9]+$ ]] || continue
113+
echo "${metric}{device=\"${DEVS[$i]}\",port=\"${PORTS[$i]}\"} ${value}"
114+
done
115+
done <<< "${counters_sorted}"
116+
} > "${TMP}"
117+
118+
# Atomic rename so node_exporter never reads a partial file.
119+
mv -f "${TMP}" "${OUTPUT}"

custom-metrics/refresh-ec2-credentials.sh

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,18 @@ set -euo pipefail
1717
CREDS_DIR="/run/prometheus-ec2-creds"
1818
CREDS_FILE="${CREDS_DIR}/credentials"
1919
mkdir -p "${CREDS_DIR}"
20-
chmod 0750 "${CREDS_DIR}"
21-
chown root:65534 "${CREDS_DIR}"
20+
# World-readable dir/file (see chmod on the file below). The credentials
21+
# are consumed by three containers running as different uids/gids:
22+
# prometheus uid 65534 / gid 65534
23+
# cloudwatch-exporter uid 0 / gid 0
24+
# grafana uid 472 / gid 0 (CloudWatch Logs datasource)
25+
# No single owner/group satisfies all three, so we rely on world-read.
26+
# This is acceptable here: the file lives only on tmpfs (never touches
27+
# disk), holds short-lived IAM role credentials that are already
28+
# retrievable from IMDS by any process on this single-tenant host, and
29+
# the directory is not network-exposed.
30+
chmod 0755 "${CREDS_DIR}"
31+
chown root:root "${CREDS_DIR}"
2232

2333
# IMDSv2 token
2434
TOKEN=$(curl -sS --max-time 5 -X PUT "http://169.254.169.254/latest/api/token" \
@@ -46,8 +56,32 @@ aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}
4656
aws_session_token = ${AWS_SESSION_TOKEN}
4757
CRED
4858

49-
# Restrict to root + the container's UID (nobody/65534 on our images).
50-
# Directory is also tmpfs-backed so creds never touch persistent disk.
51-
chmod 0640 "${TMP_FILE}"
52-
chown root:65534 "${TMP_FILE}"
59+
# World-readable (see dir comment above): consumed by prometheus,
60+
# cloudwatch-exporter, and grafana running as different uids/gids.
61+
# tmpfs-backed so creds never touch persistent disk.
62+
chmod 0644 "${TMP_FILE}"
63+
chown root:root "${TMP_FILE}"
64+
65+
# Detect whether the IMDS access key actually rotated (IMDS rotates role
66+
# creds roughly every ~6h). Prometheus re-reads the credentials file on
67+
# every ec2_sd refresh, so it never goes stale. But long-running JVM/Go
68+
# consumers — the cloudwatch-exporter and Grafana's CloudWatch datasource —
69+
# load the credentials provider once at startup and cache it in memory,
70+
# so when the underlying token expires their CloudWatch calls start
71+
# returning 403 "security token expired" and the Storage / Logs dashboards
72+
# go blank. Restart those two containers, but only when the key actually
73+
# changed, to avoid needless churn on every 5-minute tick.
74+
OLD_KEY=""
75+
if [[ -r "${CREDS_FILE}" ]]; then
76+
OLD_KEY=$(awk -F'= *' '/aws_access_key_id/{print $2; exit}' "${CREDS_FILE}" 2>/dev/null || echo "")
77+
fi
5378
mv -f "${TMP_FILE}" "${CREDS_FILE}"
79+
80+
if [[ "${AWS_ACCESS_KEY_ID}" != "${OLD_KEY}" && -n "${OLD_KEY}" ]]; then
81+
# Key rotated since last run — bounce the CloudWatch consumers so they
82+
# pick up the new credentials. Ignore errors (containers may not exist
83+
# on compute nodes; this script only does meaningful work on head/login).
84+
for c in cloudwatch-exporter grafana; do
85+
docker restart "${c}" >/dev/null 2>&1 || true
86+
done
87+
fi

0 commit comments

Comments
 (0)