256 lines
6.5 KiB
Python
256 lines
6.5 KiB
Python
"""
|
|
Logging Configuration
|
|
|
|
Provides structured logging setup for the application.
|
|
Based on Python Quick Start Guide best practices.
|
|
"""
|
|
|
|
import logging
|
|
import sys
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
|
|
|
|
def setup_logging(
|
|
log_level: str = "INFO",
|
|
environment: str = "development"
|
|
) -> None:
|
|
"""
|
|
Configure application logging.
|
|
|
|
Args:
|
|
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
environment: Environment name (development, production)
|
|
|
|
Usage:
|
|
setup_logging(log_level="INFO", environment="production")
|
|
"""
|
|
level = getattr(logging, log_level.upper(), logging.INFO)
|
|
|
|
# Clear existing handlers
|
|
root_logger = logging.getLogger()
|
|
for handler in root_logger.handlers[:]:
|
|
root_logger.removeHandler(handler)
|
|
|
|
# Console handler
|
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
console_handler.setLevel(level)
|
|
|
|
# Format based on environment
|
|
if environment == "development":
|
|
# Human-readable format for development
|
|
formatter = logging.Formatter(
|
|
fmt='%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d | %(message)s',
|
|
datefmt='%H:%M:%S'
|
|
)
|
|
else:
|
|
# Structured format for production (easier to parse)
|
|
formatter = logging.Formatter(
|
|
fmt='%(asctime)s | %(levelname)s | %(name)s | %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
|
|
console_handler.setFormatter(formatter)
|
|
root_logger.addHandler(console_handler)
|
|
root_logger.setLevel(level)
|
|
|
|
# Suppress noisy loggers
|
|
logging.getLogger('werkzeug').setLevel(logging.WARNING)
|
|
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
|
|
|
|
|
def get_logger(name: str) -> logging.Logger:
|
|
"""
|
|
Get a logger instance.
|
|
|
|
Args:
|
|
name: Logger name (typically __name__)
|
|
|
|
Returns:
|
|
Logger instance
|
|
|
|
Example:
|
|
logger = get_logger(__name__)
|
|
logger.info("User logged in", extra={"user_id": 123})
|
|
"""
|
|
return logging.getLogger(name)
|
|
|
|
|
|
# ==========================================
|
|
# Structured Logging Helpers
|
|
# ==========================================
|
|
|
|
def log_audit_event(
|
|
logger: logging.Logger,
|
|
action: str,
|
|
user_id: Optional[int] = None,
|
|
ip_address: Optional[str] = None,
|
|
**kwargs
|
|
) -> None:
|
|
"""
|
|
Log an audit event with structured data.
|
|
|
|
Args:
|
|
logger: Logger instance
|
|
action: Action performed (e.g., "login_success", "user_created")
|
|
user_id: User ID performing action
|
|
ip_address: IP address of request
|
|
**kwargs: Additional context data
|
|
|
|
Example:
|
|
log_audit_event(
|
|
logger,
|
|
action="login_success",
|
|
user_id=123,
|
|
ip_address="192.168.1.1",
|
|
username="admin"
|
|
)
|
|
"""
|
|
extra_data = {
|
|
'action': action,
|
|
'user_id': user_id,
|
|
'ip_address': ip_address,
|
|
'timestamp': datetime.utcnow().isoformat(),
|
|
**kwargs
|
|
}
|
|
|
|
# Filter out None values
|
|
extra_data = {k: v for k, v in extra_data.items() if v is not None}
|
|
|
|
logger.info(f"AUDIT: {action}", extra=extra_data)
|
|
|
|
|
|
def log_security_event(
|
|
logger: logging.Logger,
|
|
event_type: str,
|
|
severity: str = "warning",
|
|
**kwargs
|
|
) -> None:
|
|
"""
|
|
Log a security-related event.
|
|
|
|
Args:
|
|
logger: Logger instance
|
|
event_type: Type of security event (e.g., "failed_login", "rate_limit_exceeded")
|
|
severity: Severity level (debug, info, warning, error, critical)
|
|
**kwargs: Additional context data
|
|
|
|
Example:
|
|
log_security_event(
|
|
logger,
|
|
event_type="failed_login",
|
|
severity="warning",
|
|
username="admin",
|
|
ip_address="192.168.1.1",
|
|
attempts=3
|
|
)
|
|
"""
|
|
extra_data = {
|
|
'event_type': event_type,
|
|
'timestamp': datetime.utcnow().isoformat(),
|
|
**kwargs
|
|
}
|
|
|
|
log_method = getattr(logger, severity.lower(), logger.warning)
|
|
log_method(f"SECURITY: {event_type}", extra=extra_data)
|
|
|
|
|
|
# ==========================================
|
|
# Request Logging Helpers
|
|
# ==========================================
|
|
|
|
def log_request(
|
|
logger: logging.Logger,
|
|
method: str,
|
|
path: str,
|
|
status_code: int,
|
|
duration_ms: float,
|
|
user_id: Optional[int] = None
|
|
) -> None:
|
|
"""
|
|
Log an HTTP request.
|
|
|
|
Args:
|
|
logger: Logger instance
|
|
method: HTTP method (GET, POST, etc.)
|
|
path: Request path
|
|
status_code: HTTP status code
|
|
duration_ms: Request duration in milliseconds
|
|
user_id: Authenticated user ID (if any)
|
|
|
|
Example:
|
|
log_request(
|
|
logger,
|
|
method="POST",
|
|
path="/api/users",
|
|
status_code=201,
|
|
duration_ms=45.2,
|
|
user_id=123
|
|
)
|
|
"""
|
|
extra_data = {
|
|
'method': method,
|
|
'path': path,
|
|
'status_code': status_code,
|
|
'duration_ms': round(duration_ms, 2),
|
|
'user_id': user_id
|
|
}
|
|
|
|
# Filter out None values
|
|
extra_data = {k: v for k, v in extra_data.items() if v is not None}
|
|
|
|
# Use different log levels based on status code
|
|
if status_code >= 500:
|
|
logger.error(f"{method} {path} {status_code}", extra=extra_data)
|
|
elif status_code >= 400:
|
|
logger.warning(f"{method} {path} {status_code}", extra=extra_data)
|
|
else:
|
|
logger.info(f"{method} {path} {status_code}", extra=extra_data)
|
|
|
|
|
|
# ==========================================
|
|
# Error Logging Helpers
|
|
# ==========================================
|
|
|
|
def log_exception(
|
|
logger: logging.Logger,
|
|
error: Exception,
|
|
context: Optional[str] = None,
|
|
**kwargs
|
|
) -> None:
|
|
"""
|
|
Log an exception with context.
|
|
|
|
Args:
|
|
logger: Logger instance
|
|
error: Exception instance
|
|
context: Additional context about where error occurred
|
|
**kwargs: Additional context data
|
|
|
|
Example:
|
|
try:
|
|
# Some operation
|
|
except Exception as e:
|
|
log_exception(
|
|
logger,
|
|
error=e,
|
|
context="Failed to create user",
|
|
user_id=123
|
|
)
|
|
"""
|
|
extra_data = {
|
|
'error_type': type(error).__name__,
|
|
'error_message': str(error),
|
|
'context': context,
|
|
**kwargs
|
|
}
|
|
|
|
# Filter out None values
|
|
extra_data = {k: v for k, v in extra_data.items() if v is not None}
|
|
|
|
logger.error(
|
|
f"Exception: {type(error).__name__}: {str(error)}",
|
|
extra=extra_data,
|
|
exc_info=True
|
|
)
|