A Prometheus exporter for BSV nodes: SV Node and Teranode. On every scrape it makes up to four JSON-RPC calls from a fixed allowlist, and turns them into about 30 node-health, fork and mempool series per node. It is scrape-only and stateless.
Configuration is read from environment variables only.
| Variable | Default | Meaning |
|---|---|---|
BSV_RPC_URL |
required | e.g. http://rpc:9292. Must not contain credentials |
BSV_RPC_USER / BSV_RPC_PASSWORD |
empty | RPC credentials |
BSV_RPC_PASSWORD_FILE |
empty | Read the password from this file instead; takes precedence over BSV_RPC_PASSWORD |
BSV_RPC_TIMEOUT |
5s |
Timeout for each RPC call, at most 5m. Keep it below the scraper's scrape_timeout (default 10s) |
BSV_MEMPOOL_SOURCE |
mempoolinfo |
mempoolinfo or miningcandidate. Teranode needs miningcandidate: its getmempoolinfo is unimplemented |
BSV_COLLECTORS |
blockchain,peers,mempool,chaintips |
Comma-separated collectors to enable |
LISTEN_ADDR |
:9480 |
HTTP listen address |
The exporter serves GET /metrics and GET /healthz. /healthz never calls the node. At most two /metrics scrapes run at once; further concurrent scrapes get HTTP 503, so the exporter cannot multiply load on the node. Node RPC never goes through HTTP_PROXY / HTTPS_PROXY.
| Series | Type | Source |
|---|---|---|
bsv_rpc_up{method} |
gauge 0/1 | 1 if the call succeeded during this scrape |
bsv_rpc_duration_seconds{method} |
gauge | wall time of the call during this scrape |
bsv_blocks |
gauge | getblockchaininfo.blocks |
bsv_headers |
gauge | getblockchaininfo.headers |
bsv_difficulty |
gauge | getblockchaininfo.difficulty |
bsv_peers{kind} |
gauge | getpeerinfo, counted as inbound / outbound / p2p |
bsv_mempool_txs |
gauge | getmempoolinfo.size; with BSV_MEMPOOL_SOURCE=mempoolinfo |
bsv_mining_candidate_txs |
gauge | getminingcandidate.num_tx, coinbase included; with BSV_MEMPOOL_SOURCE=miningcandidate |
bsv_mempool_bytes |
gauge | getmempoolinfo.bytes; with BSV_MEMPOOL_SOURCE=mempoolinfo |
bsv_chaintips{status} |
gauge | getchaintips, counted by status |
bsv_chaintip_forks{window,length} |
gauge | see below |
bsv_exporter_build_info{version,goversion} |
gauge | always 1 |
A failed call sets bsv_rpc_up{method} to 0 and omits that call's series. A scrape takes at most about BSV_RPC_TIMEOUT, so as long as that is below the scraper's scrape_timeout, one hung call costs only its own series. The rest of the scrape is still served, with HTTP 200. Alert on bsv_rpc_up == 0 for "node unreachable", and on the scraper's own up for "exporter down".
Peer kinds. A peer with a non-empty peerid is a Teranode libp2p peer and counts as p2p. Teranode sets inbound on those peers to mean "connected", not direction, so direction is ignored for them. Every other peer counts as inbound when inbound is true, and as outbound when it is false or missing. Teranode omits inbound: false from legacy peers. All three kinds are always emitted.
Statuses. The five statuses SV Node and Teranode return (active, valid-fork, valid-headers, headers-only, invalid) are always emitted, at 0 when absent. Any other value counts as other, so a node cannot create arbitrary label values.
Fork buckets.
- The active height is the highest
heightamong tips with statusactive. - A fork is a non-
activetip with0 <= active height − height <= window, forwindow144 and 10000. Tips above the active height are headers ahead of validation, not forks, and are not counted (they still count inbsv_chaintips{status}). length="single"counts tips withbranchlen == 1.length="long"counts tips withbranchlen > 1.
All four buckets are always emitted, at 0 when empty. With no active tip, all four are 0.
The exporter adds no deployment labels such as network, host or node type. Attach them in the scrape configuration, so a single build works everywhere.
Recommended Teranode settings:
BSV_MEMPOOL_SOURCE=miningcandidate
BSV_COLLECTORS=blockchain,peers,mempoolminingcandidateis required, because Teranode does not implementgetmempoolinfo.- Leave the
chaintipscollector off for Teranode. On long chains itsgetchaintipsmay not return at all, so every scrape would start another expensive call that only ends atBSV_RPC_TIMEOUT. getminingcandidateis not strictly read-only: the node builds and caches a candidate. At normal scrape intervals that is harmless.
The container runs as UID 65532, so the password file must be readable by that UID:
sudo install -d -m 0755 /etc/bsv-node-exporter
sudo install -m 0400 -o 65532 -g 65532 ./rpc-password /etc/bsv-node-exporter/rpc-password
docker run --read-only -p 127.0.0.1:9480:9480 \
-e BSV_RPC_URL=http://node:8332 \
-e BSV_RPC_USER=exporter \
-e BSV_RPC_PASSWORD_FILE=/run/secrets/rpc-password \
-v /etc/bsv-node-exporter/rpc-password:/run/secrets/rpc-password:ro \
ghcr.io/bsv-blockchain/bsv-node-exporter:<version>The image is distroless and needs no writable filesystem. Inside the container the exporter listens on all interfaces (:9480) so it can be reached at all; publish the port only where the scraper runs, as 127.0.0.1 does above. /metrics has no authentication.
/etc/bsv-node-exporter.env holds the non-secret settings:
BSV_RPC_URL=http://127.0.0.1:8332
BSV_RPC_USER=exporter
LISTEN_ADDR=127.0.0.1:9480/etc/systemd/system/bsv-node-exporter.service:
[Unit]
Description=BSV node Prometheus exporter
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/bsv-node-exporter
EnvironmentFile=/etc/bsv-node-exporter.env
LoadCredential=rpc-password:/etc/bsv-node-exporter/rpc-password
Environment=BSV_RPC_PASSWORD_FILE=%d/rpc-password
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
RestrictAddressFamilies=AF_INET AF_INET6
Restart=on-failure
[Install]
WantedBy=multi-user.targetWith Prometheus, attach deployment labels on the target:
scrape_configs:
- job_name: bsv-node
scrape_interval: 60s
static_configs:
- targets: ["localhost:9480"]
labels:
network: mainnetWith the OpenTelemetry Collector (contrib), add a prometheus receiver to a metrics pipeline. A resource processor in the same pipeline can attach deployment attributes:
receivers:
prometheus/bsv-node:
config:
scrape_configs:
- job_name: bsv-node
scrape_interval: 60s
static_configs:
- targets: ["localhost:9480"]- Credentials never appear in logs, metric labels or error messages, and a
BSV_RPC_URLcontaining credentials is rejected. - Only
getblockchaininfo,getpeerinfo,getmempoolinfo,getminingcandidateandgetchaintipscan be called. There is no RPC passthrough. - RPC calls never follow redirects. Each method has a response budget: 1 MiB for
getblockchaininfo,getmempoolinfoandgetminingcandidate, 8 MiB forgetpeerinfo, 16 MiB forgetchaintips. Response headers are capped at 64 KiB. Peer and chain-tip arrays are decoded one element at a time and rejected beyond 10,000 peers or 100,000 tips. - Logged errors carry no node-supplied data. A JSON-RPC error is logged as its code, plus a fixed description for well-known codes; the node's message is never logged. Transport and decode failures map to fixed categories. Every logged error is capped at 300 bytes. The RPC endpoint is logged as scheme and host only.
- The HTTP server sets read, write and header timeouts, and serves only
/metricsand/healthz. - The only direct dependencies are the Go standard library and
prometheus/client_golang. - CI runs
govulncheckandgolangci-lint, and release images are scanned with trivy.
Report vulnerabilities privately; see SECURITY.md.
Apache-2.0. See LICENSE.