Add blockchain devnet metrics dashboard

This commit is contained in:
juvdiaz 2026-06-29 19:22:23 -06:00
parent c4865f17c7
commit 709881be86
7 changed files with 397 additions and 2 deletions

View File

@ -346,7 +346,9 @@ and recent repo-server/application-controller errors.
`grafana-dashboards` manages repo-provisioned Grafana dashboards under `grafana-dashboards` manages repo-provisioned Grafana dashboards under
`bootstrap/platform/grafana-dashboards`. `./jeannie up` provisions `bootstrap/platform/grafana-dashboards`. `./jeannie up` provisions
them through the platform stack; `./jeannie grafana-dashboards apply` them through the platform stack; `./jeannie grafana-dashboards apply`
rebuilds just the dashboard ConfigMaps and restarts Grafana. rebuilds just the dashboard ConfigMaps and restarts Grafana. The optional
blockchain devnet dashboard uses metrics from
`infra/blockchain-devnet/metrics/exporter.py`.
`cert-check` verifies public DNS, TLS certificate expiry, public website/Gitea `cert-check` verifies public DNS, TLS certificate expiry, public website/Gitea
HTTP status, and DuckDNS-to-OCI edge IP drift from the canonical inventory. HTTP status, and DuckDNS-to-OCI edge IP drift from the canonical inventory.

View File

@ -346,7 +346,9 @@ and recent repo-server/application-controller errors.
`grafana-dashboards` manages repo-provisioned Grafana dashboards under `grafana-dashboards` manages repo-provisioned Grafana dashboards under
`bootstrap/platform/grafana-dashboards`. `./{{ main_script }} up` provisions `bootstrap/platform/grafana-dashboards`. `./{{ main_script }} up` provisions
them through the platform stack; `./{{ main_script }} grafana-dashboards apply` them through the platform stack; `./{{ main_script }} grafana-dashboards apply`
rebuilds just the dashboard ConfigMaps and restarts Grafana. rebuilds just the dashboard ConfigMaps and restarts Grafana. The optional
blockchain devnet dashboard uses metrics from
`infra/blockchain-devnet/metrics/exporter.py`.
`cert-check` verifies public DNS, TLS certificate expiry, public website/Gitea `cert-check` verifies public DNS, TLS certificate expiry, public website/Gitea
HTTP status, and DuckDNS-to-OCI edge IP drift from the canonical inventory. HTTP status, and DuckDNS-to-OCI edge IP drift from the canonical inventory.

View File

@ -0,0 +1,247 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"0": {
"text": "down"
},
"1": {
"text": "up"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"expr": "homelab_blockchain_rpc_up",
"legendFormat": "rpc",
"refId": "A"
}
],
"title": "RPC Health",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"h": 8,
"w": 6,
"x": 6,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"expr": "homelab_blockchain_latest_block",
"legendFormat": "latest block",
"refId": "A"
}
],
"title": "Latest Block",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"h": 8,
"w": 6,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"expr": "homelab_blockchain_gas_price_wei",
"legendFormat": "wei",
"refId": "A"
}
],
"title": "Gas Price",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"h": 8,
"w": 6,
"x": 18,
"y": 0
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"expr": "homelab_blockchain_peer_count",
"legendFormat": "peers",
"refId": "A"
}
],
"title": "Peer Count",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 8
},
"id": 5,
"targets": [
{
"expr": "homelab_blockchain_rpc_latency_seconds",
"legendFormat": "{{ method }}",
"refId": "A"
}
],
"title": "RPC Latency",
"type": "timeseries"
}
],
"refresh": "10s",
"schemaVersion": 39,
"tags": [
"homelab",
"blockchain"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Homelab Blockchain Devnet",
"uid": "homelab-blockchain-devnet",
"version": 1,
"weekStart": ""
}

View File

@ -37,6 +37,29 @@ Use the gateway when you want to practice RPC protection patterns such as method
restriction, rate limiting, CORS headers, and request logging. Keep the raw restriction, rate limiting, CORS headers, and request logging. Keep the raw
Anvil RPC for direct local development. Anvil RPC for direct local development.
The Prometheus metrics endpoint is available at:
```text
http://127.0.0.1:9109/metrics
```
Metrics include:
- `homelab_blockchain_rpc_up`
- `homelab_blockchain_chain_id`
- `homelab_blockchain_latest_block`
- `homelab_blockchain_peer_count`
- `homelab_blockchain_gas_price_wei`
- `homelab_blockchain_rpc_latency_seconds`
Use `prometheus-scrape.example.yml` as the scrape target shape. The repo also
includes the `Homelab Blockchain Devnet` Grafana dashboard under
`bootstrap/platform/grafana-dashboards`; rebuild dashboard ConfigMaps with:
```bash
./jeannie grafana-dashboards apply
```
From another LAN host, use the Debian host IP: From another LAN host, use the Debian host IP:
```text ```text
@ -61,6 +84,7 @@ curl -sS -X POST http://127.0.0.1:8545 \
BLOCKCHAIN_RPC_PORT=8545 BLOCKCHAIN_RPC_PORT=8545
BLOCKCHAIN_RPC_GATEWAY_PORT=18545 BLOCKCHAIN_RPC_GATEWAY_PORT=18545
BLOCKCHAIN_EXPLORER_PORT=8089 BLOCKCHAIN_EXPLORER_PORT=8089
BLOCKCHAIN_METRICS_PORT=9109
BLOCKCHAIN_CHAIN_ID=31337 BLOCKCHAIN_CHAIN_ID=31337
BLOCKCHAIN_BLOCK_TIME=2 BLOCKCHAIN_BLOCK_TIME=2
``` ```

View File

@ -39,3 +39,20 @@ services:
volumes: volumes:
- ./nginx/rpc-gateway.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx/rpc-gateway.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stopped restart: unless-stopped
metrics:
image: python:3.12-alpine
container_name: homelab-blockchain-metrics
depends_on:
- anvil
environment:
BLOCKCHAIN_METRICS_RPC_URL: http://anvil:8545
BLOCKCHAIN_METRICS_PORT: "9109"
ports:
- "${BLOCKCHAIN_METRICS_PORT:-9109}:9109"
volumes:
- ./metrics/exporter.py:/app/exporter.py:ro
command:
- python
- /app/exporter.py
restart: unless-stopped

View File

@ -0,0 +1,97 @@
#!/usr/bin/env python3
import json
import os
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
RPC_URL = os.environ.get("BLOCKCHAIN_METRICS_RPC_URL", "http://anvil:8545")
LISTEN_ADDR = os.environ.get("BLOCKCHAIN_METRICS_ADDR", "0.0.0.0")
LISTEN_PORT = int(os.environ.get("BLOCKCHAIN_METRICS_PORT", "9109"))
def rpc(method):
started = time.monotonic()
body = json.dumps(
{"jsonrpc": "2.0", "method": method, "params": [], "id": 1}
).encode("utf-8")
request = urllib.request.Request(
RPC_URL,
data=body,
headers={"content-type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=3) as response:
payload = json.loads(response.read().decode("utf-8"))
if "error" in payload:
raise RuntimeError(payload["error"])
return payload.get("result"), time.monotonic() - started
def hex_to_int(value):
if isinstance(value, str) and value.startswith("0x"):
return int(value, 16)
return int(value)
def collect_metrics():
metrics = []
latencies = {}
values = {}
up = 1
for method in ["eth_chainId", "eth_blockNumber", "net_peerCount", "eth_gasPrice"]:
try:
result, latency = rpc(method)
latencies[method] = latency
values[method] = hex_to_int(result)
except (OSError, RuntimeError, urllib.error.URLError, ValueError):
up = 0
latencies[method] = 0
metrics.append("# HELP homelab_blockchain_rpc_up Blockchain RPC health.")
metrics.append("# TYPE homelab_blockchain_rpc_up gauge")
metrics.append(f"homelab_blockchain_rpc_up {up}")
gauge_map = {
"eth_chainId": "homelab_blockchain_chain_id",
"eth_blockNumber": "homelab_blockchain_latest_block",
"net_peerCount": "homelab_blockchain_peer_count",
"eth_gasPrice": "homelab_blockchain_gas_price_wei",
}
for method, metric_name in gauge_map.items():
if method in values:
metrics.append(f"# TYPE {metric_name} gauge")
metrics.append(f"{metric_name} {values[method]}")
metrics.append("# TYPE homelab_blockchain_rpc_latency_seconds gauge")
for method, latency in latencies.items():
metrics.append(
f'homelab_blockchain_rpc_latency_seconds{{method="{method}"}} {latency:.6f}'
)
return "\n".join(metrics) + "\n"
class MetricsHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path not in ["/metrics", "/"]:
self.send_response(404)
self.end_headers()
return
body = collect_metrics().encode("utf-8")
self.send_response(200)
self.send_header("content-type", "text/plain; version=0.0.4")
self.send_header("content-length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
return
if __name__ == "__main__":
server = HTTPServer((LISTEN_ADDR, LISTEN_PORT), MetricsHandler)
server.serve_forever()

View File

@ -0,0 +1,6 @@
- job_name: homelab-blockchain-devnet
static_configs:
- targets:
- 192.168.100.73:9109
labels:
service: blockchain-devnet