mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-14 22:35:47 -07:00
Added API Access Logging
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
0
Data/Engine/services/API/assemblies/__init__.py
Normal file
0
Data/Engine/services/API/assemblies/__init__.py
Normal file
0
Data/Engine/services/API/devices/__init__.py
Normal file
0
Data/Engine/services/API/devices/__init__.py
Normal file
0
Data/Engine/services/API/filters/__init__.py
Normal file
0
Data/Engine/services/API/filters/__init__.py
Normal file
0
Data/Engine/services/API/scheduled_jobs/__init__.py
Normal file
0
Data/Engine/services/API/scheduled_jobs/__init__.py
Normal file
0
Data/Engine/services/API/server/__init__.py
Normal file
0
Data/Engine/services/API/server/__init__.py
Normal file
0
Data/Engine/services/API/sites/__init__.py
Normal file
0
Data/Engine/services/API/sites/__init__.py
Normal file
Reference in New Issue
Block a user