Release 0.9.0
This commit is contained in:
311
bot/logger.py
Normal file
311
bot/logger.py
Normal file
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
Structured logging system for chrani-bot-tng
|
||||
|
||||
Provides context-aware logging with consistent formatting across the entire application.
|
||||
Replaces print() statements with structured, grep-able log output.
|
||||
|
||||
Usage:
|
||||
from bot.logger import get_logger
|
||||
|
||||
logger = get_logger("webserver")
|
||||
|
||||
# Error logging (always shown)
|
||||
logger.error("tile_fetch_failed", user="steamid123", z=4, x=-2, y=1, status=404)
|
||||
|
||||
# Warning logging (always shown)
|
||||
logger.warn("auth_missing_sid", user="steamid456", action="tile_request")
|
||||
|
||||
# Info logging (startup only, can be disabled)
|
||||
logger.info("module_loaded", module="webserver", version="1.0")
|
||||
|
||||
# Debug logging (opt-in via config)
|
||||
logger.debug("action_trace", action="select_dom_element", path="/map/owner/id")
|
||||
|
||||
Output format:
|
||||
[ERROR] [2025-01-19 12:34:56.123] tile_fetch_failed | user=steamid123 z=4 x=-2 y=1 status=404
|
||||
"""
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class Colors:
|
||||
"""ANSI color codes for terminal output"""
|
||||
# Foreground colors (bright versions for better visibility)
|
||||
RED = "\033[91m" # Errors
|
||||
YELLOW = "\033[93m" # Warnings
|
||||
GREEN = "\033[92m" # Info
|
||||
GRAY = "\033[90m" # Debug
|
||||
LIGHT_GRAY = "\033[37m" # Timestamps
|
||||
WHITE = "\033[97m" # Event/context text
|
||||
RESET = "\033[0m" # Reset to default
|
||||
|
||||
# Optional: Bold variants
|
||||
BOLD = "\033[1m"
|
||||
|
||||
|
||||
class LogLevel:
|
||||
"""Log level constants"""
|
||||
ERROR = "ERROR"
|
||||
WARN = "WARN"
|
||||
INFO = "INFO"
|
||||
DEBUG = "DEBUG"
|
||||
|
||||
|
||||
class LogConfig:
|
||||
"""Global logging configuration"""
|
||||
# Which levels to actually output
|
||||
enabled_levels = {
|
||||
LogLevel.ERROR, # Always enabled
|
||||
LogLevel.WARN, # Always enabled
|
||||
LogLevel.INFO, # Enabled (for now, can be disabled after testing)
|
||||
# LogLevel.DEBUG # Disabled by default, uncomment to enable
|
||||
}
|
||||
|
||||
# Format settings
|
||||
show_timestamps = True
|
||||
timestamp_format = "%Y-%m-%d %H:%M:%S.%f" # Includes microseconds
|
||||
use_colors = True # Enable colored output
|
||||
|
||||
@classmethod
|
||||
def enable_debug(cls):
|
||||
"""Enable debug logging (call this from config or command line)"""
|
||||
cls.enabled_levels.add(LogLevel.DEBUG)
|
||||
|
||||
@classmethod
|
||||
def disable_info(cls):
|
||||
"""Disable info logging (for production)"""
|
||||
cls.enabled_levels.discard(LogLevel.INFO)
|
||||
|
||||
@classmethod
|
||||
def disable_colors(cls):
|
||||
"""Disable colored output (for log files or incompatible terminals)"""
|
||||
cls.use_colors = False
|
||||
|
||||
|
||||
class ContextLogger:
|
||||
"""
|
||||
Structured logger with context support
|
||||
|
||||
Provides consistent, grep-able logging across the application.
|
||||
Each logger instance is bound to a specific module/component.
|
||||
"""
|
||||
|
||||
def __init__(self, module_name: str):
|
||||
"""
|
||||
Create a logger for a specific module
|
||||
|
||||
Args:
|
||||
module_name: Name of the module (e.g., "webserver", "dom_management")
|
||||
"""
|
||||
self.module_name = module_name
|
||||
|
||||
def _format_context(self, **context) -> str:
|
||||
"""
|
||||
Format context dict as key=value pairs
|
||||
|
||||
Args:
|
||||
**context: Arbitrary key-value pairs
|
||||
|
||||
Returns:
|
||||
Formatted string: "key1=value1 key2=value2"
|
||||
"""
|
||||
if not context:
|
||||
return ""
|
||||
|
||||
# Sort keys for consistent output
|
||||
pairs = []
|
||||
for key in sorted(context.keys()):
|
||||
value = context[key]
|
||||
|
||||
# Format value appropriately
|
||||
if value is None:
|
||||
formatted_value = "null"
|
||||
elif isinstance(value, str):
|
||||
# Escape spaces and special chars if needed
|
||||
if " " in value or "=" in value:
|
||||
formatted_value = f'"{value}"'
|
||||
else:
|
||||
formatted_value = value
|
||||
elif isinstance(value, bool):
|
||||
formatted_value = str(value).lower()
|
||||
elif isinstance(value, (list, dict)):
|
||||
# Complex types: just show type and length
|
||||
formatted_value = f"{type(value).__name__}[{len(value)}]"
|
||||
else:
|
||||
formatted_value = str(value)
|
||||
|
||||
pairs.append(f"{key}={formatted_value}")
|
||||
|
||||
return " ".join(pairs)
|
||||
|
||||
def _log(self, level: str, event: str, **context):
|
||||
"""
|
||||
Internal logging method
|
||||
|
||||
Args:
|
||||
level: Log level (ERROR, WARN, INFO, DEBUG)
|
||||
event: Event name (snake_case, descriptive)
|
||||
**context: Additional context as key=value pairs
|
||||
"""
|
||||
# Check if this level is enabled
|
||||
if level not in LogConfig.enabled_levels:
|
||||
return
|
||||
|
||||
# Always include module in context
|
||||
context["module"] = self.module_name
|
||||
|
||||
# Build timestamp
|
||||
timestamp = ""
|
||||
timestamp_color = ""
|
||||
if LogConfig.show_timestamps:
|
||||
now = datetime.now()
|
||||
# Truncate microseconds to milliseconds
|
||||
timestamp_str = now.strftime(LogConfig.timestamp_format)[:-3]
|
||||
if LogConfig.use_colors:
|
||||
timestamp_color = Colors.LIGHT_GRAY
|
||||
timestamp = f"{timestamp_color}[{timestamp_str}]{Colors.RESET} "
|
||||
else:
|
||||
timestamp = f"[{timestamp_str}] "
|
||||
|
||||
# Build context string
|
||||
context_str = self._format_context(**context)
|
||||
|
||||
# Get color for this level
|
||||
level_color = ""
|
||||
text_color = ""
|
||||
reset = ""
|
||||
if LogConfig.use_colors:
|
||||
if level == LogLevel.ERROR:
|
||||
level_color = Colors.RED
|
||||
elif level == LogLevel.WARN:
|
||||
level_color = Colors.YELLOW
|
||||
elif level == LogLevel.INFO:
|
||||
level_color = Colors.GREEN
|
||||
elif level == LogLevel.DEBUG:
|
||||
level_color = Colors.GRAY
|
||||
text_color = Colors.WHITE
|
||||
reset = Colors.RESET
|
||||
|
||||
# Format: [LEVEL] [TIMESTAMP] event | context
|
||||
# Only the [LEVEL] tag is colored, rest is white text with gray timestamp
|
||||
if context_str:
|
||||
message = f"{level_color}[{level:<5}]{reset} {timestamp}{text_color}{event} | {context_str}{reset}"
|
||||
else:
|
||||
message = f"{level_color}[{level:<5}]{reset} {timestamp}{text_color}{event}{reset}"
|
||||
|
||||
# Output to stderr (standard for logs, keeps stdout clean)
|
||||
print(message, file=sys.stderr, flush=True)
|
||||
|
||||
def error(self, event: str, **context):
|
||||
"""
|
||||
Log an error - always shown, indicates something failed
|
||||
|
||||
Use for:
|
||||
- Action failures
|
||||
- Network errors
|
||||
- Parse errors
|
||||
- Validation failures
|
||||
- Unexpected exceptions
|
||||
|
||||
Args:
|
||||
event: Error event name (e.g., "tile_fetch_failed")
|
||||
**context: Additional context (user, action, error message, etc.)
|
||||
"""
|
||||
self._log(LogLevel.ERROR, event, **context)
|
||||
|
||||
def warn(self, event: str, **context):
|
||||
"""
|
||||
Log a warning - always shown, indicates something unexpected but handled
|
||||
|
||||
Use for:
|
||||
- Fallback behavior activated
|
||||
- Deprecated feature used
|
||||
- Rate limiting triggered
|
||||
- Auth retry needed
|
||||
- Missing optional config
|
||||
|
||||
Args:
|
||||
event: Warning event name (e.g., "auth_missing_sid")
|
||||
**context: Additional context
|
||||
"""
|
||||
self._log(LogLevel.WARN, event, **context)
|
||||
|
||||
def info(self, event: str, **context):
|
||||
"""
|
||||
Log informational message - shown during startup, can be disabled
|
||||
|
||||
Use for:
|
||||
- Module loaded
|
||||
- Server started
|
||||
- Configuration summary
|
||||
- Connection established
|
||||
|
||||
Args:
|
||||
event: Info event name (e.g., "module_loaded")
|
||||
**context: Additional context
|
||||
"""
|
||||
self._log(LogLevel.INFO, event, **context)
|
||||
|
||||
def debug(self, event: str, **context):
|
||||
"""
|
||||
Log debug information - disabled by default, opt-in
|
||||
|
||||
Use for:
|
||||
- Action execution traces
|
||||
- Data transformations
|
||||
- Flow control decisions
|
||||
- Detailed state changes
|
||||
|
||||
Enable with: LogConfig.enable_debug()
|
||||
|
||||
Args:
|
||||
event: Debug event name (e.g., "action_trace")
|
||||
**context: Additional context
|
||||
"""
|
||||
self._log(LogLevel.DEBUG, event, **context)
|
||||
|
||||
|
||||
# Global logger registry
|
||||
_loggers: Dict[str, ContextLogger] = {}
|
||||
|
||||
|
||||
def get_logger(module_name: str) -> ContextLogger:
|
||||
"""
|
||||
Get or create a logger for a specific module
|
||||
|
||||
Args:
|
||||
module_name: Module identifier (e.g., "webserver", "dom_management")
|
||||
|
||||
Returns:
|
||||
ContextLogger instance for this module
|
||||
|
||||
Example:
|
||||
logger = get_logger("webserver")
|
||||
logger.error("connection_failed", host="localhost", port=8080)
|
||||
"""
|
||||
if module_name not in _loggers:
|
||||
_loggers[module_name] = ContextLogger(module_name)
|
||||
return _loggers[module_name]
|
||||
|
||||
|
||||
# Convenience function for quick usage
|
||||
def log_error(module: str, event: str, **context):
|
||||
"""Quick error logging without getting logger instance"""
|
||||
get_logger(module).error(event, **context)
|
||||
|
||||
|
||||
def log_warn(module: str, event: str, **context):
|
||||
"""Quick warning logging without getting logger instance"""
|
||||
get_logger(module).warn(event, **context)
|
||||
|
||||
|
||||
def log_info(module: str, event: str, **context):
|
||||
"""Quick info logging without getting logger instance"""
|
||||
get_logger(module).info(event, **context)
|
||||
|
||||
|
||||
def log_debug(module: str, event: str, **context):
|
||||
"""Quick debug logging without getting logger instance"""
|
||||
get_logger(module).debug(event, **context)
|
||||
Reference in New Issue
Block a user