mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 07:25:48 -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),
|
"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)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
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