202 lines
6.9 KiB
Python
202 lines
6.9 KiB
Python
# log_manager.py
|
|
import logging
|
|
import structlog
|
|
from pathlib import Path
|
|
import sys
|
|
import shutil
|
|
from datetime import datetime
|
|
import os
|
|
|
|
LOG_DIR = Path("logs")
|
|
LOG_DIR.mkdir(exist_ok=True)
|
|
|
|
# Путь к latest.log
|
|
LATEST_LOG = LOG_DIR / "latest.log"
|
|
|
|
def rotate_logs():
|
|
"""Rotate logs without compression (like Minecraft)"""
|
|
log_files = sorted(LOG_DIR.glob("*.log"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
|
|
for old_log in log_files[10:]:
|
|
if old_log.name != "latest.log":
|
|
try:
|
|
old_log.unlink()
|
|
except Exception:
|
|
pass
|
|
|
|
if LATEST_LOG.exists() and LATEST_LOG.stat().st_size > 0:
|
|
timestamp = datetime.fromtimestamp(LATEST_LOG.stat().st_mtime).strftime("%Y-%m-%d_%H-%M-%S")
|
|
backup_name = LOG_DIR / f"{timestamp}.log"
|
|
shutil.move(str(LATEST_LOG), str(backup_name))
|
|
|
|
def _add_location(logger, method_name, event_dict):
|
|
"""Add location to event dict"""
|
|
module = event_dict.pop("module", "unknown")
|
|
func_name = event_dict.pop("func_name", "")
|
|
|
|
# Store location
|
|
if func_name and func_name != "<module>":
|
|
event_dict["_location"] = f"{module}.{func_name}"
|
|
else:
|
|
event_dict["_location"] = module
|
|
|
|
return event_dict
|
|
|
|
class HumanConsoleRenderer:
|
|
"""Render logs in human-readable format with colors"""
|
|
|
|
def __init__(self):
|
|
self.colors = {
|
|
'debug': '\033[36m',
|
|
'info': '\033[32m',
|
|
'warning': '\033[33m',
|
|
'error': '\033[31m',
|
|
'critical': '\033[35m',
|
|
'reset': '\033[0m'
|
|
}
|
|
|
|
def __call__(self, logger, name, event_dict):
|
|
level = event_dict.get('level', 'info')
|
|
timestamp = event_dict.get('timestamp', datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3])
|
|
event = event_dict.get('event', '')
|
|
location = event_dict.get('_location', 'unknown')
|
|
|
|
# Format event with location
|
|
formatted_event = f"{event} [{location}]"
|
|
|
|
# Colorize level
|
|
color = self.colors.get(level, self.colors['reset'])
|
|
colored_level = f"{color}{level:<8}{self.colors['reset']}"
|
|
|
|
return f"{timestamp} [{colored_level}] {formatted_event}"
|
|
|
|
class HumanFileRenderer:
|
|
"""Render logs in human-readable format without colors for files"""
|
|
|
|
def __call__(self, logger, name, event_dict):
|
|
level = event_dict.get('level', 'info')
|
|
timestamp = event_dict.get('timestamp', datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3])
|
|
event = event_dict.get('event', '')
|
|
location = event_dict.get('_location', 'unknown')
|
|
|
|
# Format event with location
|
|
formatted_event = f"{event} [{location}]"
|
|
|
|
return f"{timestamp} [{level:<8}] {formatted_event}"
|
|
|
|
def setup_logging():
|
|
"""Setup human-readable logging"""
|
|
|
|
# Rotate logs on startup
|
|
rotate_logs()
|
|
|
|
# Configure structlog processors
|
|
structlog.configure(
|
|
processors=[
|
|
structlog.contextvars.merge_contextvars,
|
|
structlog.stdlib.add_logger_name,
|
|
structlog.processors.add_log_level,
|
|
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S.%f", utc=False),
|
|
structlog.processors.CallsiteParameterAdder(
|
|
{
|
|
structlog.processors.CallsiteParameter.MODULE,
|
|
structlog.processors.CallsiteParameter.FUNC_NAME,
|
|
}
|
|
),
|
|
_add_location,
|
|
],
|
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
wrapper_class=structlog.stdlib.BoundLogger,
|
|
cache_logger_on_first_use=True,
|
|
)
|
|
|
|
# Configure standard logging
|
|
root_logger = logging.getLogger()
|
|
root_logger.setLevel(logging.DEBUG)
|
|
root_logger.handlers.clear()
|
|
|
|
# Create formatters
|
|
class StructlogConsoleHandler(logging.StreamHandler):
|
|
def emit(self, record):
|
|
try:
|
|
# Convert record to event dict
|
|
event_dict = {
|
|
'level': record.levelname.lower(),
|
|
'timestamp': datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
|
|
'event': record.getMessage(),
|
|
'_location': getattr(record, 'module', 'unknown')
|
|
}
|
|
|
|
# Add function name if available
|
|
if hasattr(record, 'funcName') and record.funcName:
|
|
event_dict['_location'] = f"{record.module}.{record.funcName}"
|
|
|
|
# Render
|
|
renderer = HumanConsoleRenderer()
|
|
output = renderer(None, None, event_dict)
|
|
|
|
# Write to console
|
|
self.stream.write(output + '\n')
|
|
self.flush()
|
|
|
|
except Exception:
|
|
self.handleError(record)
|
|
|
|
class StructlogFileHandler(logging.FileHandler):
|
|
def emit(self, record):
|
|
try:
|
|
# Convert record to event dict
|
|
event_dict = {
|
|
'level': record.levelname.lower(),
|
|
'timestamp': datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
|
|
'event': record.getMessage(),
|
|
'_location': getattr(record, 'module', 'unknown')
|
|
}
|
|
|
|
# Add function name if available
|
|
if hasattr(record, 'funcName') and record.funcName:
|
|
event_dict['_location'] = f"{record.module}.{record.funcName}"
|
|
|
|
# Render
|
|
renderer = HumanFileRenderer()
|
|
output = renderer(None, None, event_dict)
|
|
|
|
# Write to file
|
|
self.stream.write(output + '\n')
|
|
self.flush()
|
|
|
|
except Exception:
|
|
self.handleError(record)
|
|
|
|
# Add handlers
|
|
console_handler = StructlogConsoleHandler(sys.stdout)
|
|
console_handler.setLevel(logging.DEBUG)
|
|
|
|
file_handler = StructlogFileHandler(LATEST_LOG, encoding='utf-8')
|
|
file_handler.setLevel(logging.DEBUG)
|
|
|
|
root_logger.addHandler(console_handler)
|
|
root_logger.addHandler(file_handler)
|
|
|
|
# Reduce noise
|
|
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
|
logging.getLogger("uvicorn.error").setLevel(logging.INFO)
|
|
logging.getLogger("fastapi").setLevel(logging.INFO)
|
|
|
|
return structlog.get_logger()
|
|
|
|
def get_logger(name):
|
|
"""Get a logger instance"""
|
|
return logging.getLogger(name)
|
|
|
|
# Global logger instance
|
|
_logger = None
|
|
|
|
def init_logging():
|
|
"""Initialize logging system"""
|
|
global _logger
|
|
if _logger is None:
|
|
_logger = setup_logging()
|
|
# Use standard logging for the initial message
|
|
logging.info(f"Logging initialized (log_file: {LATEST_LOG})")
|
|
return _logger |