feat: Implement comprehensive non-hierarchical logging system

- Created new logging infrastructure with per-component filtering
- Added 6 log levels: DEBUG, INFO, API, WARNING, ERROR, CRITICAL
- Implemented non-hierarchical level control (any combination can be enabled)
- Migrated 917 print() statements across 31 files to structured logging
- Created web UI (system.html) for runtime configuration with dark theme
- Added global level controls to enable/disable levels across all components
- Added timestamp format control (off/time/date/datetime options)
- Implemented log rotation (10MB per file, 5 backups)
- Added API endpoints for dynamic log configuration
- Configured HTTP request logging with filtering via api.requests component
- Intercepted APScheduler logs with proper formatting
- Fixed persistence paths to use /app/memory for Docker volume compatibility
- Fixed checkbox display bug in web UI (enabled_levels now properly shown)
- Changed System Settings button to open in same tab instead of new window

Components: bot, api, api.requests, autonomous, persona, vision, llm,
conversation, mood, dm, scheduled, gpu, media, server, commands,
sentiment, core, apscheduler

All settings persist across container restarts via JSON config.
This commit is contained in:
2026-01-10 20:46:19 +02:00
parent ce00f9bd95
commit 32c2a7b930
34 changed files with 2766 additions and 936 deletions

View File

@@ -9,6 +9,9 @@ import discord
from datetime import datetime
from typing import List, Optional
import globals
from utils.logger import get_logger
logger = get_logger('dm')
# Directory for storing DM logs
DM_LOG_DIR = "memory/dms"
@@ -19,7 +22,7 @@ class DMLogger:
"""Initialize the DM logger and ensure directory exists"""
os.makedirs(DM_LOG_DIR, exist_ok=True)
os.makedirs("memory", exist_ok=True)
print(f"📁 DM Logger initialized: {DM_LOG_DIR}")
logger.info(f"DM Logger initialized: {DM_LOG_DIR}")
def _get_user_log_file(self, user_id: int) -> str:
"""Get the log file path for a specific user"""
@@ -28,19 +31,19 @@ class DMLogger:
def _load_user_logs(self, user_id: int) -> dict:
"""Load existing logs for a user, create new if doesn't exist"""
log_file = self._get_user_log_file(user_id)
print(f"📁 DM Logger: Loading logs from {log_file}")
logger.debug(f"DM Logger: Loading logs from {log_file}")
if os.path.exists(log_file):
try:
with open(log_file, 'r', encoding='utf-8') as f:
logs = json.load(f)
print(f"📁 DM Logger: Successfully loaded logs for user {user_id}: {len(logs.get('conversations', []))} conversations")
logger.debug(f"DM Logger: Successfully loaded logs for user {user_id}: {len(logs.get('conversations', []))} conversations")
return logs
except Exception as e:
print(f"⚠️ DM Logger: Failed to load DM logs for user {user_id}: {e}")
logger.error(f"DM Logger: Failed to load DM logs for user {user_id}: {e}")
return {"user_id": user_id, "username": "Unknown", "conversations": []}
else:
print(f"📁 DM Logger: No log file found for user {user_id}, creating new")
logger.debug(f"DM Logger: No log file found for user {user_id}, creating new")
return {"user_id": user_id, "username": "Unknown", "conversations": []}
def _save_user_logs(self, user_id: int, logs: dict):
@@ -50,7 +53,7 @@ class DMLogger:
with open(log_file, 'w', encoding='utf-8') as f:
json.dump(logs, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"⚠️ Failed to save DM logs for user {user_id}: {e}")
logger.error(f"Failed to save DM logs for user {user_id}: {e}")
def log_user_message(self, user: discord.User, message: discord.Message, is_bot_message: bool = False):
"""Log a user message in DMs"""
@@ -92,15 +95,15 @@ class DMLogger:
# Keep only last 1000 messages to prevent files from getting too large
if len(logs["conversations"]) > 1000:
logs["conversations"] = logs["conversations"][-1000:]
print(f"📝 DM logs for user {username} trimmed to last 1000 messages")
logger.info(f"DM logs for user {username} trimmed to last 1000 messages")
# Save logs
self._save_user_logs(user_id, logs)
if is_bot_message:
print(f"🤖 DM logged: Bot -> {username} ({len(message_entry['attachments'])} attachments)")
logger.debug(f"DM logged: Bot -> {username} ({len(message_entry['attachments'])} attachments)")
else:
print(f"💬 DM logged: {username} -> Bot ({len(message_entry['attachments'])} attachments)")
logger.debug(f"DM logged: {username} -> Bot ({len(message_entry['attachments'])} attachments)")
def get_user_conversation_summary(self, user_id: int) -> dict:
"""Get a summary of conversations with a user"""
@@ -211,10 +214,10 @@ class DMLogger:
bot_msg = MockMessage(bot_response, attachments=bot_attachments)
self.log_user_message(user, bot_msg, is_bot_message=True)
print(f"📝 Conversation logged for user {user_id}: user='{user_message[:50]}...', bot='{bot_response[:50]}...'")
logger.debug(f"Conversation logged for user {user_id}: user='{user_message[:50]}...', bot='{bot_response[:50]}...'")
except Exception as e:
print(f"⚠️ Failed to log conversation for user {user_id}: {e}")
logger.error(f"Failed to log conversation for user {user_id}: {e}")
def export_user_conversation(self, user_id: int, format: str = "json") -> str:
"""Export all conversations with a user in specified format"""
@@ -254,7 +257,7 @@ class DMLogger:
with open(BLOCKED_USERS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Failed to load blocked users: {e}")
logger.error(f"Failed to load blocked users: {e}")
return {"blocked_users": []}
return {"blocked_users": []}
@@ -262,9 +265,9 @@ class DMLogger:
"""Save the blocked users list"""
try:
with open(BLOCKED_USERS_FILE, 'w', encoding='utf-8') as f:
json.dump(blocked_data, f, indent=2, ensure_ascii=False)
json.dump(blocked_data, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save blocked users: {e}")
logger.error(f"Failed to save blocked users: {e}")
def is_user_blocked(self, user_id: int) -> bool:
"""Check if a user is blocked"""
@@ -289,13 +292,13 @@ class DMLogger:
}
self._save_blocked_users(blocked_data)
print(f"🚫 User {user_id} ({username}) has been blocked")
logger.info(f"User {user_id} ({username}) has been blocked")
return True
else:
print(f"⚠️ User {user_id} is already blocked")
logger.warning(f"User {user_id} is already blocked")
return False
except Exception as e:
print(f"Failed to block user {user_id}: {e}")
logger.error(f"Failed to block user {user_id}: {e}")
return False
def unblock_user(self, user_id: int) -> bool:
@@ -313,13 +316,13 @@ class DMLogger:
username = "Unknown"
self._save_blocked_users(blocked_data)
print(f"User {user_id} ({username}) has been unblocked")
logger.info(f"User {user_id} ({username}) has been unblocked")
return True
else:
print(f"⚠️ User {user_id} is not blocked")
logger.warning(f"User {user_id} is not blocked")
return False
except Exception as e:
print(f"Failed to unblock user {user_id}: {e}")
logger.error(f"Failed to unblock user {user_id}: {e}")
return False
def get_blocked_users(self) -> List[dict]:
@@ -368,17 +371,17 @@ class DMLogger:
self._save_user_logs(user_id, logs)
reactor_type = "🤖 Miku" if is_bot_reactor else f"👤 {reactor_name}"
print(f" Reaction logged: {emoji} by {reactor_type} on message {message_id}")
logger.debug(f"Reaction logged: {emoji} by {reactor_type} on message {message_id}")
return True
else:
print(f"⚠️ Reaction {emoji} by {reactor_name} already exists on message {message_id}")
logger.debug(f"Reaction {emoji} by {reactor_name} already exists on message {message_id}")
return False
print(f"⚠️ Message {message_id} not found in user {user_id}'s logs")
logger.warning(f"Message {message_id} not found in user {user_id}'s logs")
return False
except Exception as e:
print(f"Failed to log reaction add for user {user_id}, message {message_id}: {e}")
logger.error(f"Failed to log reaction add for user {user_id}, message {message_id}: {e}")
return False
async def log_reaction_remove(self, user_id: int, message_id: int, emoji: str, reactor_id: int):
@@ -399,20 +402,20 @@ class DMLogger:
if len(message["reactions"]) < original_count:
self._save_user_logs(user_id, logs)
print(f" Reaction removed: {emoji} by user/bot {reactor_id} from message {message_id}")
logger.debug(f"Reaction removed: {emoji} by user/bot {reactor_id} from message {message_id}")
return True
else:
print(f"⚠️ Reaction {emoji} by {reactor_id} not found on message {message_id}")
logger.debug(f"Reaction {emoji} by {reactor_id} not found on message {message_id}")
return False
else:
print(f"⚠️ No reactions on message {message_id}")
logger.debug(f"No reactions on message {message_id}")
return False
print(f"⚠️ Message {message_id} not found in user {user_id}'s logs")
logger.warning(f"Message {message_id} not found in user {user_id}'s logs")
return False
except Exception as e:
print(f"Failed to log reaction remove for user {user_id}, message {message_id}: {e}")
logger.error(f"Failed to log reaction remove for user {user_id}, message {message_id}: {e}")
return False
async def delete_conversation(self, user_id: int, conversation_id: str) -> bool:
@@ -420,8 +423,8 @@ class DMLogger:
try:
logs = self._load_user_logs(user_id)
print(f"🔍 DM Logger: Looking for bot message ID '{conversation_id}' for user {user_id}")
print(f"🔍 DM Logger: Searching through {len(logs['conversations'])} conversations")
logger.debug(f"DM Logger: Looking for bot message ID '{conversation_id}' for user {user_id}")
logger.debug(f"DM Logger: Searching through {len(logs['conversations'])} conversations")
# Convert conversation_id to int for comparison if it looks like a Discord message ID
conv_id_as_int = None
@@ -441,7 +444,7 @@ class DMLogger:
break
if not message_to_delete:
print(f"⚠️ No bot message found with ID {conversation_id} for user {user_id}")
logger.warning(f"No bot message found with ID {conversation_id} for user {user_id}")
return False
# Try to delete from Discord first
@@ -463,13 +466,13 @@ class DMLogger:
discord_message = await dm_channel.fetch_message(int(message_id))
await discord_message.delete()
discord_deleted = True
print(f"Deleted Discord message {message_id} from DM with user {user_id}")
logger.info(f"Deleted Discord message {message_id} from DM with user {user_id}")
except Exception as e:
print(f"⚠️ Could not delete Discord message {message_id}: {e}")
logger.warning(f"Could not delete Discord message {message_id}: {e}")
# Continue anyway to delete from logs
except Exception as e:
print(f"⚠️ Discord deletion failed: {e}")
logger.warning(f"Discord deletion failed: {e}")
# Continue anyway to delete from logs
# Remove from logs regardless of Discord deletion success
@@ -488,16 +491,16 @@ class DMLogger:
if deleted_count > 0:
self._save_user_logs(user_id, logs)
if discord_deleted:
print(f"🗑️ Deleted bot message from both Discord and logs for user {user_id}")
logger.info(f"Deleted bot message from both Discord and logs for user {user_id}")
else:
print(f"🗑️ Deleted bot message from logs only (Discord deletion failed) for user {user_id}")
logger.info(f"Deleted bot message from logs only (Discord deletion failed) for user {user_id}")
return True
else:
print(f"⚠️ No bot message found in logs with ID {conversation_id} for user {user_id}")
logger.warning(f"No bot message found in logs with ID {conversation_id} for user {user_id}")
return False
except Exception as e:
print(f"Failed to delete conversation {conversation_id} for user {user_id}: {e}")
logger.error(f"Failed to delete conversation {conversation_id} for user {user_id}: {e}")
return False
async def delete_all_conversations(self, user_id: int) -> bool:
@@ -507,12 +510,12 @@ class DMLogger:
conversation_count = len(logs["conversations"])
if conversation_count == 0:
print(f"⚠️ No conversations found for user {user_id}")
logger.warning(f"No conversations found for user {user_id}")
return False
# Find all bot messages to delete from Discord
bot_messages = [conv for conv in logs["conversations"] if conv.get("is_bot_message", False)]
print(f"🔍 Found {len(bot_messages)} bot messages to delete from Discord for user {user_id}")
logger.debug(f"Found {len(bot_messages)} bot messages to delete from Discord for user {user_id}")
# Try to delete all bot messages from Discord
discord_deleted_count = 0
@@ -534,13 +537,13 @@ class DMLogger:
discord_message = await dm_channel.fetch_message(int(message_id))
await discord_message.delete()
discord_deleted_count += 1
print(f"Deleted Discord message {message_id} from DM with user {user_id}")
logger.info(f"Deleted Discord message {message_id} from DM with user {user_id}")
except Exception as e:
print(f"⚠️ Could not delete Discord message {message_id}: {e}")
logger.error(f"Could not delete Discord message {message_id}: {e}")
# Continue with other messages
except Exception as e:
print(f"⚠️ Discord bulk deletion failed: {e}")
logger.warning(f"Discord bulk deletion failed: {e}")
# Continue anyway to delete from logs
# Delete all conversations from logs regardless of Discord deletion success
@@ -548,14 +551,14 @@ class DMLogger:
self._save_user_logs(user_id, logs)
if discord_deleted_count > 0:
print(f"🗑️ Deleted {discord_deleted_count} bot messages from Discord and all {conversation_count} conversations from logs for user {user_id}")
logger.info(f"Deleted {discord_deleted_count} bot messages from Discord and all {conversation_count} conversations from logs for user {user_id}")
else:
print(f"🗑️ Deleted all {conversation_count} conversations from logs only (Discord deletion failed) for user {user_id}")
logger.info(f"Deleted all {conversation_count} conversations from logs only (Discord deletion failed) for user {user_id}")
return True
except Exception as e:
print(f"Failed to delete all conversations for user {user_id}: {e}")
logger.error(f"Failed to delete all conversations for user {user_id}: {e}")
return False
def delete_user_completely(self, user_id: int) -> bool:
@@ -564,13 +567,13 @@ class DMLogger:
log_file = self._get_user_log_file(user_id)
if os.path.exists(log_file):
os.remove(log_file)
print(f"🗑️ Completely deleted log file for user {user_id}")
logger.info(f"Completely deleted log file for user {user_id}")
return True
else:
print(f"⚠️ No log file found for user {user_id}")
logger.warning(f"No log file found for user {user_id}")
return False
except Exception as e:
print(f"Failed to delete user log file {user_id}: {e}")
logger.error(f"Failed to delete user log file {user_id}: {e}")
return False
# Global instance