feat: Enhance logging configuration with support for multiple log files and formats; improve CLI help output

This commit is contained in:
Илья Глазунов 2025-09-02 15:27:20 +03:00
parent 79c8f127ca
commit fb38853427
10 changed files with 584 additions and 72 deletions

View File

@ -7,11 +7,55 @@ PACKAGE_NAME = pyserve
GREEN = \033[0;32m GREEN = \033[0;32m
YELLOW = \033[1;33m YELLOW = \033[1;33m
RED = \033[0;31m RED = \033[0;31m
CYAN = \033[0;36m
NC = \033[0m NC = \033[0m
help: help:
@echo "$(GREEN)Commands:$(NC)" @echo "$(GREEN)╔══════════════════════════════════════════════════════════════════════════════╗$(NC)"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-20s$(NC) %s\n", $$1, $$2}' @echo "$(GREEN)$(NC) $(GREEN)Available Commands:$(NC) $(GREEN)$(NC)"
@echo "$(GREEN)╠══════════════════════════════════════════════════════════════════════════════╣$(NC)"
@echo "$(YELLOW)Installation:$(NC)"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "install" "Installing dependencies"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "dev-install" "Installing development dependencies"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "dev-deps" "Installing additional tools"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "install-package" "Installing package locally"
@echo ""
@echo "$(YELLOW)Building:$(NC)"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "build" "Building package"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "clean" "Cleaning temporary files"
@echo ""
@echo "$(YELLOW)Testing:$(NC)"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "test" "Running tests"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "test-cov" "Running tests with coverage"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "lint" "Checking code with linters"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "format" "Formatting code"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "check" "Lint and test"
@echo ""
@echo "$(YELLOW)Running:$(NC)"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "run" "Starting server in development mode"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "run-prod" "Starting server in production mode"
@echo ""
@echo "$(YELLOW)Publishing:$(NC)"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "publish-test" "Publishing to Test PyPI"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "publish" "Publishing to PyPI"
@echo ""
@echo "$(YELLOW)Versioning:$(NC)"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "version" "Show current version"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "version-patch" "Increase patch version"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "version-minor" "Increase minor version"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "version-major" "Increase major version"
@echo ""
@echo "$(YELLOW)Environment:$(NC)"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "shell" "Opening Poetry shell"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "env-info" "Environment information"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "deps-update" "Updating dependencies"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "deps-show" "Dependency tree"
@echo ""
@echo "$(YELLOW)Configuration:$(NC)"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "config-create" "Creating config.yaml"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "watch-logs" "Last server logs"
@printf " $(YELLOW)%-20s$(CYAN) %s$(NC)\n" "init" "Project initialized for development"
@echo "$(GREEN)╚══════════════════════════════════════════════════════════════════════════════╝$(NC)"
install: install:
@echo "$(GREEN)Installing dependencies...$(NC)" @echo "$(GREEN)Installing dependencies...$(NC)"
@ -118,7 +162,7 @@ config-create:
echo "$(YELLOW)config.yaml already exists$(NC)"; \ echo "$(YELLOW)config.yaml already exists$(NC)"; \
fi fi
logs: watch-logs:
@echo "$(GREEN)Last server logs:$(NC)" @echo "$(GREEN)Last server logs:$(NC)"
@if [ -f logs/pyserve.log ]; then tail -f logs/pyserve.log; else echo "$(RED)Log file not found$(NC)"; fi @if [ -f logs/pyserve.log ]; then tail -f logs/pyserve.log; else echo "$(RED)Log file not found$(NC)"; fi

View File

@ -18,29 +18,73 @@ ssl:
logging: logging:
level: DEBUG level: DEBUG
console_output: true console_output: true
log_file: ./logs/pyserve.log format:
type: standard
use_colors: true
show_module: true
timestamp_format: "%Y-%m-%d %H:%M:%S"
console:
format:
type: standard
use_colors: true
show_module: true
level: DEBUG
files:
- path: "./logs/pyserve.log"
level: DEBUG
loggers: []
format:
type: standard
use_colors: false
show_module: true
# НОВОЕ: Расширяемые модули - path: "./logs/pyserve.json"
level: INFO
loggers: []
format:
type: json
use_colors: false
show_module: true
- path: "./logs/server.log"
level: DEBUG
loggers: ["pyserve.server"]
format:
type: standard
use_colors: false
show_module: true
- path: "./logs/access.log"
level: INFO
loggers: ["pyserve.access"]
max_bytes: 5242880 # 5MB
backup_count: 10
format:
type: standard
use_colors: false
show_module: false
# NEW: Extendable modules
extensions: extensions:
# Встроенное расширение для продвинутой маршрутизации # Built-in extension for advanced routing
- type: routing - type: routing
config: config:
regex_locations: regex_locations:
# API маршруты с захватом версии # API routes with version capture
"~^/api/v(?P<version>\\d+)/": "~^/api/v(?P<version>\\d+)/":
proxy_pass: "http://localhost:9001" proxy_pass: "http://localhost:9001"
headers: headers:
- "API-Version: {version}" - "API-Version: {version}"
- "X-Forwarded-For: $remote_addr" - "X-Forwarded-For: $remote_addr"
# Статические файлы с долгим кэшем # Static files with long cache
"~*\\.(js|css|png|jpg|gif|ico|svg|woff2?)$": "~*\\.(js|css|png|jpg|gif|ico|svg|woff2?)$":
root: "./static" root: "./static"
cache_control: "public, max-age=31536000" cache_control: "public, max-age=31536000"
headers: headers:
- "Access-Control-Allow-Origin: *" - "Access-Control-Allow-Origin: *"
# Exact match для health check # Exact match for health check
"=/health": "=/health":
return: "200 OK" return: "200 OK"
content_type: "text/plain" content_type: "text/plain"
@ -49,12 +93,12 @@ extensions:
root: "./static" root: "./static"
index_file: "index.html" index_file: "index.html"
# SPA fallback для всех остальных маршрутов # SPA fallback for all other routes
"__default__": "__default__":
spa_fallback: true spa_fallback: true
root: "./static" root: "./static"
index_file: "docs.html" index_file: "docs.html"
# Исключения для SPA (не попадают в fallback) # Exceptions for SPA (do not hit fallback)
exclude_patterns: exclude_patterns:
- "/api/" - "/api/"
- "/admin/" - "/admin/"

View File

@ -0,0 +1,131 @@
# Example extended logging configuration for PyServe
# Support for multiple files and logger-based separation
logging:
# Main logging level (applies by default)
level: DEBUG # DEBUG, INFO, WARNING, ERROR, CRITICAL
# Enable/disable console output
console_output: true
# Global formatting settings (applied to console and files unless overridden)
format:
type: standard # standard or json
use_colors: true # Use colors (recommended false for files)
show_module: true # Show module name
timestamp_format: "%Y-%m-%d %H:%M:%S" # Timestamp format
# Specific settings for console output
console:
level: INFO # You can specify a separate level for the console
format:
type: standard # standard or json
use_colors: true # Colors in the console are usually helpful
show_module: true
timestamp_format: "%H:%M:%S" # Short timestamp format for console
# Logging file settings (you can specify multiple files)
files:
# Main log file - all logs
- path: "./logs/pyserve.log"
level: DEBUG
loggers: [] # Empty list means all loggers
max_bytes: 10485760 # 10MB (default)
backup_count: 5 # Number of backup files (default)
format:
type: standard
use_colors: false
show_module: true
# JSON log for automated analysis
- path: "./logs/pyserve.json"
level: INFO
loggers: []
format:
type: json
use_colors: false
show_module: true
# Separate file for server logs
- path: "./logs/server.log"
level: DEBUG
loggers: ["pyserve.server"] # Only logs from pyserve.server
format:
type: standard
use_colors: false
show_module: true
# Separate file for access logs
- path: "./logs/access.log"
level: INFO
loggers: ["pyserve.access"] # Only access logs
max_bytes: 5242880 # 5MB
backup_count: 10
format:
type: standard
use_colors: false
show_module: false # For access logs module is not needed
# Separate file for error logs
- path: "./logs/errors.log"
level: ERROR
loggers: [] # All errors from all loggers
format:
type: json # JSON for easy error parsing
use_colors: false
show_module: true
# Examples of various configurations:
# 1. Minimal configuration (backward compatibility):
# logging:
# level: INFO
# console_output: true
# files:
# - path: "./logs/pyserve.log"
# level: INFO
# loggers: []
# 2. Only JSON logs:
# logging:
# level: INFO
# console_output: false
# files:
# - path: "./logs/pyserve.json"
# format:
# type: json
# 3. Separation by log type:
# logging:
# level: DEBUG
# console_output: true
# files:
# - path: "./logs/general.log"
# level: INFO
# loggers: []
# - path: "./logs/server.log"
# level: DEBUG
# loggers: ["pyserve.server", "pyserve.routing"]
# - path: "./logs/access.log"
# level: INFO
# loggers: ["pyserve.access"]
# - path: "./logs/errors.json"
# level: ERROR
# loggers: []
# format:
# type: json
# 4. Console without colors, different formats for files:
# logging:
# level: DEBUG
# console_output: true
# console:
# format:
# use_colors: false
# files:
# - path: "./logs/standard.log"
# format:
# type: standard
# - path: "./logs/structured.json"
# format:
# type: json

View File

@ -1,9 +1,9 @@
""" """
PyServe - HTTP веб-сервер с функционалом nginx PyServe - HTTP web server written on Python
""" """
__version__ = "0.6.0" __version__ = "0.6.0"
__author__ = "Илья Глазунов" __author__ = "Ilya Glazunov"
from .server import PyServeServer from .server import PyServeServer
from .config import Config from .config import Config

View File

@ -8,7 +8,7 @@ from . import PyServeServer, Config, __version__
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="PyServe - HTTP web server", description="PyServe - HTTP web server",
prog="pyserve" prog="pyserve",
) )
parser.add_argument( parser.add_argument(
"-c", "--config", "-c", "--config",

View File

@ -1,6 +1,6 @@
import yaml import yaml
import os import os
from typing import Dict, Any, List from typing import Dict, Any, List, cast
from dataclasses import dataclass, field from dataclasses import dataclass, field
import logging import logging
from .logging_utils import setup_logging from .logging_utils import setup_logging
@ -28,11 +28,37 @@ class SSLConfig:
key_file: str = "./ssl/key.pem" key_file: str = "./ssl/key.pem"
@dataclass
class LogFormatConfig:
type: str = "standard"
use_colors: bool = True
show_module: bool = True
timestamp_format: str = "%Y-%m-%d %H:%M:%S"
@dataclass
class LogFileConfig:
path: str
level: str = "INFO"
format: LogFormatConfig = field(default_factory=LogFormatConfig)
loggers: List[str] = field(default_factory=list) # Список logger'ов для этого файла
max_bytes: int = 10 * 1024 * 1024 # 10MB
backup_count: int = 5
@dataclass
class LogHandlerConfig:
level: str = "INFO"
format: LogFormatConfig = field(default_factory=LogFormatConfig)
@dataclass @dataclass
class LoggingConfig: class LoggingConfig:
level: str = "INFO" level: str = "INFO"
console_output: bool = True console_output: bool = True
log_file: str = "./logs/pyserve.log" format: LogFormatConfig = field(default_factory=LogFormatConfig)
console: LogHandlerConfig = field(default_factory=LogHandlerConfig)
files: List[LogFileConfig] = field(default_factory=list)
@dataclass @dataclass
@ -99,10 +125,85 @@ class Config:
if 'logging' in data: if 'logging' in data:
log_data = data['logging'] log_data = data['logging']
format_data = log_data.get('format', {})
global_format = LogFormatConfig(
type=format_data.get('type', 'standard'),
use_colors=format_data.get('use_colors', True),
show_module=format_data.get('show_module', True),
timestamp_format=format_data.get('timestamp_format', '%Y-%m-%d %H:%M:%S')
)
console_data = log_data.get('console', {})
console_format_data = console_data.get('format', {})
console_format = LogFormatConfig(
type=console_format_data.get('type', global_format.type),
use_colors=console_format_data.get('use_colors', global_format.use_colors),
show_module=console_format_data.get('show_module', global_format.show_module),
timestamp_format=console_format_data.get('timestamp_format', global_format.timestamp_format)
)
console_config = LogHandlerConfig(
level=console_data.get('level', log_data.get('level', 'INFO')),
format=console_format
)
files_config = []
if 'log_file' in log_data:
default_file_format = LogFormatConfig(
type=global_format.type,
use_colors=False,
show_module=global_format.show_module,
timestamp_format=global_format.timestamp_format
)
default_file = LogFileConfig(
path=log_data['log_file'],
level=log_data.get('level', 'INFO'),
format=default_file_format,
loggers=[], # Empty list means including all loggers
max_bytes=10 * 1024 * 1024,
backup_count=5
)
files_config.append(default_file)
if 'files' in log_data:
for file_data in log_data['files']:
file_format_data = file_data.get('format', {})
file_format = LogFormatConfig(
type=file_format_data.get('type', global_format.type),
use_colors=file_format_data.get('use_colors', False),
show_module=file_format_data.get('show_module', global_format.show_module),
timestamp_format=file_format_data.get('timestamp_format', global_format.timestamp_format)
)
file_config = LogFileConfig(
path=file_data.get('path', './logs/pyserve.log'),
level=file_data.get('level', log_data.get('level', 'INFO')),
format=file_format,
loggers=file_data.get('loggers', []),
max_bytes=file_data.get('max_bytes', 10 * 1024 * 1024),
backup_count=file_data.get('backup_count', 5)
)
files_config.append(file_config)
if not files_config:
default_file_format = LogFormatConfig(
type=global_format.type,
use_colors=False,
show_module=global_format.show_module,
timestamp_format=global_format.timestamp_format
)
default_file = LogFileConfig(
path='./logs/pyserve.log',
level=log_data.get('level', 'INFO'),
format=default_file_format,
loggers=[],
max_bytes=10 * 1024 * 1024,
backup_count=5
)
files_config.append(default_file)
config.logging = LoggingConfig( config.logging = LoggingConfig(
level=log_data.get('level', config.logging.level), level=log_data.get('level', 'INFO'),
console_output=log_data.get('console_output', config.logging.console_output), console_output=log_data.get('console_output', True),
log_file=log_data.get('log_file', config.logging.log_file) format=global_format,
console=console_config,
files=files_config
) )
if 'extensions' in data: if 'extensions' in data:
@ -130,12 +231,34 @@ class Config:
if not (1 <= self.server.port <= 65535): if not (1 <= self.server.port <= 65535):
errors.append(f"Invalid port: {self.server.port}") errors.append(f"Invalid port: {self.server.port}")
log_dir = os.path.dirname(self.logging.log_file) valid_log_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
if self.logging.level.upper() not in valid_log_levels:
errors.append(f"Invalid logging level: {self.logging.level}")
if self.logging.console.level.upper() not in valid_log_levels:
errors.append(f"Invalid console logging level: {self.logging.console.level}")
valid_format_types = ['standard', 'json']
if self.logging.format.type not in valid_format_types:
errors.append(f"Invalid logging format type: {self.logging.format.type}")
if self.logging.console.format.type not in valid_format_types:
errors.append(f"Invalid console format type: {self.logging.console.format.type}")
for i, file_config in enumerate(self.logging.files):
if file_config.level.upper() not in valid_log_levels:
errors.append(f"Invalid file[{i}] logging level: {file_config.level}")
if file_config.format.type not in valid_format_types:
errors.append(f"Invalid file[{i}] format type: {file_config.format.type}")
log_dir = os.path.dirname(file_config.path)
if log_dir and not os.path.exists(log_dir): if log_dir and not os.path.exists(log_dir):
try: try:
os.makedirs(log_dir, exist_ok=True) os.makedirs(log_dir, exist_ok=True)
except OSError as e: except OSError as e:
errors.append(f"Unable to create log directory: {e}") errors.append(f"Unable to create log directory for {file_config.path}: {e}")
if errors: if errors:
for error in errors: for error in errors:
@ -148,6 +271,38 @@ class Config:
config_dict = { config_dict = {
'level': self.logging.level, 'level': self.logging.level,
'console_output': self.logging.console_output, 'console_output': self.logging.console_output,
'log_file': self.logging.log_file 'format': {
'type': self.logging.format.type,
'use_colors': self.logging.format.use_colors,
'show_module': self.logging.format.show_module,
'timestamp_format': self.logging.format.timestamp_format
},
'console': {
'level': self.logging.console.level,
'format': {
'type': self.logging.console.format.type,
'use_colors': self.logging.console.format.use_colors,
'show_module': self.logging.console.format.show_module,
'timestamp_format': self.logging.console.format.timestamp_format
} }
},
'files': []
}
for file_config in self.logging.files:
file_dict = {
'path': file_config.path,
'level': file_config.level,
'loggers': file_config.loggers,
'max_bytes': file_config.max_bytes,
'backup_count': file_config.backup_count,
'format': {
'type': file_config.format.type,
'use_colors': file_config.format.use_colors,
'show_module': file_config.format.show_module,
'timestamp_format': file_config.format.timestamp_format
}
}
cast(List[Dict[str, Any]], config_dict['files']).append(file_dict)
setup_logging(config_dict) setup_logging(config_dict)

View File

@ -152,7 +152,7 @@ class ExtensionManager:
def load_extension(self, extension_type: str, config: Dict[str, Any]) -> None: def load_extension(self, extension_type: str, config: Dict[str, Any]) -> None:
if extension_type not in self.extension_registry: if extension_type not in self.extension_registry:
logger.error(f"Неизвестный тип расширения: {extension_type}") logger.error(f"Unknown extension type: {extension_type}")
return return
try: try:
@ -160,9 +160,9 @@ class ExtensionManager:
extension = extension_class(config) extension = extension_class(config)
extension.initialize() extension.initialize()
self.extensions.append(extension) self.extensions.append(extension)
logger.info(f"Загружено расширение: {extension_type}") logger.info(f"Loaded extension: {extension_type}")
except Exception as e: except Exception as e:
logger.error(f"Ошибка загрузки расширения {extension_type}: {e}") logger.error(f"Error loading extension {extension_type}: {e}")
async def process_request(self, request: Request) -> Optional[Response]: async def process_request(self, request: Request) -> Optional[Response]:
for extension in self.extensions: for extension in self.extensions:
@ -174,7 +174,7 @@ class ExtensionManager:
if response is not None: if response is not None:
return response return response
except Exception as e: except Exception as e:
logger.error(f"Ошибка в расширении {type(extension).__name__}: {e}") logger.error(f"Error in extension {type(extension).__name__}: {e}")
return None return None
@ -186,7 +186,7 @@ class ExtensionManager:
try: try:
response = await extension.process_response(request, response) response = await extension.process_response(request, response)
except Exception as e: except Exception as e:
logger.error(f"Ошибка в расширении {type(extension).__name__}: {e}") logger.error(f"Error in extension {type(extension).__name__}: {e}")
return response return response
@ -195,6 +195,6 @@ class ExtensionManager:
try: try:
extension.cleanup() extension.cleanup()
except Exception as e: except Exception as e:
logger.error(f"Ошибка при очистке расширения {type(extension).__name__}: {e}") logger.error(f"Error cleaning up extension {type(extension).__name__}: {e}")
self.extensions.clear() self.extensions.clear()

View File

@ -2,12 +2,30 @@ import logging
import logging.handlers import logging.handlers
import sys import sys
import time import time
import json
from pathlib import Path from pathlib import Path
from typing import Dict, Any, List from typing import Dict, Any, List
from . import __version__ from . import __version__
class LoggerFilter(logging.Filter):
def __init__(self, logger_names: List[str]):
super().__init__()
self.logger_names = logger_names
self.accept_all = len(logger_names) == 0
def filter(self, record: logging.LogRecord) -> bool:
if self.accept_all:
return True
for logger_name in self.logger_names:
if record.name == logger_name or record.name.startswith(logger_name + '.'):
return True
return False
class UvicornLogFilter(logging.Filter): class UvicornLogFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool: def filter(self, record: logging.LogRecord) -> bool:
if hasattr(record, 'name') and 'uvicorn.access' in record.name: if hasattr(record, 'name') and 'uvicorn.access' in record.name:
@ -36,10 +54,12 @@ class PyServeFormatter(logging.Formatter):
'RESET': '\033[0m' # Reset 'RESET': '\033[0m' # Reset
} }
def __init__(self, use_colors: bool = True, show_module: bool = True, *args: Any, **kwargs: Any): def __init__(self, use_colors: bool = True, show_module: bool = True,
timestamp_format: str = "%Y-%m-%d %H:%M:%S", *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.use_colors = use_colors and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() self.use_colors = use_colors and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty()
self.show_module = show_module self.show_module = show_module
self.timestamp_format = timestamp_format
def format(self, record: logging.LogRecord) -> str: def format(self, record: logging.LogRecord) -> str:
if self.use_colors: if self.use_colors:
@ -59,6 +79,37 @@ class PyServeFormatter(logging.Formatter):
return super().format(record) return super().format(record)
class PyServeJSONFormatter(logging.Formatter):
def __init__(self, timestamp_format: str = "%Y-%m-%d %H:%M:%S", *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.timestamp_format = timestamp_format
def format(self, record: logging.LogRecord) -> str:
log_entry = {
'timestamp': time.strftime(self.timestamp_format, time.localtime(record.created)),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno,
'thread': record.thread,
'thread_name': record.threadName,
}
if record.exc_info:
log_entry['exception'] = self.formatException(record.exc_info)
for key, value in record.__dict__.items():
if key not in ['name', 'msg', 'args', 'levelname', 'levelno', 'pathname',
'filename', 'module', 'lineno', 'funcName', 'created',
'msecs', 'relativeCreated', 'thread', 'threadName',
'processName', 'process', 'getMessage', 'exc_info', 'exc_text', 'stack_info']:
log_entry[key] = value
return json.dumps(log_entry, ensure_ascii=False, default=str)
class AccessLogHandler(logging.Handler): class AccessLogHandler(logging.Handler):
def __init__(self, logger_name: str = 'pyserve.access'): def __init__(self, logger_name: str = 'pyserve.access'):
super().__init__() super().__init__()
@ -75,68 +126,154 @@ class PyServeLogManager:
self.loggers: Dict[str, logging.Logger] = {} self.loggers: Dict[str, logging.Logger] = {}
self.original_handlers: Dict[str, List[logging.Handler]] = {} self.original_handlers: Dict[str, List[logging.Handler]] = {}
def _create_formatter(self, format_config: Dict[str, Any]) -> logging.Formatter:
format_type = format_config.get('type', 'standard').lower()
use_colors = format_config.get('use_colors', True)
show_module = format_config.get('show_module', True)
timestamp_format = format_config.get('timestamp_format', '%Y-%m-%d %H:%M:%S')
if format_type == 'json':
return PyServeJSONFormatter(timestamp_format=timestamp_format)
else:
if format_type == 'json':
fmt = None
else:
fmt = '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
return PyServeFormatter(
use_colors=use_colors,
show_module=show_module,
timestamp_format=timestamp_format,
fmt=fmt
)
def setup_logging(self, config: Dict[str, Any]) -> None: def setup_logging(self, config: Dict[str, Any]) -> None:
if self.configured: if self.configured:
return return
if 'format' not in config and 'console' not in config and 'files' not in config:
level = config.get('level', 'INFO').upper() level = config.get('level', 'INFO').upper()
console_output = config.get('console_output', True) console_output = config.get('console_output', True)
log_file = config.get('log_file', './logs/pyserve.log') log_file = config.get('log_file', './logs/pyserve.log')
config = {
'level': level,
'console_output': console_output,
'format': {
'type': 'standard',
'use_colors': True,
'show_module': True,
'timestamp_format': '%Y-%m-%d %H:%M:%S'
},
'files': [{
'path': log_file,
'level': level,
'loggers': [],
'max_bytes': 10 * 1024 * 1024,
'backup_count': 5,
'format': {
'type': 'standard',
'use_colors': False,
'show_module': True,
'timestamp_format': '%Y-%m-%d %H:%M:%S'
}
}]
}
main_level = config.get('level', 'INFO').upper()
console_output = config.get('console_output', True)
global_format = config.get('format', {})
console_config = config.get('console', {})
files_config = config.get('files', [])
console_format = {**global_format, **console_config.get('format', {})}
console_level = console_config.get('level', main_level)
self._save_original_handlers() self._save_original_handlers()
self._clear_all_handlers() self._clear_all_handlers()
root_logger = logging.getLogger() root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) root_logger.setLevel(logging.DEBUG)
detailed_formatter = PyServeFormatter( if console_output:
use_colors=False, console_handler = logging.StreamHandler(sys.stdout)
show_module=True, console_handler.setLevel(getattr(logging, console_level))
fmt='%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
)
if console_format.get('type') == 'json':
console_formatter = self._create_formatter(console_format)
else:
console_formatter = PyServeFormatter( console_formatter = PyServeFormatter(
use_colors=True, use_colors=console_format.get('use_colors', True),
show_module=True, show_module=console_format.get('show_module', True),
timestamp_format=console_format.get('timestamp_format', '%Y-%m-%d %H:%M:%S'),
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s' fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
) )
if console_output:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(getattr(logging, level))
console_handler.setFormatter(console_formatter) console_handler.setFormatter(console_formatter)
console_handler.addFilter(UvicornLogFilter()) console_handler.addFilter(UvicornLogFilter())
root_logger.addHandler(console_handler) root_logger.addHandler(console_handler)
self.handlers['console'] = console_handler self.handlers['console'] = console_handler
if log_file: for i, file_config in enumerate(files_config):
self._ensure_log_directory(log_file) file_path = file_config.get('path', './logs/pyserve.log')
file_level = file_config.get('level', main_level)
file_loggers = file_config.get('loggers', [])
max_bytes = file_config.get('max_bytes', 10 * 1024 * 1024)
backup_count = file_config.get('backup_count', 5)
file_format = {**global_format, **file_config.get('format', {})}
self._ensure_log_directory(file_path)
file_handler = logging.handlers.RotatingFileHandler( file_handler = logging.handlers.RotatingFileHandler(
log_file, file_path,
maxBytes=10 * 1024 * 1024, # 10MB maxBytes=max_bytes,
backupCount=5, backupCount=backup_count,
encoding='utf-8' encoding='utf-8'
) )
file_handler.setLevel(logging.DEBUG) file_handler.setLevel(getattr(logging, file_level))
file_handler.setFormatter(detailed_formatter)
if file_format.get('type') == 'json':
file_formatter = self._create_formatter(file_format)
else:
file_formatter = PyServeFormatter(
use_colors=file_format.get('use_colors', False),
show_module=file_format.get('show_module', True),
timestamp_format=file_format.get('timestamp_format', '%Y-%m-%d %H:%M:%S'),
fmt='%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
)
file_handler.setFormatter(file_formatter)
file_handler.addFilter(UvicornLogFilter()) file_handler.addFilter(UvicornLogFilter())
if file_loggers:
logger_filter = LoggerFilter(file_loggers)
file_handler.addFilter(logger_filter)
root_logger.addHandler(file_handler) root_logger.addHandler(file_handler)
self.handlers['file'] = file_handler self.handlers[f'file_{i}'] = file_handler
self._configure_library_loggers(level)
self._configure_library_loggers(main_level)
self._intercept_uvicorn_logging() self._intercept_uvicorn_logging()
pyserve_logger = logging.getLogger('pyserve') pyserve_logger = logging.getLogger('pyserve')
pyserve_logger.setLevel(getattr(logging, level)) pyserve_logger.setLevel(getattr(logging, main_level))
self.loggers['pyserve'] = pyserve_logger self.loggers['pyserve'] = pyserve_logger
pyserve_logger.info(f"PyServe v{__version__} - Logger initialized") pyserve_logger.info(f"PyServe v{__version__} - Logger initialized")
pyserve_logger.info(f"Logging level: {level}") pyserve_logger.info(f"Logging level: {main_level}")
pyserve_logger.info(f"Console output: {'enabled' if console_output else 'disabled'}") pyserve_logger.info(f"Console output: {'enabled' if console_output else 'disabled'}")
pyserve_logger.info(f"Log file: {log_file if log_file else 'disabled'}") pyserve_logger.info(f"Console format: {console_format.get('type', 'standard')}")
for i, file_config in enumerate(files_config):
file_path = file_config.get('path', './logs/pyserve.log')
file_loggers = file_config.get('loggers', [])
file_format = file_config.get('format', {})
pyserve_logger.info(f"Log file[{i}]: {file_path}")
pyserve_logger.info(f"File format[{i}]: {file_format.get('type', 'standard')}")
if file_loggers:
pyserve_logger.info(f"File loggers[{i}]: {', '.join(file_loggers)}")
else:
pyserve_logger.info(f"File loggers[{i}]: all loggers")
self.configured = True self.configured = True
@ -168,13 +305,13 @@ class PyServeLogManager:
def _configure_library_loggers(self, main_level: str) -> None: def _configure_library_loggers(self, main_level: str) -> None:
library_configs = { library_configs = {
# Uvicorn и связанные - только в DEBUG режиме # Uvicorn and related - only in DEBUG mode
'uvicorn': 'DEBUG' if main_level == 'DEBUG' else 'WARNING', 'uvicorn': 'DEBUG' if main_level == 'DEBUG' else 'WARNING',
'uvicorn.access': 'DEBUG' if main_level == 'DEBUG' else 'WARNING', 'uvicorn.access': 'DEBUG' if main_level == 'DEBUG' else 'WARNING',
'uvicorn.error': 'DEBUG' if main_level == 'DEBUG' else 'ERROR', 'uvicorn.error': 'DEBUG' if main_level == 'DEBUG' else 'ERROR',
'uvicorn.asgi': 'DEBUG' if main_level == 'DEBUG' else 'WARNING', 'uvicorn.asgi': 'DEBUG' if main_level == 'DEBUG' else 'WARNING',
# Starlette - только в DEBUG режиме # Starlette - only in DEBUG mode
'starlette': 'DEBUG' if main_level == 'DEBUG' else 'WARNING', 'starlette': 'DEBUG' if main_level == 'DEBUG' else 'WARNING',
'asyncio': 'WARNING', 'asyncio': 'WARNING',

View File

@ -166,7 +166,7 @@ class RequestHandler:
async def _handle_proxy(self, request: Request, config: Dict[str, Any], async def _handle_proxy(self, request: Request, config: Dict[str, Any],
params: Dict[str, str]) -> Response: params: Dict[str, str]) -> Response:
# TODO: Реализовать полноценное проксирование # TODO: implement real proxying
proxy_url = config["proxy_pass"] proxy_url = config["proxy_pass"]
for key, value in params.items(): for key, value in params.items():

View File

@ -208,7 +208,8 @@ class PyServeServer:
self.config.http.templates_dir, self.config.http.templates_dir,
] ]
log_dir = Path(self.config.logging.log_file).parent for file_config in self.config.logging.files:
log_dir = Path(file_config.path).parent
if log_dir != Path("."): if log_dir != Path("."):
directories.append(str(log_dir)) directories.append(str(log_dir))