#!/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()