Add blockchain devnet metrics dashboard
This commit is contained in:
parent
c4865f17c7
commit
709881be86
|
|
@ -346,7 +346,9 @@ and recent repo-server/application-controller errors.
|
|||
`grafana-dashboards` manages repo-provisioned Grafana dashboards under
|
||||
`bootstrap/platform/grafana-dashboards`. `./jeannie up` provisions
|
||||
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
|
||||
HTTP status, and DuckDNS-to-OCI edge IP drift from the canonical inventory.
|
||||
|
|
|
|||
|
|
@ -346,7 +346,9 @@ and recent repo-server/application-controller errors.
|
|||
`grafana-dashboards` manages repo-provisioned Grafana dashboards under
|
||||
`bootstrap/platform/grafana-dashboards`. `./{{ main_script }} up` provisions
|
||||
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
|
||||
HTTP status, and DuckDNS-to-OCI edge IP drift from the canonical inventory.
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
@ -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
|
||||
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:
|
||||
|
||||
```text
|
||||
|
|
@ -61,6 +84,7 @@ curl -sS -X POST http://127.0.0.1:8545 \
|
|||
BLOCKCHAIN_RPC_PORT=8545
|
||||
BLOCKCHAIN_RPC_GATEWAY_PORT=18545
|
||||
BLOCKCHAIN_EXPLORER_PORT=8089
|
||||
BLOCKCHAIN_METRICS_PORT=9109
|
||||
BLOCKCHAIN_CHAIN_ID=31337
|
||||
BLOCKCHAIN_BLOCK_TIME=2
|
||||
```
|
||||
|
|
|
|||
|
|
@ -39,3 +39,20 @@ services:
|
|||
volumes:
|
||||
- ./nginx/rpc-gateway.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
- job_name: homelab-blockchain-devnet
|
||||
static_configs:
|
||||
- targets:
|
||||
- 192.168.100.73:9109
|
||||
labels:
|
||||
service: blockchain-devnet
|
||||
Loading…
Reference in New Issue