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

@@ -13,6 +13,9 @@ from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
import random
from datetime import datetime, timedelta
from utils.logger import get_logger
logger = get_logger('server')
@dataclass
class ServerConfig:
@@ -58,7 +61,7 @@ class ServerConfig:
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
data['enabled_features'] = set(features_list)
except Exception as e:
print(f"⚠️ Failed to parse enabled_features string '{data['enabled_features']}': {e}")
logger.warning(f"Failed to parse enabled_features string '{data['enabled_features']}': {e}")
# Fallback to default features
data['enabled_features'] = {"autonomous", "bedtime", "monday_video"}
return cls(**data)
@@ -83,12 +86,12 @@ class ServerManager:
guild_id = int(guild_id_str)
self.servers[guild_id] = ServerConfig.from_dict(server_data)
self.server_memories[guild_id] = {}
print(f"📋 Loaded config for server: {server_data['guild_name']} (ID: {guild_id})")
logger.info(f"Loaded config for server: {server_data['guild_name']} (ID: {guild_id})")
# After loading, check if we need to repair the config
self.repair_config()
except Exception as e:
print(f"⚠️ Failed to load server config: {e}")
logger.error(f"Failed to load server config: {e}")
self._create_default_config()
else:
self._create_default_config()
@@ -101,21 +104,21 @@ class ServerManager:
# Check if enabled_features is a string (corrupted)
if isinstance(server.enabled_features, str):
needs_repair = True
print(f"🔧 Repairing corrupted enabled_features for server: {server.guild_name}")
logger.info(f"Repairing corrupted enabled_features for server: {server.guild_name}")
# Re-parse the features
try:
features_str = server.enabled_features.strip('{}')
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
server.enabled_features = set(features_list)
except Exception as e:
print(f"⚠️ Failed to repair enabled_features for {server.guild_name}: {e}")
logger.warning(f"Failed to repair enabled_features for {server.guild_name}: {e}")
server.enabled_features = {"autonomous", "bedtime", "monday_video"}
if needs_repair:
print("🔧 Saving repaired configuration...")
logger.info("Saving repaired configuration...")
self.save_config()
except Exception as e:
print(f"⚠️ Failed to repair config: {e}")
logger.error(f"Failed to repair config: {e}")
def _create_default_config(self):
"""Create default configuration for backward compatibility"""
@@ -132,7 +135,7 @@ class ServerManager:
self.servers[default_server.guild_id] = default_server
self.server_memories[default_server.guild_id] = {}
self.save_config()
print("📋 Created default server configuration")
logger.info("Created default server configuration")
def save_config(self):
"""Save server configurations to file"""
@@ -150,14 +153,14 @@ class ServerManager:
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(config_data, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save server config: {e}")
logger.error(f"Failed to save server config: {e}")
def add_server(self, guild_id: int, guild_name: str, autonomous_channel_id: int,
autonomous_channel_name: str, bedtime_channel_ids: List[int] = None,
enabled_features: Set[str] = None) -> bool:
"""Add a new server configuration"""
if guild_id in self.servers:
print(f"⚠️ Server {guild_id} already exists")
logger.info(f"Server {guild_id} already exists")
return False
if bedtime_channel_ids is None:
@@ -178,7 +181,7 @@ class ServerManager:
self.servers[guild_id] = server
self.server_memories[guild_id] = {}
self.save_config()
print(f"Added new server: {guild_name} (ID: {guild_id})")
logger.info(f"Added new server: {guild_name} (ID: {guild_id})")
return True
def remove_server(self, guild_id: int) -> bool:
@@ -199,7 +202,7 @@ class ServerManager:
del self.server_memories[guild_id]
self.save_config()
print(f"🗑️ Removed server: {server_name} (ID: {guild_id})")
logger.info(f"Removed server: {server_name} (ID: {guild_id})")
return True
def get_server_config(self, guild_id: int) -> Optional[ServerConfig]:
@@ -221,7 +224,7 @@ class ServerManager:
setattr(server, key, value)
self.save_config()
print(f"Updated config for server: {server.guild_name}")
logger.info(f"Updated config for server: {server.guild_name}")
return True
def get_server_memory(self, guild_id: int, key: str = None):
@@ -267,12 +270,12 @@ class ServerManager:
from utils.moods import load_mood_description
server.current_mood_description = load_mood_description(mood_name)
except Exception as e:
print(f"⚠️ Failed to load mood description for {mood_name}: {e}")
logger.error(f"Failed to load mood description for {mood_name}: {e}")
server.current_mood_description = f"I'm feeling {mood_name} today."
self.save_config()
print(f"😊 Server {server.guild_name} mood changed to: {mood_name}")
print(f"😊 Mood description: {server.current_mood_description[:100]}{'...' if len(server.current_mood_description) > 100 else ''}")
logger.info(f"Server {server.guild_name} mood changed to: {mood_name}")
logger.debug(f"Mood description: {server.current_mood_description[:100]}{'...' if len(server.current_mood_description) > 100 else ''}")
return True
def get_server_sleep_state(self, guild_id: int) -> bool:
@@ -323,7 +326,7 @@ class ServerManager:
def setup_server_scheduler(self, guild_id: int, client: discord.Client):
"""Setup independent scheduler for a specific server"""
if guild_id not in self.servers:
print(f"⚠️ Cannot setup scheduler for unknown server: {guild_id}")
logger.warning(f"Cannot setup scheduler for unknown server: {guild_id}")
return
server_config = self.servers[guild_id]
@@ -363,8 +366,8 @@ class ServerManager:
# Add bedtime reminder job
if "bedtime" in server_config.enabled_features:
print(f"Setting up bedtime scheduler for server {server_config.guild_name}")
print(f" Random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
logger.info(f"Setting up bedtime scheduler for server {server_config.guild_name}")
logger.debug(f" Random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
scheduler.add_job(
self._schedule_random_bedtime_for_server,
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
@@ -382,11 +385,11 @@ class ServerManager:
self.schedulers[guild_id] = scheduler
scheduler.start()
print(f"Started scheduler for server: {server_config.guild_name}")
logger.info(f"Started scheduler for server: {server_config.guild_name}")
def start_all_schedulers(self, client: discord.Client):
"""Start schedulers for all servers"""
print("🚀 Starting all server schedulers...")
logger.info("Starting all server schedulers...")
for guild_id in self.servers:
self.setup_server_scheduler(guild_id, client)
@@ -396,42 +399,42 @@ class ServerManager:
# Start Figurine DM scheduler
self.setup_figurine_updates_scheduler(client)
print(f"Started {len(self.servers)} server schedulers + DM mood scheduler")
logger.info(f"Started {len(self.servers)} server schedulers + DM mood scheduler")
def update_server_bedtime_job(self, guild_id: int, client: discord.Client):
"""Update just the bedtime job for a specific server without restarting all schedulers"""
server_config = self.servers.get(guild_id)
if not server_config:
print(f"⚠️ No server config found for guild {guild_id}")
logger.warning(f"No server config found for guild {guild_id}")
return False
scheduler = self.schedulers.get(guild_id)
if not scheduler:
print(f"⚠️ No scheduler found for guild {guild_id}")
logger.warning(f"No scheduler found for guild {guild_id}")
return False
# Remove existing bedtime job if it exists
bedtime_job_id = f"bedtime_schedule_{guild_id}"
try:
scheduler.remove_job(bedtime_job_id)
print(f"🗑️ Removed old bedtime job for server {guild_id}")
logger.info(f"Removed old bedtime job for server {guild_id}")
except Exception as e:
print(f" No existing bedtime job to remove for server {guild_id}: {e}")
logger.debug(f"No existing bedtime job to remove for server {guild_id}: {e}")
# Add new bedtime job with updated configuration
if "bedtime" in server_config.enabled_features:
print(f"Updating bedtime scheduler for server {server_config.guild_name}")
print(f" New random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
logger.info(f"Updating bedtime scheduler for server {server_config.guild_name}")
logger.debug(f" New random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
scheduler.add_job(
self._schedule_random_bedtime_for_server,
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
args=[guild_id, client],
id=bedtime_job_id
)
print(f"Updated bedtime job for server {server_config.guild_name}")
logger.info(f"Updated bedtime job for server {server_config.guild_name}")
return True
else:
print(f" Bedtime feature not enabled for server {guild_id}")
logger.info(f"Bedtime feature not enabled for server {guild_id}")
return True
def setup_dm_mood_scheduler(self, client: discord.Client):
@@ -449,10 +452,10 @@ class ServerManager:
scheduler.start()
self.schedulers["dm_mood"] = scheduler
print("🔄 DM mood rotation scheduler started (every 2 hours)")
logger.info("DM mood rotation scheduler started (every 2 hours)")
except Exception as e:
print(f"Failed to setup DM mood scheduler: {e}")
logger.error(f"Failed to setup DM mood scheduler: {e}")
def _enqueue_figurine_send(self, client: discord.Client):
"""Enqueue the figurine DM send task in the client's loop."""
@@ -460,11 +463,11 @@ class ServerManager:
from utils.figurine_notifier import send_figurine_dm_to_all_subscribers
if client.loop and client.loop.is_running():
client.loop.create_task(send_figurine_dm_to_all_subscribers(client))
print("Figurine DM send task queued")
logger.debug("Figurine DM send task queued")
else:
print("⚠️ Client loop not available for figurine DM send")
logger.warning("Client loop not available for figurine DM send")
except Exception as e:
print(f"⚠️ Error enqueuing figurine DM: {e}")
logger.error(f"Error enqueuing figurine DM: {e}")
def _schedule_one_figurine_send_today(self, scheduler: AsyncIOScheduler, client: discord.Client):
"""Schedule one figurine DM send at a random non-evening time today (or tomorrow if time passed)."""
@@ -475,7 +478,7 @@ class ServerManager:
target_time = now.replace(hour=random_hour, minute=random_minute, second=0, microsecond=0)
if target_time <= now:
target_time = target_time + timedelta(days=1)
print(f"🗓️ Scheduling figurine DM at {target_time.strftime('%Y-%m-%d %H:%M')} (random non-evening)")
logger.info(f"Scheduling figurine DM at {target_time.strftime('%Y-%m-%d %H:%M')} (random non-evening)")
scheduler.add_job(
self._enqueue_figurine_send,
DateTrigger(run_date=target_time),
@@ -499,22 +502,22 @@ class ServerManager:
self._schedule_one_figurine_send_today(scheduler, client)
scheduler.start()
self.schedulers["figurine_dm"] = scheduler
print("🗓️ Figurine updates scheduler started")
logger.info("Figurine updates scheduler started")
except Exception as e:
print(f"Failed to setup figurine updates scheduler: {e}")
logger.error(f"Failed to setup figurine updates scheduler: {e}")
def stop_all_schedulers(self):
"""Stop all schedulers"""
print("🛑 Stopping all schedulers...")
logger.info("Stopping all schedulers...")
for scheduler in self.schedulers.values():
try:
scheduler.shutdown()
except Exception as e:
print(f"⚠️ Error stopping scheduler: {e}")
logger.warning(f"Error stopping scheduler: {e}")
self.schedulers.clear()
print("All schedulers stopped")
logger.info("All schedulers stopped")
# Implementation of autonomous functions - these integrate with the autonomous system
def _run_autonomous_for_server(self, guild_id: int, client: discord.Client):
@@ -525,11 +528,11 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(autonomous_tick(guild_id))
print(f"[V2] Autonomous tick queued for server {guild_id}")
logger.debug(f"[V2] Autonomous tick queued for server {guild_id}")
else:
print(f"⚠️ Client loop not available for autonomous tick in server {guild_id}")
logger.warning(f"Client loop not available for autonomous tick in server {guild_id}")
except Exception as e:
print(f"⚠️ Error in autonomous tick for server {guild_id}: {e}")
logger.error(f"Error in autonomous tick for server {guild_id}: {e}")
def _run_autonomous_reaction_for_server(self, guild_id: int, client: discord.Client):
"""Run autonomous reaction for a specific server - called by APScheduler"""
@@ -539,11 +542,11 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(autonomous_reaction_tick(guild_id))
print(f"[V2] Autonomous reaction queued for server {guild_id}")
logger.debug(f"[V2] Autonomous reaction queued for server {guild_id}")
else:
print(f"⚠️ Client loop not available for autonomous reaction in server {guild_id}")
logger.warning(f"Client loop not available for autonomous reaction in server {guild_id}")
except Exception as e:
print(f"⚠️ Error in autonomous reaction for server {guild_id}: {e}")
logger.error(f"Error in autonomous reaction for server {guild_id}: {e}")
def _run_conversation_detection_for_server(self, guild_id: int, client: discord.Client):
"""Run conversation detection for a specific server - called by APScheduler"""
@@ -552,11 +555,11 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(miku_detect_and_join_conversation_for_server(guild_id))
print(f"Conversation detection queued for server {guild_id}")
logger.debug(f"Conversation detection queued for server {guild_id}")
else:
print(f"⚠️ Client loop not available for conversation detection in server {guild_id}")
logger.warning(f"Client loop not available for conversation detection in server {guild_id}")
except Exception as e:
print(f"⚠️ Error in conversation detection for server {guild_id}: {e}")
logger.error(f"Error in conversation detection for server {guild_id}: {e}")
def _send_monday_video_for_server(self, guild_id: int, client: discord.Client):
"""Send Monday video for a specific server - called by APScheduler"""
@@ -565,35 +568,35 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(send_monday_video_for_server(guild_id))
print(f"Monday video queued for server {guild_id}")
logger.debug(f"Monday video queued for server {guild_id}")
else:
print(f"⚠️ Client loop not available for Monday video in server {guild_id}")
logger.warning(f"Client loop not available for Monday video in server {guild_id}")
except Exception as e:
print(f"⚠️ Error in Monday video for server {guild_id}: {e}")
logger.error(f"Error in Monday video for server {guild_id}: {e}")
def _schedule_random_bedtime_for_server(self, guild_id: int, client: discord.Client):
"""Schedule bedtime reminder for a specific server at a random time within the configured range"""
print(f"Bedtime scheduler triggered for server {guild_id} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
logger.info(f"Bedtime scheduler triggered for server {guild_id} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# Get server config to determine the random time range
server_config = self.servers.get(guild_id)
if not server_config:
print(f"⚠️ No server config found for guild {guild_id}")
logger.warning(f"No server config found for guild {guild_id}")
return
# Calculate random time within the bedtime range
start_minutes = server_config.bedtime_hour * 60 + server_config.bedtime_minute
end_minutes = server_config.bedtime_hour_end * 60 + server_config.bedtime_minute_end
print(f"🕐 Bedtime range calculation: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} ({start_minutes} min) to {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d} ({end_minutes} min)")
logger.debug(f"Bedtime range calculation: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} ({start_minutes} min) to {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d} ({end_minutes} min)")
# Handle case where end time is next day (e.g., 23:30 to 00:30)
if end_minutes <= start_minutes:
end_minutes += 24 * 60 # Add 24 hours
print(f"🌙 Cross-midnight range detected, adjusted end to {end_minutes} minutes")
logger.debug(f"Cross-midnight range detected, adjusted end to {end_minutes} minutes")
random_minutes = random.randint(start_minutes, end_minutes)
print(f"🎲 Random time selected: {random_minutes} minutes from midnight")
logger.debug(f"Random time selected: {random_minutes} minutes from midnight")
# Convert back to hours and minutes
random_hour = (random_minutes // 60) % 24
@@ -609,7 +612,7 @@ class ServerManager:
delay_seconds = (target_time - now).total_seconds()
print(f"🎲 Random bedtime for server {server_config.guild_name}: {random_hour:02d}:{random_minute:02d} (in {delay_seconds/60:.1f} minutes)")
logger.info(f"Random bedtime for server {server_config.guild_name}: {random_hour:02d}:{random_minute:02d} (in {delay_seconds/60:.1f} minutes)")
# Schedule the actual bedtime reminder
try:
@@ -618,9 +621,9 @@ class ServerManager:
def send_bedtime_delayed():
if client.loop and client.loop.is_running():
client.loop.create_task(send_bedtime_reminder_for_server(guild_id, client))
print(f"Random bedtime reminder sent for server {guild_id}")
logger.info(f"Random bedtime reminder sent for server {guild_id}")
else:
print(f"⚠️ Client loop not available for bedtime reminder in server {guild_id}")
logger.warning(f"Client loop not available for bedtime reminder in server {guild_id}")
# Use the scheduler to schedule the delayed bedtime reminder
scheduler = self.schedulers.get(guild_id)
@@ -630,12 +633,12 @@ class ServerManager:
DateTrigger(run_date=target_time),
id=f"bedtime_reminder_{guild_id}_{int(target_time.timestamp())}"
)
print(f"Bedtime reminder scheduled for server {guild_id} at {target_time.strftime('%Y-%m-%d %H:%M:%S')}")
logger.info(f"Bedtime reminder scheduled for server {guild_id} at {target_time.strftime('%Y-%m-%d %H:%M:%S')}")
else:
print(f"⚠️ No scheduler found for server {guild_id}")
logger.warning(f"No scheduler found for server {guild_id}")
except Exception as e:
print(f"⚠️ Error scheduling bedtime reminder for server {guild_id}: {e}")
logger.error(f"Error scheduling bedtime reminder for server {guild_id}: {e}")
def _rotate_server_mood(self, guild_id: int, client: discord.Client):
"""Rotate mood for a specific server - called by APScheduler"""
@@ -644,11 +647,11 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(rotate_server_mood(guild_id))
print(f"Mood rotation queued for server {guild_id}")
logger.debug(f"Mood rotation queued for server {guild_id}")
else:
print(f"⚠️ Client loop not available for mood rotation in server {guild_id}")
logger.warning(f"Client loop not available for mood rotation in server {guild_id}")
except Exception as e:
print(f"⚠️ Error in mood rotation for server {guild_id}: {e}")
logger.error(f"Error in mood rotation for server {guild_id}: {e}")
# Global instance
server_manager = ServerManager()