From c8550ac82d1452ae221e626961865b3e9a70629d Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Mon, 27 Oct 2025 22:32:08 -0600 Subject: [PATCH] Added API Access Logging --- Data/Engine/Unit_Tests/conftest.py | 2 +- Data/Engine/config.py | 8 +++- Data/Engine/server.py | 45 ++++++++++++++++++- Data/Engine/services/API/__init__.py | 2 +- .../API/access_management/__init__.py | 0 .../services/API/assemblies/__init__.py | 0 Data/Engine/services/API/devices/__init__.py | 0 Data/Engine/services/API/filters/__init__.py | 0 .../services/API/scheduled_jobs/__init__.py | 0 Data/Engine/services/API/server/__init__.py | 0 Data/Engine/services/API/sites/__init__.py | 0 11 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 Data/Engine/services/API/access_management/__init__.py create mode 100644 Data/Engine/services/API/assemblies/__init__.py create mode 100644 Data/Engine/services/API/devices/__init__.py create mode 100644 Data/Engine/services/API/filters/__init__.py create mode 100644 Data/Engine/services/API/scheduled_jobs/__init__.py create mode 100644 Data/Engine/services/API/server/__init__.py create mode 100644 Data/Engine/services/API/sites/__init__.py diff --git a/Data/Engine/Unit_Tests/conftest.py b/Data/Engine/Unit_Tests/conftest.py index 4ddc44ef..bb617d38 100644 --- a/Data/Engine/Unit_Tests/conftest.py +++ b/Data/Engine/Unit_Tests/conftest.py @@ -133,7 +133,7 @@ def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[ "LOG_FILE": str(log_path), "ERROR_LOG_FILE": str(error_log_path), "STATIC_FOLDER": str(static_dir), - "API_GROUPS": ("core", "tokens", "enrollment"), + "API_GROUPS": ("core", "auth", "tokens", "enrollment"), } app, _socketio, _context = create_app(config) diff --git a/Data/Engine/config.py b/Data/Engine/config.py index 33369850..fb5cddf1 100644 --- a/Data/Engine/config.py +++ b/Data/Engine/config.py @@ -71,6 +71,7 @@ DEFAULT_DATABASE_PATH = PROJECT_ROOT / "database.db" LOG_ROOT = PROJECT_ROOT / "Logs" / "Engine" LOG_FILE_PATH = LOG_ROOT / "engine.log" ERROR_LOG_FILE_PATH = LOG_ROOT / "error.log" +API_LOG_FILE_PATH = LOG_ROOT / "api.log" def _ensure_parent(path: Path) -> None: @@ -176,6 +177,7 @@ class EngineSettings: tls_bundle_path: Optional[str] log_file: str error_log_file: str + api_log_file: str api_groups: Tuple[str, ...] raw: MutableMapping[str, Any] = field(default_factory=dict) @@ -259,11 +261,14 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine error_log_file = str(runtime_config.get("ERROR_LOG_FILE") or ERROR_LOG_FILE_PATH) _ensure_parent(Path(error_log_file)) + api_log_file = str(runtime_config.get("API_LOG_FILE") or API_LOG_FILE_PATH) + _ensure_parent(Path(api_log_file)) + api_groups = _parse_api_groups( runtime_config.get("API_GROUPS") or os.environ.get("BOREALIS_API_GROUPS") ) if not api_groups: - api_groups = ("tokens", "enrollment") + api_groups = ("auth", "tokens", "enrollment") settings = EngineSettings( database_path=database_path, @@ -278,6 +283,7 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine tls_bundle_path=tls_bundle_path if tls_bundle_path else None, log_file=str(log_file), error_log_file=str(error_log_file), + api_log_file=str(api_log_file), api_groups=api_groups, raw=runtime_config, ) diff --git a/Data/Engine/server.py b/Data/Engine/server.py index a059b232..d0dd4c23 100644 --- a/Data/Engine/server.py +++ b/Data/Engine/server.py @@ -11,9 +11,11 @@ from __future__ import annotations import importlib.util import logging +import time import ssl import sys from dataclasses import dataclass +from logging.handlers import TimedRotatingFileHandler from pathlib import Path from typing import Any, Mapping, Optional, Sequence, Tuple @@ -33,7 +35,7 @@ _require_dependency("flask_socketio", "Flask-SocketIO") import eventlet # type: ignore # noqa: E402 # pragma: no cover - import guarded above from eventlet import wsgi as eventlet_wsgi # type: ignore # noqa: E402 # pragma: no cover -from flask import Flask, request # noqa: E402 +from flask import Flask, g, request # noqa: E402 from flask_cors import CORS # noqa: E402 from flask_socketio import SocketIO # noqa: E402 from werkzeug.middleware.proxy_fix import ProxyFix # noqa: E402 @@ -117,6 +119,7 @@ class EngineContext: tls_bundle_path: Optional[str] config: Mapping[str, Any] api_groups: Sequence[str] + api_log_path: str __all__ = ["EngineContext", "create_app", "register_engine_api"] @@ -132,6 +135,7 @@ def _build_engine_context(settings: EngineSettings, logger: logging.Logger) -> E tls_bundle_path=settings.tls_bundle_path, config=settings.as_dict(), api_groups=settings.api_groups, + api_log_path=settings.api_log_file, ) @@ -193,6 +197,45 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke context = _build_engine_context(settings, logger) + api_logger = logging.getLogger("borealis.engine.api") + if not api_logger.handlers: + api_handler = TimedRotatingFileHandler( + context.api_log_path, + when="midnight", + backupCount=0, + encoding="utf-8", + ) + api_handler.setFormatter(logging.Formatter("%(asctime)s-%(name)s-%(levelname)s: %(message)s")) + api_logger.addHandler(api_handler) + api_logger.setLevel(logging.INFO) + api_logger.propagate = False + + @app.before_request + def _engine_api_start_timer() -> None: # pragma: no cover - runtime behaviour + if request.path.startswith("/api"): + g._engine_api_start = time.perf_counter() + + @app.after_request + def _engine_api_log(response): # pragma: no cover - runtime behaviour + if request.path.startswith("/api"): + start = getattr(g, "_engine_api_start", None) + duration_ms = None + if start is not None: + duration_ms = (time.perf_counter() - start) * 1000.0 + client_ip = (request.headers.get("X-Forwarded-For") or request.remote_addr or "-").split(",")[0].strip() + status = response.status_code + success = 200 <= status < 400 + api_logger.info( + "client=%s method=%s path=%s status=%s success=%s duration_ms=%.2f", + client_ip, + request.method, + request.full_path.rstrip("?"), + status, + "true" if success else "false", + duration_ms if duration_ms is not None else 0.0, + ) + return response + from .services import API, WebSocket, WebUI # Local import to avoid circular deps during bootstrap API.register_api(app, context) diff --git a/Data/Engine/services/API/__init__.py b/Data/Engine/services/API/__init__.py index 8fbaaa69..a239b479 100644 --- a/Data/Engine/services/API/__init__.py +++ b/Data/Engine/services/API/__init__.py @@ -21,7 +21,7 @@ from Modules.enrollment.nonce_store import NonceCache from Modules.tokens import routes as token_routes from ...server import EngineContext -from .authentication import register_auth +from .access_management.login import register_auth DEFAULT_API_GROUPS: Sequence[str] = ("auth", "tokens", "enrollment") diff --git a/Data/Engine/services/API/access_management/__init__.py b/Data/Engine/services/API/access_management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Data/Engine/services/API/assemblies/__init__.py b/Data/Engine/services/API/assemblies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Data/Engine/services/API/devices/__init__.py b/Data/Engine/services/API/devices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Data/Engine/services/API/filters/__init__.py b/Data/Engine/services/API/filters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Data/Engine/services/API/scheduled_jobs/__init__.py b/Data/Engine/services/API/scheduled_jobs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Data/Engine/services/API/server/__init__.py b/Data/Engine/services/API/server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Data/Engine/services/API/sites/__init__.py b/Data/Engine/services/API/sites/__init__.py new file mode 100644 index 00000000..e69de29b