feat: ADD Prometheus endpoint for simple app monitoring #30
This commit is contained in:
parent
e42e55c7e4
commit
082a9f81d7
2 changed files with 131 additions and 1 deletions
100
app.py
100
app.py
|
|
@ -17,6 +17,8 @@ RETENTION_HOURS = int(os.getenv("RETENTION_HOURS", "48")) # Default 48 hours
|
|||
MAX_ROOMS = int(os.getenv("MAX_ROOMS", "10000"))
|
||||
TRUST_PROXY = os.getenv("TRUST_PROXY", "false").lower() == "true"
|
||||
DESCRIPTION = os.getenv("DESCRIPTION", "powered by aukpad.com")
|
||||
ENABLE_METRICS = os.getenv("ENABLE_METRICS", "false").lower() == "true"
|
||||
METRICS_KEY = os.getenv("METRICS_KEY", "") # empty = no auth on /system/metrics
|
||||
START_TIME = time.time()
|
||||
|
||||
DOC_ID_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$")
|
||||
|
|
@ -674,6 +676,104 @@ def get_system_info():
|
|||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
def _metric(out: list, name: str, value, help_text: str, labels: str = ""):
|
||||
out.append(f"# HELP {name} {help_text}")
|
||||
out.append(f"# TYPE {name} gauge")
|
||||
out.append(f"{name}{labels} {value}")
|
||||
|
||||
def _esc(v: str) -> str:
|
||||
# Prometheus label values escape backslash, double quote and newline.
|
||||
return v.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||
|
||||
@app.get("/system/metrics", include_in_schema=False)
|
||||
def metrics(request: Request):
|
||||
# Off by default: these numbers are usage data, not for public instances.
|
||||
if not ENABLE_METRICS:
|
||||
raise HTTPException(status_code=404, detail="Not Found")
|
||||
|
||||
if METRICS_KEY:
|
||||
scheme, _, token = request.headers.get("authorization", "").partition(" ")
|
||||
# encode() both sides: compare_digest rejects non-ASCII str, and the
|
||||
# header is attacker-controlled.
|
||||
if scheme.lower() != "bearer" or not secrets.compare_digest(token.encode(), METRICS_KEY.encode()):
|
||||
return PlainTextResponse("Unauthorized", status_code=401,
|
||||
headers={"WWW-Authenticate": "Bearer"})
|
||||
|
||||
now = time.time()
|
||||
# Snapshot: the cleanup thread mutates rooms without a lock.
|
||||
snapshot = list(rooms.values())
|
||||
|
||||
peers = authed = active = protected = versions = 0
|
||||
total_bytes = max_bytes = 0
|
||||
idle_max = 0.0
|
||||
for room in snapshot:
|
||||
n = len(room.get("peers", ()))
|
||||
peers += n
|
||||
authed += len(room.get("authed_peers", ()))
|
||||
active += 1 if n else 0
|
||||
protected += 1 if room.get("pw_hash") else 0
|
||||
versions += room.get("ver", 0)
|
||||
size = len(room.get("text", "").encode("utf-8"))
|
||||
total_bytes += size
|
||||
max_bytes = max(max_bytes, size)
|
||||
idle_max = max(idle_max, now - room.get("last_access", now))
|
||||
|
||||
ip_counts = [c for c in list(connections_per_ip.values()) if c > 0]
|
||||
hour_ago = now - 3600
|
||||
auth_cutoff = now - AUTH_FAILURE_WINDOW
|
||||
# Both dicts prune lazily (only when that IP acts), so filter by age here.
|
||||
creations = sum(len([t for t in ts if t > hour_ago]) for ts in list(rate_limits.values()))
|
||||
auth_fails = sum(len([t for t in ts if t > auth_cutoff]) for ts in list(failed_auth_attempts.values()))
|
||||
|
||||
out: list[str] = []
|
||||
_metric(out, "aukpad_rooms", len(snapshot), "Pads currently held in memory")
|
||||
_metric(out, "aukpad_rooms_active", active, "Pads with at least one connected peer")
|
||||
_metric(out, "aukpad_rooms_protected", protected, "Pads with a password set")
|
||||
_metric(out, "aukpad_peers_connected", peers, "Live WebSocket peers across all pads")
|
||||
_metric(out, "aukpad_peers_authenticated", authed, "Peers that passed pad authentication")
|
||||
_metric(out, "aukpad_stored_bytes", total_bytes, "Total UTF-8 bytes of all pad text")
|
||||
_metric(out, "aukpad_pad_bytes_mean", total_bytes // len(snapshot) if snapshot else 0,
|
||||
"Mean pad size in UTF-8 bytes")
|
||||
_metric(out, "aukpad_pad_bytes_max", max_bytes, "Largest single pad in UTF-8 bytes")
|
||||
_metric(out, "aukpad_pad_versions", versions,
|
||||
"Sum of all pad version counters; drops when pads are evicted")
|
||||
_metric(out, "aukpad_room_idle_seconds_max", round(idle_max, 1),
|
||||
"Seconds since last access of the least recently used pad")
|
||||
_metric(out, "aukpad_client_ips", len(ip_counts), "Distinct IPs holding WebSocket connections")
|
||||
_metric(out, "aukpad_ip_connections_max", max(ip_counts) if ip_counts else 0,
|
||||
"Connection count of the busiest single IP")
|
||||
_metric(out, "aukpad_pads_created_last_hour", creations,
|
||||
"Pad creations via POST in the last hour, across all IPs")
|
||||
_metric(out, "aukpad_auth_failures_recent", auth_fails,
|
||||
f"Failed pad password attempts in the last {AUTH_FAILURE_WINDOW}s")
|
||||
|
||||
_metric(out, "aukpad_cache_enabled", 1 if USE_VALKEY else 0, "Valkey/Redis cache configured")
|
||||
cache_up, cache_keys = 0, None
|
||||
if redis_client:
|
||||
try:
|
||||
redis_client.ping()
|
||||
cache_up = 1
|
||||
cache_keys = redis_client.dbsize()
|
||||
except Exception:
|
||||
cache_up = 0
|
||||
_metric(out, "aukpad_cache_up", cache_up, "Valkey/Redis answered PING")
|
||||
if cache_keys is not None:
|
||||
_metric(out, "aukpad_cache_keys", cache_keys,
|
||||
"Keys in the Valkey/Redis database (all keys, if the DB is shared)")
|
||||
|
||||
_metric(out, "aukpad_max_rooms", MAX_ROOMS, "Configured pad limit")
|
||||
_metric(out, "aukpad_max_text_bytes", MAX_TEXT_SIZE, "Configured per-pad size limit in bytes")
|
||||
_metric(out, "aukpad_max_connections_per_ip", MAX_CONNECTIONS_PER_IP,
|
||||
"Configured WebSocket connection limit per IP")
|
||||
_metric(out, "aukpad_retention_seconds", RETENTION_HOURS * 3600, "Configured pad retention")
|
||||
_metric(out, "aukpad_start_time_seconds", round(START_TIME, 3),
|
||||
"Unix start time; uptime is time() minus this")
|
||||
_metric(out, "aukpad_build_info", 1, "Instance description as a label",
|
||||
labels=f'{{description="{_esc(DESCRIPTION)}"}}')
|
||||
|
||||
return PlainTextResponse("\n".join(out) + "\n",
|
||||
media_type="text/plain; version=0.0.4; charset=utf-8")
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
def root():
|
||||
return RedirectResponse(url=f"/{random_id()}/", status_code=307)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue