betterwithage commited on
Commit
3651bd0
·
verified ·
1 Parent(s): 6e497e5

chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)

Browse files

Automated 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.

Files changed (3) hide show
  1. Dockerfile +1 -1
  2. serve.py +25 -0
  3. 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)