Added API Access Logging

This commit is contained in:
2025-10-27 22:32:08 -06:00
parent 215a054979
commit c8550ac82d
11 changed files with 53 additions and 4 deletions

View File

@@ -133,7 +133,7 @@ def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[
"LOG_FILE": str(log_path), "LOG_FILE": str(log_path),
"ERROR_LOG_FILE": str(error_log_path), "ERROR_LOG_FILE": str(error_log_path),
"STATIC_FOLDER": str(static_dir), "STATIC_FOLDER": str(static_dir),
"API_GROUPS": ("core", "tokens", "enrollment"), "API_GROUPS": ("core", "auth", "tokens", "enrollment"),
} }
app, _socketio, _context = create_app(config) app, _socketio, _context = create_app(config)

View File

@@ -71,6 +71,7 @@ DEFAULT_DATABASE_PATH = PROJECT_ROOT / "database.db"
LOG_ROOT = PROJECT_ROOT / "Logs" / "Engine" LOG_ROOT = PROJECT_ROOT / "Logs" / "Engine"
LOG_FILE_PATH = LOG_ROOT / "engine.log" LOG_FILE_PATH = LOG_ROOT / "engine.log"
ERROR_LOG_FILE_PATH = LOG_ROOT / "error.log" ERROR_LOG_FILE_PATH = LOG_ROOT / "error.log"
API_LOG_FILE_PATH = LOG_ROOT / "api.log"
def _ensure_parent(path: Path) -> None: def _ensure_parent(path: Path) -> None:
@@ -176,6 +177,7 @@ class EngineSettings:
tls_bundle_path: Optional[str] tls_bundle_path: Optional[str]
log_file: str log_file: str
error_log_file: str error_log_file: str
api_log_file: str
api_groups: Tuple[str, ...] api_groups: Tuple[str, ...]
raw: MutableMapping[str, Any] = field(default_factory=dict) 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) error_log_file = str(runtime_config.get("ERROR_LOG_FILE") or ERROR_LOG_FILE_PATH)
_ensure_parent(Path(error_log_file)) _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( api_groups = _parse_api_groups(
runtime_config.get("API_GROUPS") or os.environ.get("BOREALIS_API_GROUPS") runtime_config.get("API_GROUPS") or os.environ.get("BOREALIS_API_GROUPS")
) )
if not api_groups: if not api_groups:
api_groups = ("tokens", "enrollment") api_groups = ("auth", "tokens", "enrollment")
settings = EngineSettings( settings = EngineSettings(
database_path=database_path, 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, tls_bundle_path=tls_bundle_path if tls_bundle_path else None,
log_file=str(log_file), log_file=str(log_file),
error_log_file=str(error_log_file), error_log_file=str(error_log_file),
api_log_file=str(api_log_file),
api_groups=api_groups, api_groups=api_groups,
raw=runtime_config, raw=runtime_config,
) )

View File

@@ -11,9 +11,11 @@ from __future__ import annotations
import importlib.util import importlib.util
import logging import logging
import time
import ssl import ssl
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path from pathlib import Path
from typing import Any, Mapping, Optional, Sequence, Tuple 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 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 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_cors import CORS # noqa: E402
from flask_socketio import SocketIO # noqa: E402 from flask_socketio import SocketIO # noqa: E402
from werkzeug.middleware.proxy_fix import ProxyFix # noqa: E402 from werkzeug.middleware.proxy_fix import ProxyFix # noqa: E402
@@ -117,6 +119,7 @@ class EngineContext:
tls_bundle_path: Optional[str] tls_bundle_path: Optional[str]
config: Mapping[str, Any] config: Mapping[str, Any]
api_groups: Sequence[str] api_groups: Sequence[str]
api_log_path: str
__all__ = ["EngineContext", "create_app", "register_engine_api"] __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, tls_bundle_path=settings.tls_bundle_path,
config=settings.as_dict(), config=settings.as_dict(),
api_groups=settings.api_groups, 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) 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 from .services import API, WebSocket, WebUI # Local import to avoid circular deps during bootstrap
API.register_api(app, context) API.register_api(app, context)

View File

@@ -21,7 +21,7 @@ from Modules.enrollment.nonce_store import NonceCache
from Modules.tokens import routes as token_routes from Modules.tokens import routes as token_routes
from ...server import EngineContext from ...server import EngineContext
from .authentication import register_auth from .access_management.login import register_auth
DEFAULT_API_GROUPS: Sequence[str] = ("auth", "tokens", "enrollment") DEFAULT_API_GROUPS: Sequence[str] = ("auth", "tokens", "enrollment")