Spaces:
Running
Running
chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)
Browse filesAutomated backend sync from szl-holdings/a11oy main via hf-sync-backend.
Updated (differed from the Space): Dockerfile, serve.py, szl_metrics_prom.py
Deleted (gone from the repo + Dockerfile COPY set): (none)
Keeps the Space-built backend (serve.py + the Dockerfile-COPY'd .py
modules) identical to GitHub main so the Space never rebuilds from a
stale backend, new endpoints don't 404 there, and orphaned modules
removed from the repo don't linger in the Space tree.
- Dockerfile +1 -1
- serve.py +25 -0
- szl_metrics_prom.py +261 -0
Dockerfile
CHANGED
|
@@ -664,7 +664,7 @@ COPY szl_connectors/ ./szl_connectors/
|
|
| 664 |
# across a11oy + killinchu (shared-file-drift enforces it via this COPY list).
|
| 665 |
# This Dockerfile never uses `COPY . .` — without this line `import
|
| 666 |
# szl_hf_bucket` fails. Imported lazily by callers; no boot-time side effects.
|
| 667 |
-
COPY szl_hf_bucket.py ./
|
| 668 |
|
| 669 |
CMD ["python", "serve.py"]
|
| 670 |
|
|
|
|
| 664 |
# across a11oy + killinchu (shared-file-drift enforces it via this COPY list).
|
| 665 |
# This Dockerfile never uses `COPY . .` — without this line `import
|
| 666 |
# szl_hf_bucket` fails. Imported lazily by callers; no boot-time side effects.
|
| 667 |
+
COPY szl_hf_bucket.py szl_metrics_prom.py ./
|
| 668 |
|
| 669 |
CMD ["python", "serve.py"]
|
| 670 |
|
serve.py
CHANGED
|
@@ -7421,6 +7421,31 @@ except Exception as _sovcomp_e:
|
|
| 7421 |
# ============================================================================
|
| 7422 |
|
| 7423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7424 |
if __name__ == "__main__":
|
| 7425 |
import uvicorn
|
| 7426 |
port = int(os.environ.get("PORT", "7860"))
|
|
|
|
| 7421 |
# ============================================================================
|
| 7422 |
|
| 7423 |
|
| 7424 |
+
# ============================================================================
|
| 7425 |
+
# ADDITIVE: Prometheus /metrics exporter (szl-metrics-prom-patch)
|
| 7426 |
+
# Date: 2026-06-13 | Signed-off-by: Forge <forge@szlholdings.ai>
|
| 7427 |
+
# WHY: the UDS Package spec.monitor scrapes GET :7860/metrics, but with no metrics
|
| 7428 |
+
# route that path fell through to the SPA /{full_path:path} catch-all and returned
|
| 7429 |
+
# the app HTML shell (200 text/html) — so Prometheus harvested ZERO samples. This
|
| 7430 |
+
# serves REAL Prometheus exposition format at /metrics (process + HTTP request
|
| 7431 |
+
# counters/latency, all self-measured, none fabricated). Registered LAST (after
|
| 7432 |
+
# frontier_patch routes.clear()+extend) and FRONT-INSERTED so it beats the SPA
|
| 7433 |
+
# catch-all. Pure-stdlib, pass-through ASGI middleware (SSE-safe), try/except so it
|
| 7434 |
+
# can NEVER take the Space down. Shared module byte-identical a11oy<->killinchu.
|
| 7435 |
+
# ============================================================================
|
| 7436 |
+
try:
|
| 7437 |
+
import szl_metrics_prom as _szl_prom
|
| 7438 |
+
import sys as _prom_sys
|
| 7439 |
+
_prom_status = _szl_prom.register(app, ns="a11oy")
|
| 7440 |
+
print(f"[a11oy] szl_metrics_prom: {_prom_status}", file=_prom_sys.stderr)
|
| 7441 |
+
except Exception as _prom_e: # pragma: no cover
|
| 7442 |
+
print(f"[a11oy] szl_metrics_prom NOT registered (non-fatal): {_prom_e!r}",
|
| 7443 |
+
file=__import__("sys").stderr)
|
| 7444 |
+
# ============================================================================
|
| 7445 |
+
# END: Prometheus /metrics exporter
|
| 7446 |
+
# ============================================================================
|
| 7447 |
+
|
| 7448 |
+
|
| 7449 |
if __name__ == "__main__":
|
| 7450 |
import uvicorn
|
| 7451 |
port = int(os.environ.get("PORT", "7860"))
|
szl_metrics_prom.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""szl_metrics_prom.py — real Prometheus /metrics exporter (shared, byte-identical
|
| 2 |
+
a11oy <-> killinchu).
|
| 3 |
+
|
| 4 |
+
WHY: the UDS Package `spec.monitor` for both flagships scrapes `GET :7860/metrics`,
|
| 5 |
+
but with no metrics route that path fell through to the SPA `/{full_path:path}`
|
| 6 |
+
catch-all and returned the app's HTML shell (HTTP 200, content-type text/html).
|
| 7 |
+
A Prometheus scrape therefore connected (up=1) yet harvested ZERO usable samples,
|
| 8 |
+
so the dashboards stayed empty.
|
| 9 |
+
|
| 10 |
+
This module fixes that by serving genuine Prometheus exposition format at `/metrics`.
|
| 11 |
+
All metrics are REAL and self-measured at runtime — no fabricated values:
|
| 12 |
+
* szl_build_info{flagship,python} — info gauge (=1)
|
| 13 |
+
* szl_process_start_time_seconds — process start (unix epoch)
|
| 14 |
+
* szl_process_uptime_seconds — now - start
|
| 15 |
+
* szl_process_resident_memory_bytes — RSS from /proc/self/statm
|
| 16 |
+
* szl_process_open_fds — len(/proc/self/fd)
|
| 17 |
+
* szl_http_requests_in_progress — in-flight requests gauge
|
| 18 |
+
* szl_http_requests_total{method,code} — request counter
|
| 19 |
+
* szl_http_request_duration_seconds (histogram) — request latency
|
| 20 |
+
* szl_routes_registered — number of registered routes
|
| 21 |
+
|
| 22 |
+
Design constraints honored:
|
| 23 |
+
* Pure stdlib (threading/time/os/sys) + starlette.responses.Response only.
|
| 24 |
+
* Request accounting uses a pass-through ASGI middleware (NOT BaseHTTPMiddleware)
|
| 25 |
+
so Server-Sent-Events / streaming responses are never buffered or broken.
|
| 26 |
+
* register() front-inserts the /metrics route so it wins over the SPA catch-all,
|
| 27 |
+
and is safe to call LAST (after any frontier_patch routes.clear()+extend).
|
| 28 |
+
* try/except guarded end-to-end — can never take the Space down.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
from __future__ import annotations
|
| 32 |
+
|
| 33 |
+
import os
|
| 34 |
+
import sys
|
| 35 |
+
import time
|
| 36 |
+
import threading
|
| 37 |
+
|
| 38 |
+
_START_EPOCH = time.time()
|
| 39 |
+
_LOCK = threading.Lock()
|
| 40 |
+
|
| 41 |
+
# request counters keyed by (method, status_code_str)
|
| 42 |
+
_req_total: "dict[tuple[str, str], int]" = {}
|
| 43 |
+
_in_progress = 0
|
| 44 |
+
|
| 45 |
+
# global latency histogram (unlabeled to keep cardinality bounded)
|
| 46 |
+
_BUCKETS = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
|
| 47 |
+
_bucket_counts = [0] * len(_BUCKETS) # cumulative: count of obs with dur <= BUCKETS[i]
|
| 48 |
+
_lat_sum = 0.0
|
| 49 |
+
_lat_count = 0
|
| 50 |
+
|
| 51 |
+
_flagship = "unknown"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _observe(method: str, status, dur: float) -> None:
|
| 55 |
+
"""Record one completed HTTP request. Thread-safe."""
|
| 56 |
+
global _lat_sum, _lat_count
|
| 57 |
+
code = str(status if status else 0)
|
| 58 |
+
with _LOCK:
|
| 59 |
+
key = (method or "UNKNOWN", code)
|
| 60 |
+
_req_total[key] = _req_total.get(key, 0) + 1
|
| 61 |
+
_lat_sum += dur
|
| 62 |
+
_lat_count += 1
|
| 63 |
+
for i, b in enumerate(_BUCKETS):
|
| 64 |
+
if dur <= b:
|
| 65 |
+
_bucket_counts[i] += 1
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _esc(v: str) -> str:
|
| 69 |
+
"""Escape a Prometheus label VALUE (backslash, double-quote, newline)."""
|
| 70 |
+
return str(v).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _proc_metrics() -> "dict[str, int]":
|
| 74 |
+
out: "dict[str, int]" = {}
|
| 75 |
+
try:
|
| 76 |
+
with open("/proc/self/statm") as f:
|
| 77 |
+
pages = int(f.read().split()[1]) # resident pages
|
| 78 |
+
out["rss"] = pages * os.sysconf("SC_PAGE_SIZE")
|
| 79 |
+
except Exception:
|
| 80 |
+
pass
|
| 81 |
+
try:
|
| 82 |
+
out["open_fds"] = len(os.listdir("/proc/self/fd"))
|
| 83 |
+
except Exception:
|
| 84 |
+
pass
|
| 85 |
+
return out
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def render(app=None) -> str:
|
| 89 |
+
"""Build the Prometheus exposition text from the live counters."""
|
| 90 |
+
now = time.time()
|
| 91 |
+
pyver = "%d.%d.%d" % sys.version_info[:3]
|
| 92 |
+
lines: "list[str]" = []
|
| 93 |
+
|
| 94 |
+
lines.append("# HELP szl_build_info Flagship build/runtime info (constant 1).")
|
| 95 |
+
lines.append("# TYPE szl_build_info gauge")
|
| 96 |
+
lines.append(
|
| 97 |
+
'szl_build_info{flagship="%s",python="%s"} 1'
|
| 98 |
+
% (_esc(_flagship), _esc(pyver))
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
lines.append("# HELP szl_process_start_time_seconds Process start time (unix epoch seconds).")
|
| 102 |
+
lines.append("# TYPE szl_process_start_time_seconds gauge")
|
| 103 |
+
lines.append("szl_process_start_time_seconds %r" % _START_EPOCH)
|
| 104 |
+
|
| 105 |
+
lines.append("# HELP szl_process_uptime_seconds Seconds since process start.")
|
| 106 |
+
lines.append("# TYPE szl_process_uptime_seconds gauge")
|
| 107 |
+
lines.append("szl_process_uptime_seconds %r" % (now - _START_EPOCH))
|
| 108 |
+
|
| 109 |
+
proc = _proc_metrics()
|
| 110 |
+
if "rss" in proc:
|
| 111 |
+
lines.append("# HELP szl_process_resident_memory_bytes Resident memory (RSS) in bytes.")
|
| 112 |
+
lines.append("# TYPE szl_process_resident_memory_bytes gauge")
|
| 113 |
+
lines.append("szl_process_resident_memory_bytes %d" % proc["rss"])
|
| 114 |
+
if "open_fds" in proc:
|
| 115 |
+
lines.append("# HELP szl_process_open_fds Number of open file descriptors.")
|
| 116 |
+
lines.append("# TYPE szl_process_open_fds gauge")
|
| 117 |
+
lines.append("szl_process_open_fds %d" % proc["open_fds"])
|
| 118 |
+
|
| 119 |
+
with _LOCK:
|
| 120 |
+
in_prog = _in_progress
|
| 121 |
+
req_snapshot = dict(_req_total)
|
| 122 |
+
buckets = list(_bucket_counts)
|
| 123 |
+
lat_sum = _lat_sum
|
| 124 |
+
lat_count = _lat_count
|
| 125 |
+
|
| 126 |
+
lines.append("# HELP szl_http_requests_in_progress In-flight HTTP requests.")
|
| 127 |
+
lines.append("# TYPE szl_http_requests_in_progress gauge")
|
| 128 |
+
lines.append("szl_http_requests_in_progress %d" % in_prog)
|
| 129 |
+
|
| 130 |
+
lines.append("# HELP szl_http_requests_total Total HTTP requests handled.")
|
| 131 |
+
lines.append("# TYPE szl_http_requests_total counter")
|
| 132 |
+
if req_snapshot:
|
| 133 |
+
for (method, code), n in sorted(req_snapshot.items()):
|
| 134 |
+
lines.append(
|
| 135 |
+
'szl_http_requests_total{method="%s",code="%s"} %d'
|
| 136 |
+
% (_esc(method), _esc(code), n)
|
| 137 |
+
)
|
| 138 |
+
else:
|
| 139 |
+
# Emit a zero series so the metric always exists for the scraper.
|
| 140 |
+
lines.append('szl_http_requests_total{method="GET",code="200"} 0')
|
| 141 |
+
|
| 142 |
+
lines.append("# HELP szl_http_request_duration_seconds HTTP request latency.")
|
| 143 |
+
lines.append("# TYPE szl_http_request_duration_seconds histogram")
|
| 144 |
+
for i, b in enumerate(_BUCKETS):
|
| 145 |
+
lines.append(
|
| 146 |
+
'szl_http_request_duration_seconds_bucket{le="%s"} %d' % (b, buckets[i])
|
| 147 |
+
)
|
| 148 |
+
lines.append(
|
| 149 |
+
'szl_http_request_duration_seconds_bucket{le="+Inf"} %d' % lat_count
|
| 150 |
+
)
|
| 151 |
+
lines.append("szl_http_request_duration_seconds_sum %r" % lat_sum)
|
| 152 |
+
lines.append("szl_http_request_duration_seconds_count %d" % lat_count)
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
n_routes = len(app.router.routes) if app is not None else 0
|
| 156 |
+
except Exception:
|
| 157 |
+
n_routes = 0
|
| 158 |
+
lines.append("# HELP szl_routes_registered Number of registered application routes.")
|
| 159 |
+
lines.append("# TYPE szl_routes_registered gauge")
|
| 160 |
+
lines.append("szl_routes_registered %d" % n_routes)
|
| 161 |
+
|
| 162 |
+
return "\n".join(lines) + "\n"
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class _PromASGIMiddleware:
|
| 166 |
+
"""Pass-through ASGI middleware that counts requests + latency WITHOUT buffering
|
| 167 |
+
the response body, so SSE / streaming responses keep working."""
|
| 168 |
+
|
| 169 |
+
def __init__(self, app):
|
| 170 |
+
self.app = app
|
| 171 |
+
|
| 172 |
+
async def __call__(self, scope, receive, send):
|
| 173 |
+
if scope.get("type") != "http":
|
| 174 |
+
await self.app(scope, receive, send)
|
| 175 |
+
return
|
| 176 |
+
global _in_progress
|
| 177 |
+
method = scope.get("method", "")
|
| 178 |
+
status = {"code": 0}
|
| 179 |
+
|
| 180 |
+
async def _send(message):
|
| 181 |
+
if message.get("type") == "http.response.start":
|
| 182 |
+
status["code"] = message.get("status", 0)
|
| 183 |
+
await send(message)
|
| 184 |
+
|
| 185 |
+
with _LOCK:
|
| 186 |
+
_in_progress += 1
|
| 187 |
+
t0 = time.perf_counter()
|
| 188 |
+
try:
|
| 189 |
+
await self.app(scope, receive, _send)
|
| 190 |
+
finally:
|
| 191 |
+
dur = time.perf_counter() - t0
|
| 192 |
+
with _LOCK:
|
| 193 |
+
_in_progress -= 1
|
| 194 |
+
_observe(method, status["code"] or 500, dur)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def register(app, ns: str = "a11oy") -> str:
|
| 198 |
+
"""Install the request-accounting middleware and the /metrics route.
|
| 199 |
+
|
| 200 |
+
Call this LAST in serve.py (after any frontier_patch routes.clear()+extend):
|
| 201 |
+
the route is front-inserted so it always beats the SPA /{full_path:path}
|
| 202 |
+
catch-all. Idempotent-ish: a second call adds a second (harmless) route, so
|
| 203 |
+
register exactly once per app.
|
| 204 |
+
"""
|
| 205 |
+
global _flagship
|
| 206 |
+
_flagship = ns
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
from starlette.responses import Response
|
| 210 |
+
except Exception as e: # pragma: no cover
|
| 211 |
+
return "unavailable: %r" % (e,)
|
| 212 |
+
|
| 213 |
+
mw_ok = False
|
| 214 |
+
try:
|
| 215 |
+
app.add_middleware(_PromASGIMiddleware)
|
| 216 |
+
mw_ok = True
|
| 217 |
+
except Exception as e:
|
| 218 |
+
print("[%s] prom metrics middleware NOT added (non-fatal): %r" % (ns, e),
|
| 219 |
+
file=sys.stderr)
|
| 220 |
+
|
| 221 |
+
n_before = len(app.router.routes)
|
| 222 |
+
|
| 223 |
+
@app.get("/metrics")
|
| 224 |
+
async def _szl_prom_metrics(): # noqa
|
| 225 |
+
body = render(app)
|
| 226 |
+
return Response(
|
| 227 |
+
content=body,
|
| 228 |
+
media_type="text/plain; version=0.0.4; charset=utf-8",
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
new = app.router.routes[n_before:]
|
| 232 |
+
del app.router.routes[n_before:]
|
| 233 |
+
app.router.routes[0:0] = new
|
| 234 |
+
|
| 235 |
+
print(
|
| 236 |
+
"[%s] szl_metrics_prom: GET /metrics registered (Prometheus exposition; "
|
| 237 |
+
"request-accounting middleware=%s) [moved %d route(s) to front]"
|
| 238 |
+
% (ns, "on" if mw_ok else "off", len(new)),
|
| 239 |
+
file=sys.stderr,
|
| 240 |
+
)
|
| 241 |
+
return "ok: /metrics middleware=%s" % ("on" if mw_ok else "off")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
if __name__ == "__main__":
|
| 245 |
+
# Self-test: render with a couple of synthetic observations.
|
| 246 |
+
_flagship = "selftest"
|
| 247 |
+
_observe("GET", 200, 0.003)
|
| 248 |
+
_observe("GET", 200, 0.07)
|
| 249 |
+
_observe("POST", 500, 1.2)
|
| 250 |
+
text = render(None)
|
| 251 |
+
print(text)
|
| 252 |
+
assert "szl_build_info{" in text
|
| 253 |
+
assert "szl_http_requests_total{method=\"GET\",code=\"200\"} 2" in text
|
| 254 |
+
assert "szl_http_request_duration_seconds_bucket{le=\"+Inf\"} 3" in text
|
| 255 |
+
assert "szl_http_request_duration_seconds_count 3" in text
|
| 256 |
+
assert text.endswith("\n")
|
| 257 |
+
# bucket monotonicity
|
| 258 |
+
import re as _re
|
| 259 |
+
cum = [int(m) for m in _re.findall(r'_bucket\{le="[^+][^"]*"\} (\d+)', text)]
|
| 260 |
+
assert cum == sorted(cum), cum
|
| 261 |
+
print("SELFTEST OK", file=sys.stderr)
|