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
|
`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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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