155 lines
4.8 KiB
Python
155 lines
4.8 KiB
Python
"""Application observability: metrics and request correlation.
|
|
|
|
This module owns Prometheus metrics and request-id generation so that the
|
|
rest of the backend can stay focused on business logic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from contextvars import ContextVar
|
|
from typing import Any
|
|
|
|
from fastapi import Request, Response
|
|
from prometheus_client import (
|
|
CONTENT_TYPE_LATEST,
|
|
Counter,
|
|
Gauge,
|
|
Histogram,
|
|
generate_latest,
|
|
)
|
|
|
|
# Context-local request id for code paths that cannot receive a Request object.
|
|
_current_request_id: ContextVar[str | None] = ContextVar("request_id", default=None)
|
|
|
|
# Request metrics
|
|
REQUESTS_TOTAL = Counter(
|
|
"manage_api_requests_total",
|
|
"Total API requests",
|
|
["method", "path", "status_code"],
|
|
)
|
|
REQUEST_DURATION = Histogram(
|
|
"manage_api_request_duration_seconds",
|
|
"API request duration",
|
|
["method", "path"],
|
|
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
|
|
)
|
|
|
|
# Operation metrics
|
|
SSH_COMMANDS_TOTAL = Counter(
|
|
"manage_ssh_commands_total",
|
|
"Total SSH/local commands executed",
|
|
["machine_id", "action", "status"],
|
|
)
|
|
SSH_COMMAND_DURATION = Histogram(
|
|
"manage_ssh_command_duration_seconds",
|
|
"SSH/local command duration",
|
|
["machine_id", "action"],
|
|
buckets=(0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0),
|
|
)
|
|
|
|
MEDIA_INDEX_BUILDS_TOTAL = Counter(
|
|
"manage_media_index_builds_total",
|
|
"Total media index build attempts",
|
|
["status"],
|
|
)
|
|
MEDIA_INDEX_BUILD_DURATION = Histogram(
|
|
"manage_media_index_build_duration_seconds",
|
|
"Media index build duration",
|
|
buckets=(1.0, 5.0, 15.0, 30.0, 60.0, 120.0, 300.0, 600.0),
|
|
)
|
|
|
|
BACKUP_RUNS_TOTAL = Counter(
|
|
"manage_backup_runs_total",
|
|
"Total backup runs",
|
|
["job_name", "status"],
|
|
)
|
|
|
|
BACKUP_RUNS_LAST_SUCCESS = Gauge(
|
|
"manage_backup_runs_last_success_timestamp",
|
|
"Unix timestamp of the last successful backup run per job",
|
|
["job_name"],
|
|
)
|
|
|
|
MAIL_QUEUE_SIZE = Counter(
|
|
"manage_mail_queue_messages_total",
|
|
"Total messages enqueued",
|
|
["status"],
|
|
)
|
|
|
|
|
|
def set_current_request_id(request_id: str | None) -> None:
|
|
"""Set the context-local request id."""
|
|
_current_request_id.set(request_id)
|
|
|
|
|
|
def get_current_request_id() -> str | None:
|
|
"""Return the current context-local request id or None."""
|
|
return _current_request_id.get()
|
|
|
|
|
|
def generate_request_id() -> str:
|
|
"""Return a short unique request id."""
|
|
return uuid.uuid4().hex[:16]
|
|
|
|
|
|
def get_request_id(request: Request | None = None) -> str:
|
|
"""Resolve a request id from the request header, context, or a new value."""
|
|
if request is not None:
|
|
header = request.headers.get("x-request-id") or request.headers.get("x-correlation-id")
|
|
if header:
|
|
return header.strip()
|
|
existing = _current_request_id.get()
|
|
if existing:
|
|
return existing
|
|
new_id = generate_request_id()
|
|
_current_request_id.set(new_id)
|
|
return new_id
|
|
|
|
|
|
def metrics_payload() -> tuple[bytes, str]:
|
|
"""Return the Prometheus metrics payload and content type."""
|
|
return generate_latest(), CONTENT_TYPE_LATEST
|
|
|
|
|
|
def record_request(request: Request, response: Response, duration_seconds: float) -> None:
|
|
"""Record Prometheus metrics for a completed request."""
|
|
status = str(response.status_code)
|
|
path = request.url.path
|
|
method = request.method
|
|
REQUESTS_TOTAL.labels(method=method, path=path, status_code=status).inc()
|
|
REQUEST_DURATION.labels(method=method, path=path).observe(duration_seconds)
|
|
|
|
|
|
def record_ssh_command(machine_id: str, action: str, status: str, duration_seconds: float) -> None:
|
|
"""Record metrics for an SSH/local command."""
|
|
SSH_COMMANDS_TOTAL.labels(machine_id=machine_id or "unknown", action=action, status=status).inc()
|
|
SSH_COMMAND_DURATION.labels(machine_id=machine_id or "unknown", action=action).observe(duration_seconds)
|
|
|
|
|
|
def record_media_index_build(status: str, duration_seconds: float | None = None) -> None:
|
|
"""Record metrics for a media index build."""
|
|
MEDIA_INDEX_BUILDS_TOTAL.labels(status=status).inc()
|
|
if duration_seconds is not None:
|
|
MEDIA_INDEX_BUILD_DURATION.observe(duration_seconds)
|
|
|
|
|
|
def record_backup_run(job_name: str, status: str, success: bool = False) -> None:
|
|
"""Record metrics for a backup run."""
|
|
job_name = job_name or "unknown"
|
|
BACKUP_RUNS_TOTAL.labels(job_name=job_name, status=status).inc()
|
|
if success:
|
|
BACKUP_RUNS_LAST_SUCCESS.labels(job_name=job_name).set_to_current_time()
|
|
|
|
|
|
def record_mail_queue(status: str) -> None:
|
|
"""Record metrics for a mail queue message outcome."""
|
|
MAIL_QUEUE_SIZE.labels(status=status).inc()
|
|
|
|
|
|
def log_extra(request: Request | None = None, **kwargs: Any) -> dict[str, Any]:
|
|
"""Build a standard extra dict for structured logging."""
|
|
extra: dict[str, Any] = {"request_id": get_request_id(request)}
|
|
extra.update(kwargs)
|
|
return extra
|