Files
winauthmon-server/utils/db_logging.py
T
2025-05-25 20:26:18 +01:00

268 lines
9.7 KiB
Python

import logging
import traceback
import uuid
from datetime import datetime, timezone
from flask import request, g, has_request_context
from extensions import db
from api.models import ErrorLog
import threading
import pytz
from .toolbox import get_app_timezone, get_current_timestamp
class DatabaseLogHandler(logging.Handler):
"""
Custom logging handler that stores log records in the database.
Configurable logging level via database settings.
"""
def __init__(self):
super().__init__()
self.setLevel(logging.WARNING) # Default level, will be updated from config
self._app = None # Store app reference
self._processed_records = set() # Track processed records to avoid duplicates
self._max_cache_size = 1000 # Limit cache size to prevent memory issues
def emit(self, record):
"""
Emit a log record to the database.
This runs in a separate thread to avoid blocking the main application.
"""
# Import filter functions
from .toolbox import get_filtered_loggers, get_filtered_message_patterns
# Get configured filters
filtered_loggers = get_filtered_loggers()
filtered_patterns = get_filtered_message_patterns()
# Skip database logging for filtered loggers to prevent feedback loops
if record.name in filtered_loggers:
return
# Also filter out messages containing specific patterns
message = record.getMessage().lower()
for pattern in filtered_patterns:
if pattern.lower() in message:
return
# Create a unique identifier for this record to prevent duplicates
record_id = (
record.name,
record.levelname,
record.getMessage(),
record.created,
getattr(record, 'pathname', ''),
getattr(record, 'lineno', 0)
)
# Check if we've already processed this exact record
if record_id in self._processed_records:
return
# Add to processed records cache
self._processed_records.add(record_id)
# Clean cache if it gets too large
if len(self._processed_records) > self._max_cache_size:
# Remove oldest half of entries (simple cleanup)
self._processed_records = set(list(self._processed_records)[self._max_cache_size//2:])
# Store the app reference if we have an application context
if not self._app:
try:
from flask import current_app
self._app = current_app._get_current_object()
except RuntimeError:
# No application context available, try to import app
try:
from app import app
self._app = app
except ImportError:
pass
# Use a thread to avoid blocking the main application
threading.Thread(target=self._emit_to_db, args=(record,), daemon=True).start()
def _emit_to_db(self, record):
"""
Actually write the log record to the database.
This method runs in a separate thread.
"""
try:
# Use the stored app reference or try to get it
app = self._app
if not app:
try:
from flask import current_app
app = current_app._get_current_object()
except RuntimeError:
# No application context, try to import app
try:
from app import app
except ImportError:
print("Could not import app for database logging")
return
with app.app_context():
# Extract request information if available
request_id = None
user_id = None
remote_addr = None
if has_request_context():
try:
request_id = getattr(g, 'request_id', str(uuid.uuid4())[:8])
user_id = getattr(g, 'user_id', None)
remote_addr = request.remote_addr
except Exception:
# If we can't get request context, continue without it
pass
# Format exception info if present
exception_text = None
if record.exc_info:
exception_text = ''.join(traceback.format_exception(*record.exc_info))
# Create error log entry
error_log = ErrorLog(
level=record.levelname,
logger_name=record.name,
message=self.format(record),
timestamp=get_current_timestamp(),
pathname=record.pathname if hasattr(record, 'pathname') else None,
lineno=record.lineno if hasattr(record, 'lineno') else None,
request_id=request_id,
user_id=user_id,
remote_addr=remote_addr,
exception=exception_text
)
db.session.add(error_log)
db.session.commit()
except Exception as e:
# If database logging fails, fall back to console logging
# Don't raise the exception to avoid breaking the application
print(f"Failed to log to database: {e}")
def setup_database_logging(app):
"""
Set up database logging for the Flask application.
"""
# Create and configure the database handler
db_handler = DatabaseLogHandler()
db_handler._app = app # Store app reference
# Set initial logging level from settings (will be updated dynamically)
update_logging_level(app, db_handler)
# Set a formatter for the database logs
formatter = logging.Formatter(
'%(name)s - %(levelname)s - %(message)s'
)
db_handler.setFormatter(formatter)
# Store handler reference in app for dynamic updates
app.db_handler = db_handler
# Only add to root logger to avoid duplicate logging
# (app.logger propagates to root logger by default)
root_logger = logging.getLogger()
# Check if we already have this handler to avoid duplicates
handler_exists = False
for handler in root_logger.handlers:
if isinstance(handler, DatabaseLogHandler):
handler_exists = True
break
if not handler_exists:
root_logger.addHandler(db_handler)
# Add request ID to flask request context
@app.before_request
def add_request_id():
g.request_id = str(uuid.uuid4())[:8]
if hasattr(g, 'api_key') and g.api_key and g.api_key.user_id:
g.user_id = g.api_key.user_id
elif hasattr(g, 'current_user') and g.current_user and hasattr(g.current_user, 'id'):
g.user_id = g.current_user.id
app.logger.info("Database logging initialized")
def update_logging_level(app, db_handler=None):
"""
Update the database logging level from settings.
"""
try:
with app.app_context():
from auth.models import Settings
settings = Settings.query.first()
if settings and hasattr(settings, 'log_level'):
# Convert string to logging level
level_map = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARNING': logging.WARNING,
'ERROR': logging.ERROR,
'CRITICAL': logging.CRITICAL
}
new_level = level_map.get(settings.log_level, logging.WARNING)
# Update the handler if provided
if db_handler:
db_handler.setLevel(new_level)
# Or get it from app if stored
elif hasattr(app, 'db_handler'):
app.db_handler.setLevel(new_level)
app.logger.info(f"Database logging level updated to: {settings.log_level}")
else:
# Default to WARNING if no setting found
if db_handler:
db_handler.setLevel(logging.WARNING)
elif hasattr(app, 'db_handler'):
app.db_handler.setLevel(logging.WARNING)
except Exception as e:
print(f"Failed to update logging level: {e}")
def get_available_log_levels():
"""
Get list of available logging levels for admin configuration.
"""
return [
('DEBUG', 'Debug - All messages'),
('INFO', 'Info - General information and above'),
('WARNING', 'Warning - Warnings and above (default)'),
('ERROR', 'Error - Errors and above'),
('CRITICAL', 'Critical - Only critical errors')
]
def log_error(message, exception=None, level=logging.ERROR):
"""
Convenience function to log errors to the database.
Args:
message: Error message
exception: Exception object (optional)
level: Logging level (default: ERROR)
"""
logger = logging.getLogger(__name__)
if exception:
logger.log(level, message, exc_info=True)
else:
logger.log(level, message)
def log_warning(message):
"""Convenience function to log warnings."""
log_error(message, level=logging.WARNING)
def log_critical(message, exception=None):
"""Convenience function to log critical errors."""
log_error(message, exception, level=logging.CRITICAL)