refactor(backend): external logging metrics, required mode, health checks
- Add external_logging_init_failures counter - Add external_log_required flag, raise if init fails and required - Health endpoint: add external_logging status check - Blocklist service: enrich with metadata fields, update import logic - Health check task: add runtime_state dependency, fix return typing - Metrics: add Histogram for request latencies - Frontend: align BlocklistImportLogSection props - Docs: update deployment guide, remove stale tasks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -22,6 +22,7 @@ registered *before* the ``/{id}`` routes so FastAPI resolves them correctly.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, Query, Request, status
|
||||
|
||||
from app.dependencies import (
|
||||
@@ -34,7 +35,12 @@ from app.dependencies import (
|
||||
SchedulerDep,
|
||||
SettingsDep,
|
||||
)
|
||||
from app.exceptions import BadRequestError, BlocklistSourceNotFoundError
|
||||
from app.exceptions import (
|
||||
BadRequestError,
|
||||
BlocklistSourceAlreadyExistsError,
|
||||
BlocklistSourceNotFoundError,
|
||||
RateLimitError,
|
||||
)
|
||||
from app.mappers import blocklist_mappers
|
||||
from app.models.blocklist import (
|
||||
BlocklistListResponse,
|
||||
@@ -53,11 +59,13 @@ from app.utils.constants import DEFAULT_PAGE_SIZE, RATE_LIMIT_BLOCKLIST_IMPORT_R
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/api/v1/blocklists", tags=["Blocklists"])
|
||||
|
||||
# Rate limit bucket constants
|
||||
#: Rate limit bucket constants
|
||||
_BLOCKLIST_IMPORT_BUCKET = "blocklist:import"
|
||||
# 3600 seconds per hour
|
||||
_HOUR = 3600
|
||||
|
||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
|
||||
|
||||
def _check_blocklist_import_rate_limit(
|
||||
request: Request,
|
||||
@@ -72,10 +80,6 @@ def _check_blocklist_import_rate_limit(
|
||||
_BLOCKLIST_IMPORT_BUCKET, client_ip, RATE_LIMIT_BLOCKLIST_IMPORT_REQUESTS, _HOUR
|
||||
)
|
||||
if not is_allowed:
|
||||
from app.exceptions import RateLimitError
|
||||
import structlog
|
||||
|
||||
log = structlog.get_logger()
|
||||
log.warning(
|
||||
"blocklist_import_rate_limit_exceeded",
|
||||
client_ip=client_ip,
|
||||
@@ -128,6 +132,7 @@ async def list_blocklists(
|
||||
201: {"description": "Blocklist source created", "model": BlocklistSource},
|
||||
400: {"description": "URL validation failed"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "A blocklist source with this URL already exists"},
|
||||
},
|
||||
)
|
||||
async def create_blocklist(
|
||||
@@ -154,6 +159,8 @@ async def create_blocklist(
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise BadRequestError(str(exc)) from exc
|
||||
except BlocklistSourceAlreadyExistsError as exc:
|
||||
raise exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -128,6 +128,26 @@ async def health_check(
|
||||
ComponentHealth(name="fail2ban", healthy=False, message="Socket not reachable"),
|
||||
)
|
||||
|
||||
# --- External logging check ---
|
||||
external_log_state: Literal["ok", "error", "disabled", "unknown"] = "unknown"
|
||||
effective_settings: Settings = (
|
||||
app_state.runtime_settings if app_state.runtime_settings is not None else app_state.settings
|
||||
)
|
||||
try:
|
||||
ext_log_failed = getattr(app_state.runtime_state, "external_log_init_failed", False)
|
||||
if effective_settings.external_logging_enabled and effective_settings.external_logging_provider:
|
||||
if ext_log_failed:
|
||||
external_log_state = "error"
|
||||
components.append(
|
||||
ComponentHealth(name="external_logging", healthy=False, message="Handler initialization failed"),
|
||||
)
|
||||
else:
|
||||
external_log_state = "ok"
|
||||
else:
|
||||
external_log_state = "disabled"
|
||||
except AttributeError: # pragma: no cover - defensive
|
||||
external_log_state = "unknown"
|
||||
|
||||
# --- Overall status ---
|
||||
overall_status: Literal["ok", "degraded", "unavailable"]
|
||||
if not fail2ban_online:
|
||||
@@ -148,6 +168,7 @@ async def health_check(
|
||||
database="ok" if db_healthy else "error",
|
||||
scheduler=scheduler_state,
|
||||
cache=cache_state,
|
||||
external_logging=external_log_state,
|
||||
components=components,
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user