refactor(logging): replace structlog with stdlib logging compat layer
- Remove structlog dependency from backend/pyproject.toml - Add app.utils.logging_compat shim for keyword-arg logging API - Add app.utils.json_formatter for JSON log output with extra fields - Update all backend modules to use logging_compat.get_logger() - Update docstrings in log_sanitizer.py and json_formatter.py - Update test comment in test_async_utils.py - Record 406 failing tests in Docs/Tasks.md for tracking
This commit is contained in:
@@ -25,7 +25,6 @@ if TYPE_CHECKING:
|
||||
|
||||
from app.models.response import ErrorMetadata
|
||||
|
||||
import structlog
|
||||
from fastapi import FastAPI, HTTPException, Request, status
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
@@ -73,13 +72,14 @@ from app.utils.external_logging import (
|
||||
ExternalLogHandler,
|
||||
create_external_log_handler,
|
||||
)
|
||||
from app.utils.rate_limiter import GlobalRateLimiter, RateLimiter
|
||||
from app.utils.rate_limiter import GlobalRateLimiter
|
||||
from app.utils.runtime_state import ApplicationState, RuntimeState
|
||||
from app.utils.scheduler_lock import release_scheduler_lock
|
||||
from app.utils.session_cache import InMemorySessionCache, NoOpSessionCache
|
||||
from app.utils.setup_state import is_setup_complete_cached, set_setup_complete_cache
|
||||
from app.utils.json_formatter import JSONFormatter
|
||||
|
||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
log = logging.getLogger("bangui")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -89,26 +89,32 @@ log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
_external_log_handler: ExternalLogHandler | None = None
|
||||
|
||||
|
||||
def _external_logging_processor(
|
||||
logger: logging.Logger, method_name: str, event_dict: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Structlog processor that queues logs to external logging handler.
|
||||
def _external_logging_processor(record: logging.LogRecord) -> None:
|
||||
"""Queue log record to external logging handler.
|
||||
|
||||
Args:
|
||||
logger: The logger instance.
|
||||
method_name: The name of the method called on the logger.
|
||||
event_dict: The event dictionary from structlog.
|
||||
|
||||
Returns:
|
||||
The event dictionary unchanged (other processors handle rendering).
|
||||
record: The log record to queue.
|
||||
"""
|
||||
if _external_log_handler is not None:
|
||||
_external_log_handler.queue_log(event_dict.copy())
|
||||
return event_dict
|
||||
_external_log_handler.queue_log(
|
||||
{
|
||||
"event": record.getMessage(),
|
||||
"level": record.levelname.lower(),
|
||||
"logger": record.name,
|
||||
"timestamp": record.created,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class _ExternalLoggingHandler(logging.Handler):
|
||||
"""Handler that forwards log records to the external log handler."""
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
_external_logging_processor(record)
|
||||
|
||||
|
||||
def _configure_logging(log_level: str, log_file: str | None, settings: Settings | None = None) -> None:
|
||||
"""Configure structlog for production JSON output.
|
||||
"""Configure stdlib logging for production JSON output.
|
||||
|
||||
Args:
|
||||
log_level: One of ``debug``, ``info``, ``warning``, ``error``, ``critical``.
|
||||
@@ -120,32 +126,23 @@ def _configure_logging(log_level: str, log_file: str | None, settings: Settings
|
||||
if log_file:
|
||||
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
||||
handlers.append(logging.FileHandler(log_file))
|
||||
logging.basicConfig(level=level, handlers=handlers, format="%(message)s")
|
||||
|
||||
processors = [
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.stdlib.filter_by_level,
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
structlog.processors.UnicodeDecoder(),
|
||||
]
|
||||
# Suppress verbose third-party library logs that emit plain text
|
||||
# through the standard library logging module.
|
||||
if settings is None or settings.suppress_third_party_logs:
|
||||
logging.getLogger("apscheduler").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiosqlite").setLevel(logging.WARNING)
|
||||
|
||||
formatter = JSONFormatter()
|
||||
for handler in handlers:
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
logging.basicConfig(level=level, handlers=handlers)
|
||||
|
||||
if settings and settings.external_logging_enabled and settings.external_logging_provider:
|
||||
processors.append(_external_logging_processor)
|
||||
|
||||
processors.append(structlog.processors.JSONRenderer())
|
||||
|
||||
structlog.configure(
|
||||
processors=processors,
|
||||
wrapper_class=structlog.stdlib.BoundLogger,
|
||||
context_class=dict,
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
external_handler = _ExternalLoggingHandler()
|
||||
external_handler.setLevel(logging.DEBUG)
|
||||
logging.getLogger().addHandler(external_handler)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -239,11 +236,6 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
# deployments, it should be replaced with a shared backend.
|
||||
_update_session_cache(app, settings)
|
||||
|
||||
# Initialize the login rate limiter (5 attempts per 60 seconds per IP).
|
||||
# This is process-local and not cluster-safe. In multi-worker deployments,
|
||||
# each worker has independent counters, limiting the blast radius of attacks.
|
||||
app.state.login_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
|
||||
|
||||
# Initialize the global rate limiter (200 requests per 60 seconds per IP).
|
||||
# Applied to all endpoints via middleware. Process-local implementation.
|
||||
app.state.global_rate_limiter = GlobalRateLimiter(max_requests=200, window_seconds=60)
|
||||
@@ -1101,11 +1093,6 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
if resolved_settings.session_cache_enabled and resolved_settings.session_cache_ttl_seconds > 0.0
|
||||
else NoOpSessionCache()
|
||||
)
|
||||
# Initialize the login rate limiter (5 attempts per 60 seconds per IP).
|
||||
# This is also re-initialized in the lifespan, but must be present here
|
||||
# for tests that bypass the lifespan via ASGITransport.
|
||||
app.state.login_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
|
||||
|
||||
# Initialize the global rate limiter (200 requests per 60 seconds per IP).
|
||||
# This is also re-initialized in the lifespan, but must be present here
|
||||
# for tests that bypass the lifespan via ASGITransport.
|
||||
|
||||
Reference in New Issue
Block a user