Expose protected operability metrics
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

This commit is contained in:
tegwick 2026-07-29 23:52:06 +02:00
parent a336f594e7
commit 2bcda7f26f
3 changed files with 56 additions and 0 deletions

View file

@ -91,6 +91,11 @@ class PortalApplication:
if path == "/readyz":
report = self.service.readiness()
return self._json(start_response, "200 OK" if report.ready else "503 Service Unavailable", _jsonable(report), correlation_id)
if path == "/metrics":
supplied = str(environ.get("HTTP_X_USER_ENGINE_PROXY_SECRET", ""))
if not secrets.compare_digest(supplied, self.trusted_proxy_secret):
raise AuthorizationDenied("metrics require the trusted workload marker")
return self._metrics(start_response, correlation_id)
if path in {"/login", "/oidc/start"}:
location = self.oidc_client.begin() if self.oidc_client else self.login_url
start_response("303 See Other", [("Location", location), *self._security_headers(correlation_id)])
@ -536,6 +541,35 @@ a:focus-visible,input:focus-visible,select:focus-visible,button:focus-visible{{o
start_response(status, [("Content-Type", "application/json"), ("Content-Length", str(len(data))), *self._security_headers(correlation_id)])
return [data]
def _metrics(
self, start_response: StartResponse, correlation_id: str
) -> list[bytes]:
report = self.service.readiness()
counts = self.service.operability_snapshot().metrics
lines = [
"# HELP user_engine_ready Whether runtime dependencies are ready.",
"# TYPE user_engine_ready gauge",
f"user_engine_ready {1 if report.ready else 0}",
"# HELP user_engine_records Durable logical record counts by kind.",
"# TYPE user_engine_records gauge",
]
for kind, count in sorted(counts.items()):
safe_kind = "".join(
character for character in str(kind)
if character.isalnum() or character in "_-"
)
lines.append(f'user_engine_records{{kind="{safe_kind}"}} {int(count)}')
data = ("\n".join(lines) + "\n").encode()
start_response(
"200 OK",
[
("Content-Type", "text/plain; version=0.0.4; charset=utf-8"),
("Content-Length", str(len(data))),
*self._security_headers(correlation_id),
],
)
return [data]
def _error(self, start_response: StartResponse, status: str, code: str, message: str, correlation_id: str) -> list[bytes]:
return self._json(start_response, status, {"error": {"code": code, "message": message, "correlation_id": correlation_id}}, correlation_id)