Major changes: - Organized Python v1 implementation into v1/ subdirectory - Created Rust workspace with 9 modular crates: - wifi-densepose-core: Core types, traits, errors - wifi-densepose-signal: CSI processing, phase sanitization, FFT - wifi-densepose-nn: Neural network inference (ONNX/Candle/tch) - wifi-densepose-api: Axum-based REST/WebSocket API - wifi-densepose-db: SQLx database layer - wifi-densepose-config: Configuration management - wifi-densepose-hardware: Hardware abstraction - wifi-densepose-wasm: WebAssembly bindings - wifi-densepose-cli: Command-line interface Documentation: - ADR-001: Workspace structure - ADR-002: Signal processing library selection - ADR-003: Neural network inference strategy - DDD domain model with bounded contexts Testing: - 69 tests passing across all crates - Signal processing: 45 tests - Neural networks: 21 tests - Core: 3 doc tests Performance targets: - 10x faster CSI processing (~0.5ms vs ~5ms) - 5x lower memory usage (~100MB vs ~500MB) - WASM support for browser deployment
330 lines
10 KiB
Python
330 lines
10 KiB
Python
"""
|
|
Logging configuration for WiFi-DensePose API
|
|
"""
|
|
|
|
import logging
|
|
import logging.config
|
|
import logging.handlers
|
|
import sys
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional
|
|
from datetime import datetime
|
|
|
|
from src.config.settings import Settings
|
|
|
|
|
|
class ColoredFormatter(logging.Formatter):
|
|
"""Colored log formatter for console output."""
|
|
|
|
# ANSI color codes
|
|
COLORS = {
|
|
'DEBUG': '\033[36m', # Cyan
|
|
'INFO': '\033[32m', # Green
|
|
'WARNING': '\033[33m', # Yellow
|
|
'ERROR': '\033[31m', # Red
|
|
'CRITICAL': '\033[35m', # Magenta
|
|
'RESET': '\033[0m' # Reset
|
|
}
|
|
|
|
def format(self, record):
|
|
"""Format log record with colors."""
|
|
if hasattr(record, 'levelname'):
|
|
color = self.COLORS.get(record.levelname, self.COLORS['RESET'])
|
|
record.levelname = f"{color}{record.levelname}{self.COLORS['RESET']}"
|
|
|
|
return super().format(record)
|
|
|
|
|
|
class StructuredFormatter(logging.Formatter):
|
|
"""Structured JSON formatter for log files."""
|
|
|
|
def format(self, record):
|
|
"""Format log record as structured JSON."""
|
|
import json
|
|
|
|
log_entry = {
|
|
'timestamp': datetime.utcnow().isoformat(),
|
|
'level': record.levelname,
|
|
'logger': record.name,
|
|
'message': record.getMessage(),
|
|
'module': record.module,
|
|
'function': record.funcName,
|
|
'line': record.lineno,
|
|
}
|
|
|
|
# Add exception info if present
|
|
if record.exc_info:
|
|
log_entry['exception'] = self.formatException(record.exc_info)
|
|
|
|
# Add extra fields
|
|
for key, value in record.__dict__.items():
|
|
if key not in ['name', 'msg', 'args', 'levelname', 'levelno', 'pathname',
|
|
'filename', 'module', 'lineno', 'funcName', 'created',
|
|
'msecs', 'relativeCreated', 'thread', 'threadName',
|
|
'processName', 'process', 'getMessage', 'exc_info',
|
|
'exc_text', 'stack_info']:
|
|
log_entry[key] = value
|
|
|
|
return json.dumps(log_entry)
|
|
|
|
|
|
class RequestContextFilter(logging.Filter):
|
|
"""Filter to add request context to log records."""
|
|
|
|
def filter(self, record):
|
|
"""Add request context to log record."""
|
|
# Try to get request context from contextvars or thread local
|
|
try:
|
|
import contextvars
|
|
request_id = contextvars.ContextVar('request_id', default=None).get()
|
|
user_id = contextvars.ContextVar('user_id', default=None).get()
|
|
|
|
if request_id:
|
|
record.request_id = request_id
|
|
if user_id:
|
|
record.user_id = user_id
|
|
|
|
except (ImportError, LookupError):
|
|
pass
|
|
|
|
return True
|
|
|
|
|
|
def setup_logging(settings: Settings) -> None:
|
|
"""Setup application logging configuration."""
|
|
|
|
# Create log directory if file logging is enabled
|
|
if settings.log_file:
|
|
log_path = Path(settings.log_file)
|
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Build logging configuration
|
|
config = build_logging_config(settings)
|
|
|
|
# Apply configuration
|
|
logging.config.dictConfig(config)
|
|
|
|
# Set up root logger
|
|
root_logger = logging.getLogger()
|
|
root_logger.setLevel(settings.log_level)
|
|
|
|
# Add request context filter to all handlers
|
|
request_filter = RequestContextFilter()
|
|
for handler in root_logger.handlers:
|
|
handler.addFilter(request_filter)
|
|
|
|
# Log startup message
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(f"Logging configured - Level: {settings.log_level}, File: {settings.log_file}")
|
|
|
|
|
|
def build_logging_config(settings: Settings) -> Dict[str, Any]:
|
|
"""Build logging configuration dictionary."""
|
|
|
|
config = {
|
|
'version': 1,
|
|
'disable_existing_loggers': False,
|
|
'formatters': {
|
|
'console': {
|
|
'()': ColoredFormatter,
|
|
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
'datefmt': '%Y-%m-%d %H:%M:%S'
|
|
},
|
|
'file': {
|
|
'format': '%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s',
|
|
'datefmt': '%Y-%m-%d %H:%M:%S'
|
|
},
|
|
'structured': {
|
|
'()': StructuredFormatter
|
|
}
|
|
},
|
|
'handlers': {
|
|
'console': {
|
|
'class': 'logging.StreamHandler',
|
|
'level': settings.log_level,
|
|
'formatter': 'console',
|
|
'stream': 'ext://sys.stdout'
|
|
}
|
|
},
|
|
'loggers': {
|
|
'': { # Root logger
|
|
'level': settings.log_level,
|
|
'handlers': ['console'],
|
|
'propagate': False
|
|
},
|
|
'src': { # Application logger
|
|
'level': settings.log_level,
|
|
'handlers': ['console'],
|
|
'propagate': False
|
|
},
|
|
'uvicorn': {
|
|
'level': 'INFO',
|
|
'handlers': ['console'],
|
|
'propagate': False
|
|
},
|
|
'uvicorn.access': {
|
|
'level': 'INFO',
|
|
'handlers': ['console'],
|
|
'propagate': False
|
|
},
|
|
'fastapi': {
|
|
'level': 'INFO',
|
|
'handlers': ['console'],
|
|
'propagate': False
|
|
},
|
|
'sqlalchemy': {
|
|
'level': 'WARNING',
|
|
'handlers': ['console'],
|
|
'propagate': False
|
|
},
|
|
'sqlalchemy.engine': {
|
|
'level': 'INFO' if settings.debug else 'WARNING',
|
|
'handlers': ['console'],
|
|
'propagate': False
|
|
}
|
|
}
|
|
}
|
|
|
|
# Add file handler if log file is specified
|
|
if settings.log_file:
|
|
config['handlers']['file'] = {
|
|
'class': 'logging.handlers.RotatingFileHandler',
|
|
'level': settings.log_level,
|
|
'formatter': 'file',
|
|
'filename': settings.log_file,
|
|
'maxBytes': settings.log_max_size,
|
|
'backupCount': settings.log_backup_count,
|
|
'encoding': 'utf-8'
|
|
}
|
|
|
|
# Add structured log handler for JSON logs
|
|
structured_log_file = str(Path(settings.log_file).with_suffix('.json'))
|
|
config['handlers']['structured'] = {
|
|
'class': 'logging.handlers.RotatingFileHandler',
|
|
'level': settings.log_level,
|
|
'formatter': 'structured',
|
|
'filename': structured_log_file,
|
|
'maxBytes': settings.log_max_size,
|
|
'backupCount': settings.log_backup_count,
|
|
'encoding': 'utf-8'
|
|
}
|
|
|
|
# Add file handlers to all loggers
|
|
for logger_config in config['loggers'].values():
|
|
logger_config['handlers'].extend(['file', 'structured'])
|
|
|
|
return config
|
|
|
|
|
|
def get_logger(name: str) -> logging.Logger:
|
|
"""Get a logger with the specified name."""
|
|
return logging.getLogger(name)
|
|
|
|
|
|
def configure_third_party_loggers(settings: Settings) -> None:
|
|
"""Configure third-party library loggers."""
|
|
|
|
# Suppress noisy loggers in production
|
|
if settings.is_production:
|
|
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
|
logging.getLogger('requests').setLevel(logging.WARNING)
|
|
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
|
logging.getLogger('multipart').setLevel(logging.WARNING)
|
|
|
|
# Configure SQLAlchemy logging
|
|
if settings.debug and settings.is_development:
|
|
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
|
|
logging.getLogger('sqlalchemy.pool').setLevel(logging.DEBUG)
|
|
else:
|
|
logging.getLogger('sqlalchemy').setLevel(logging.WARNING)
|
|
|
|
# Configure Redis logging
|
|
logging.getLogger('redis').setLevel(logging.WARNING)
|
|
|
|
# Configure WebSocket logging
|
|
logging.getLogger('websockets').setLevel(logging.INFO)
|
|
|
|
|
|
class LoggerMixin:
|
|
"""Mixin class to add logging capabilities to any class."""
|
|
|
|
@property
|
|
def logger(self) -> logging.Logger:
|
|
"""Get logger for this class."""
|
|
return logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}")
|
|
|
|
|
|
def log_function_call(func):
|
|
"""Decorator to log function calls."""
|
|
import functools
|
|
|
|
@functools.wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
logger = logging.getLogger(func.__module__)
|
|
logger.debug(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
|
|
|
|
try:
|
|
result = func(*args, **kwargs)
|
|
logger.debug(f"{func.__name__} completed successfully")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"{func.__name__} failed with error: {e}")
|
|
raise
|
|
|
|
return wrapper
|
|
|
|
|
|
def log_async_function_call(func):
|
|
"""Decorator to log async function calls."""
|
|
import functools
|
|
|
|
@functools.wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
logger = logging.getLogger(func.__module__)
|
|
logger.debug(f"Calling async {func.__name__} with args={args}, kwargs={kwargs}")
|
|
|
|
try:
|
|
result = await func(*args, **kwargs)
|
|
logger.debug(f"Async {func.__name__} completed successfully")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Async {func.__name__} failed with error: {e}")
|
|
raise
|
|
|
|
return wrapper
|
|
|
|
|
|
def setup_request_logging():
|
|
"""Setup request-specific logging context."""
|
|
import contextvars
|
|
import uuid
|
|
|
|
# Create context variables for request tracking
|
|
request_id_var = contextvars.ContextVar('request_id')
|
|
user_id_var = contextvars.ContextVar('user_id')
|
|
|
|
def set_request_context(request_id: Optional[str] = None, user_id: Optional[str] = None):
|
|
"""Set request context for logging."""
|
|
if request_id is None:
|
|
request_id = str(uuid.uuid4())
|
|
|
|
request_id_var.set(request_id)
|
|
if user_id:
|
|
user_id_var.set(user_id)
|
|
|
|
def get_request_context():
|
|
"""Get current request context."""
|
|
try:
|
|
return {
|
|
'request_id': request_id_var.get(),
|
|
'user_id': user_id_var.get(None)
|
|
}
|
|
except LookupError:
|
|
return {}
|
|
|
|
return set_request_context, get_request_context
|
|
|
|
|
|
# Initialize request logging context
|
|
set_request_context, get_request_context = setup_request_logging() |