98 lines
3.0 KiB
Python
98 lines
3.0 KiB
Python
#!/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()
|