merge midi metrics into prometheus-exporter

midi-bridge: removed HTTP server on :9091, now writes /tmp/midi-stats.json
prometheus-exporter: reads /tmp/midi-stats.json, exposes all metrics on :9090/metrics
Single scrape endpoint for all pipeline components
This commit is contained in:
2026-06-24 02:41:27 -07:00
parent 7706517b88
commit f673da4b3b
2 changed files with 164 additions and 156 deletions
+32 -71
View File
@@ -4,16 +4,16 @@
Sends parsed MIDI messages as Pd-style 'midi status data1 data2;' strings
to UDP ports for Pd (8081) and LED matrix visualization (8082).
Exposes Prometheus metrics on :9091/metrics.
Writes metrics to /tmp/midi-stats.json for prometheus-exporter to collect.
"""
import os, socket, time, select, pathlib, threading, http.server
import os, socket, time, select, pathlib, json
PD_HOST = os.environ.get("MIDI_BRIDGE_HOST", "127.0.0.1")
PD_PORT = int(os.environ.get("MIDI_BRIDGE_PORT", "8081"))
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
METRICS_PORT = int(os.environ.get("MIDI_METRICS_PORT", "9091"))
STATS_FILE = os.environ.get("MIDI_STATS_FILE", "/tmp/midi-stats.json")
STATS_INTERVAL = 2.0
# ── Metrics ──
midi_notes_on = 0
midi_notes_off = 0
midi_messages_total = 0
@@ -22,7 +22,7 @@ last_status = 0
last_note = 0
last_velocity = 0
last_time = 0.0
_metrics_lock = threading.Lock()
_stats_lock = object()
def _find_midi_dev():
@@ -74,7 +74,7 @@ def _flush(sock, buf):
sock.sendto(msg.encode(), (PD_HOST, PD_PORT))
sock.sendto(msg.encode(), (PD_HOST, VIZ_PORT))
with _metrics_lock:
with _stats_lock:
midi_messages_total += 1
last_status = status
last_note = data1
@@ -87,75 +87,28 @@ def _flush(sock, buf):
midi_notes_off += 1
# ── Prometheus HTTP ──
def _format_metrics():
with _metrics_lock:
lines = [
"# HELP midi_messages_total Total MIDI messages received",
"# TYPE midi_messages_total counter",
f"midi_messages_total {midi_messages_total}",
"",
"# HELP midi_notes_on_total Total note-on messages",
"# TYPE midi_notes_on_total counter",
f"midi_notes_on_total {midi_notes_on}",
"",
"# HELP midi_notes_off_total Total note-off messages",
"# TYPE midi_notes_off_total counter",
f"midi_notes_off_total {midi_notes_off}",
"",
"# HELP midi_errors_total Total MIDI parse errors",
"# TYPE midi_errors_total counter",
f"midi_errors_total {midi_errors}",
"",
"# HELP midi_last_status Status byte of last message",
"# TYPE midi_last_status gauge",
f"midi_last_status {last_status}",
"",
"# HELP midi_last_note Note number of last message",
"# TYPE midi_last_note gauge",
f"midi_last_note {last_note}",
"",
"# HELP midi_last_velocity Velocity of last message",
"# TYPE midi_last_velocity gauge",
f"midi_last_velocity {last_velocity}",
"",
"# HELP midi_last_time_unix Epoch of last MIDI message",
"# TYPE midi_last_time_unix gauge",
f"midi_last_time_unix {last_time:.3f}",
"",
]
return "\n".join(lines) + "\n"
class _MetricsHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/metrics":
body = _format_metrics().encode()
self.send_response(200)
self.send_header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
def _write_stats():
try:
with open(STATS_FILE, "w") as f:
json.dump({
"midi_messages_total": midi_messages_total,
"midi_notes_on_total": midi_notes_on,
"midi_notes_off_total": midi_notes_off,
"midi_errors_total": midi_errors,
"midi_last_status": last_status,
"midi_last_note": last_note,
"midi_last_velocity": last_velocity,
"midi_last_time_unix": last_time,
}, f)
except OSError:
pass
def _start_metrics_server():
server = http.server.HTTPServer(("0.0.0.0", METRICS_PORT), _MetricsHandler)
t = threading.Thread(target=server.serve_forever, daemon=True)
t.start()
def main():
_start_metrics_server()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
running = True
last_stats_write = time.monotonic()
while running:
try:
fd = _open_midi()
@@ -163,8 +116,16 @@ def main():
poll.register(fd, select.POLLIN)
buf = bytearray()
while running:
now = time.monotonic()
# Write stats periodically
if now - last_stats_write >= STATS_INTERVAL:
with _stats_lock:
_write_stats()
last_stats_write = now
try:
events = poll.poll(500)
events = poll.poll(100)
for f, flags in events:
if flags & select.POLLIN:
data = os.read(fd, 1024)
@@ -200,4 +161,4 @@ def main():
if __name__ == "__main__":
main()
main()