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

@@ -11,6 +11,9 @@ import discord
import globals
from utils.llm import query_llama
from utils.dm_logger import dm_logger
from utils.logger import get_logger
logger = get_logger('dm')
# Directories
REPORTS_DIR = "memory/dm_reports"
@@ -26,7 +29,7 @@ class DMInteractionAnalyzer:
"""
self.owner_user_id = owner_user_id
os.makedirs(REPORTS_DIR, exist_ok=True)
print(f"📊 DM Interaction Analyzer initialized for owner: {owner_user_id}")
logger.info(f"DM Interaction Analyzer initialized for owner: {owner_user_id}")
def _load_reported_today(self) -> Dict[str, str]:
"""Load the list of users reported today with their dates"""
@@ -35,7 +38,7 @@ class DMInteractionAnalyzer:
with open(REPORTED_TODAY_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Failed to load reported_today.json: {e}")
logger.error(f"Failed to load reported_today.json: {e}")
return {}
return {}
@@ -45,7 +48,7 @@ class DMInteractionAnalyzer:
with open(REPORTED_TODAY_FILE, 'w', encoding='utf-8') as f:
json.dump(reported, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save reported_today.json: {e}")
logger.error(f"Failed to save reported_today.json: {e}")
def _clean_old_reports(self, reported: Dict[str, str]) -> Dict[str, str]:
"""Remove entries from reported_today that are older than 24 hours"""
@@ -58,7 +61,7 @@ class DMInteractionAnalyzer:
if now - report_date < timedelta(hours=24):
cleaned[user_id] = date_str
except Exception as e:
print(f"⚠️ Failed to parse date for user {user_id}: {e}")
logger.error(f"Failed to parse date for user {user_id}: {e}")
return cleaned
@@ -91,7 +94,7 @@ class DMInteractionAnalyzer:
if msg_time >= cutoff_time:
recent_messages.append(msg)
except Exception as e:
print(f"⚠️ Failed to parse message timestamp: {e}")
logger.error(f"Failed to parse message timestamp: {e}")
return recent_messages
@@ -126,14 +129,14 @@ class DMInteractionAnalyzer:
recent_messages = self._get_recent_messages(user_id, hours=24)
if not recent_messages:
print(f"📊 No recent messages from user {username} ({user_id})")
logger.debug(f"No recent messages from user {username} ({user_id})")
return None
# Count user messages only (not bot responses)
user_messages = [msg for msg in recent_messages if not msg.get("is_bot_message", False)]
if len(user_messages) < 3: # Minimum threshold for analysis
print(f"📊 Not enough messages from user {username} ({user_id}) for analysis")
logger.info(f"Not enough messages from user {username} ({user_id}) for analysis")
return None
# Format messages for analysis
@@ -174,7 +177,7 @@ Respond ONLY with the JSON object, no other text."""
response_type="dm_analysis"
)
print(f"📊 Raw LLM response for {username}:\n{response}\n")
logger.debug(f"Raw LLM response for {username}:\n{response}\n")
# Parse JSON response
# Remove markdown code blocks if present
@@ -192,7 +195,7 @@ Respond ONLY with the JSON object, no other text."""
if start_idx != -1 and end_idx != -1:
cleaned_response = cleaned_response[start_idx:end_idx+1]
print(f"📊 Cleaned JSON for {username}:\n{cleaned_response}\n")
logger.debug(f"Cleaned JSON for {username}:\n{cleaned_response}\n")
analysis = json.loads(cleaned_response)
@@ -205,11 +208,11 @@ Respond ONLY with the JSON object, no other text."""
return analysis
except json.JSONDecodeError as e:
print(f"⚠️ JSON parse error for user {username}: {e}")
print(f"⚠️ Failed response: {response}")
logger.error(f"JSON parse error for user {username}: {e}")
logger.error(f"Failed response: {response}")
return None
except Exception as e:
print(f"⚠️ Failed to analyze interaction for user {username}: {e}")
logger.error(f"Failed to analyze interaction for user {username}: {e}")
return None
def _save_report(self, user_id: int, analysis: Dict) -> str:
@@ -221,10 +224,10 @@ Respond ONLY with the JSON object, no other text."""
try:
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(analysis, f, indent=2, ensure_ascii=False)
print(f"💾 Saved report: {filepath}")
logger.info(f"Saved report: {filepath}")
return filepath
except Exception as e:
print(f"⚠️ Failed to save report: {e}")
logger.error(f"Failed to save report: {e}")
return ""
async def _send_report_to_owner(self, analysis: Dict):
@@ -232,7 +235,7 @@ Respond ONLY with the JSON object, no other text."""
try:
# Ensure we're using the Discord client's event loop
if not globals.client or not globals.client.is_ready():
print(f"⚠️ Discord client not ready, cannot send report")
logger.warning(f"Discord client not ready, cannot send report")
return
owner = await globals.client.fetch_user(self.owner_user_id)
@@ -294,10 +297,10 @@ Respond ONLY with the JSON object, no other text."""
)
await owner.send(embed=embed)
print(f"📤 Report sent to owner for user {username}")
logger.info(f"Report sent to owner for user {username}")
except Exception as e:
print(f"⚠️ Failed to send report to owner: {e}")
logger.error(f"Failed to send report to owner: {e}")
async def analyze_and_report(self, user_id: int) -> bool:
"""
@@ -306,12 +309,11 @@ Respond ONLY with the JSON object, no other text."""
Returns:
True if analysis was performed and reported, False otherwise
"""
# Check if already reported today
if self.has_been_reported_today(user_id):
print(f"📊 User {user_id} already reported today, skipping")
return False
# Analyze interaction
logger.debug(f"User {user_id} already reported today, skipping")
return False # Analyze interaction
analysis = await self.analyze_user_interaction(user_id)
if not analysis:
@@ -331,13 +333,13 @@ Respond ONLY with the JSON object, no other text."""
async def run_daily_analysis(self):
"""Run analysis on all DM users and report significant interactions"""
print("📊 Starting daily DM interaction analysis...")
logger.info("Starting daily DM interaction analysis...")
# Get all DM users
all_users = dm_logger.get_all_dm_users()
if not all_users:
print("📊 No DM users to analyze")
logger.info("No DM users to analyze")
return
reported_count = 0
@@ -363,9 +365,9 @@ Respond ONLY with the JSON object, no other text."""
analyzed_count += 1
except Exception as e:
print(f"⚠️ Failed to process user {user_summary.get('username', 'Unknown')}: {e}")
logger.error(f"Failed to process user {user_summary.get('username', 'Unknown')}: {e}")
print(f"📊 Daily analysis complete: Analyzed {analyzed_count} users, reported {reported_count}")
logger.info(f"Daily analysis complete: Analyzed {analyzed_count} users, reported {reported_count}")
# Global instance (will be initialized with owner ID)