SummarizerApp / app /core /logging.py
ming
Apply ruff formatting to Loguru migration files
75fe59b
Raw History Blame Contribute Delete
6.81 kB
"""
Logging configuration for the text summarizer backend using Loguru.
This module provides structured logging with automatic request ID tracking,
environment-aware formatting (JSON for production, colored text for development),
and backward-compatible API with the previous stdlib logging implementation.
"""
import os
import sys
from contextvars import ContextVar
from typing import Any
from loguru import logger
from app.core.config import settings
# Context variable for request ID (automatic propagation across async contexts)
request_id_var: ContextVar[str | None] = ContextVar("request_id", default=None)
def _serialize_record(record: dict) -> str:
"""
Custom serializer for JSON format that includes extra fields.
Args:
record: Loguru record dictionary
Returns:
JSON formatted log string
"""
import json
# Build structured log entry
log_entry = {
"timestamp": record["time"].isoformat(),
"level": record["level"].name,
"logger": record["name"],
"message": record["message"],
"function": record["function"],
"line": record["line"],
}
# Add request ID from context if available
request_id = request_id_var.get()
if request_id:
log_entry["request_id"] = request_id
# Add any extra fields bound to the logger
if record["extra"]:
log_entry.update(record["extra"])
# Add exception info if present
if record["exception"]:
log_entry["exception"] = {
"type": record["exception"].type.__name__
if record["exception"].type
else None,
"value": str(record["exception"].value),
}
return json.dumps(log_entry)
def _determine_log_format() -> str:
"""
Determine log format based on environment.
Returns:
"json" for production (HF Spaces), "text" for development
"""
# Check if LOG_FORMAT is explicitly set
log_format = getattr(settings, "log_format", "auto")
if log_format == "auto":
# Auto-detect: JSON if running on HuggingFace Spaces, text otherwise
is_hf_spaces = os.getenv("HF_SPACE_ROOT_PATH") is not None
return "json" if is_hf_spaces else "text"
return log_format
def setup_logging() -> None:
"""
Set up Loguru logging configuration.
Configures logging with environment-aware formatting:
- Production (HF Spaces): Structured JSON output for log aggregation
- Development: Colored, human-readable text output
The logger automatically includes request IDs from context variables.
"""
# Remove default handler
logger.remove()
# Determine format based on environment
log_format_type = _determine_log_format()
log_level = settings.log_level.upper()
if log_format_type == "json":
# Production: JSON structured logging
logger.add(
sys.stdout,
format=_serialize_record,
level=log_level,
serialize=False, # We handle serialization ourselves
backtrace=True,
diagnose=False, # Don't show local variables in production
)
else:
# Development: Colored text logging
logger.add(
sys.stdout,
format=(
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
"<level>{message}</level>"
),
level=log_level,
colorize=True,
backtrace=True,
diagnose=True, # Show local variables for debugging
)
# Log startup configuration
logger.info(f"Logging initialized with format={log_format_type}, level={log_level}")
def get_logger(name: str) -> Any:
"""
Get a logger instance (backward-compatible with stdlib logging).
Args:
name: Logger name (typically __name__ of the calling module)
Returns:
Loguru logger instance bound to the module name
"""
# Bind the logger to the module name for context
return logger.bind(module=name)
class RequestLogger:
"""
Logger for request/response logging with automatic request ID tracking.
This class provides a backward-compatible API with the previous
stdlib logging implementation, but uses Loguru with automatic
context variable propagation for request IDs.
"""
def __init__(self, base_logger: Any = None):
"""
Initialize request logger.
Args:
base_logger: Base logger (ignored, uses global Loguru logger)
"""
# Always use the global logger with automatic request ID binding
self.logger = logger
def log_request(
self, method: str, path: str, request_id: str, **kwargs: Any
) -> None:
"""
Log incoming request with structured fields.
Args:
method: HTTP method (GET, POST, etc.)
path: Request path
request_id: Unique request identifier
**kwargs: Additional fields to log
"""
# Get request ID from context var (fallback to parameter)
context_request_id = request_id_var.get() or request_id
self.logger.bind(
request_id=context_request_id, method=method, path=path, **kwargs
).info(f"Request {context_request_id}: {method} {path}")
def log_response(
self, request_id: str, status_code: int, duration_ms: float, **kwargs: Any
) -> None:
"""
Log response with structured fields.
Args:
request_id: Unique request identifier
status_code: HTTP status code
duration_ms: Request duration in milliseconds
**kwargs: Additional fields to log
"""
# Get request ID from context var (fallback to parameter)
context_request_id = request_id_var.get() or request_id
self.logger.bind(
request_id=context_request_id,
status_code=status_code,
duration_ms=duration_ms,
**kwargs,
).info(f"Response {context_request_id}: {status_code} ({duration_ms:.2f}ms)")
def log_error(self, request_id: str, error: str, **kwargs: Any) -> None:
"""
Log error with structured fields.
Args:
request_id: Unique request identifier
error: Error message
**kwargs: Additional fields to log
"""
# Get request ID from context var (fallback to parameter)
context_request_id = request_id_var.get() or request_id
self.logger.bind(request_id=context_request_id, error=error, **kwargs).error(
f"Error {context_request_id}: {error}"
)