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),
"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)

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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")