feat: Add CLI for PyServe with configuration options
- Introduced a new CLI module (`cli.py`) to manage server configurations via command line arguments. - Added script entry point in `pyproject.toml` for easy access to the CLI. - Enhanced `Config` class to load configurations from a YAML file. - Updated `__init__.py` to include `__version__` in the module exports. - Added optional dependencies for development tools in `pyproject.toml`. - Implemented logging improvements and error handling in various modules. - Created tests for the CLI functionality to ensure proper behavior. - Removed the old `run.py` implementation in favor of the new CLI approach.
This commit is contained in:
+1
-1
@@ -8,4 +8,4 @@ __author__ = "Илья Глазунов"
|
||||
from .server import PyServeServer
|
||||
from .config import Config
|
||||
|
||||
__all__ = ["PyServeServer", "Config"]
|
||||
__all__ = ["PyServeServer", "Config", "__version__"]
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from . import PyServeServer, Config, __version__
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="PyServe - HTTP web server",
|
||||
prog="pyserve"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--config",
|
||||
default="config.yaml",
|
||||
help="Path to configuration file (default: config.yaml)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
help="Host to bind the server to"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
help="Port to bind the server to"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
help="Enable debug mode"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"%(prog)s {__version__}"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
config_path = args.config
|
||||
if not Path(config_path).exists():
|
||||
print(f"Configuration file {config_path} not found")
|
||||
print("Using default configuration")
|
||||
config = Config()
|
||||
else:
|
||||
try:
|
||||
config = Config.from_yaml(config_path)
|
||||
except Exception as e:
|
||||
print(f"Configuration loading error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if args.host:
|
||||
config.server.host = args.host
|
||||
if args.port:
|
||||
config.server.port = args.port
|
||||
if args.debug:
|
||||
config.logging.level = "DEBUG"
|
||||
|
||||
server = PyServeServer(config)
|
||||
|
||||
try:
|
||||
print(f"Starting PyServe server on {config.server.host}:{config.server.port}")
|
||||
server.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\nServer stopped by user")
|
||||
except Exception as e:
|
||||
print(f"Server startup error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+22
-24
@@ -1,7 +1,6 @@
|
||||
import yaml
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional
|
||||
from typing import Dict, Any, List
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from .logging_utils import setup_logging
|
||||
@@ -60,26 +59,26 @@ class Config:
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
|
||||
return cls._from_dict(data)
|
||||
except FileNotFoundError:
|
||||
logging.warning(f"Конфигурационный файл {file_path} не найден. Используются значения по умолчанию.")
|
||||
logging.warning(f"Configuration file {file_path} not found. Using default values.")
|
||||
return cls()
|
||||
except yaml.YAMLError as e:
|
||||
logging.error(f"Ошибка парсинга YAML файла {file_path}: {e}")
|
||||
logging.error(f"YAML file parsing error {file_path}: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, data: Dict[str, Any]) -> "Config":
|
||||
config = cls()
|
||||
|
||||
|
||||
if 'http' in data:
|
||||
http_data = data['http']
|
||||
config.http = HttpConfig(
|
||||
static_dir=http_data.get('static_dir', config.http.static_dir),
|
||||
templates_dir=http_data.get('templates_dir', config.http.templates_dir)
|
||||
)
|
||||
|
||||
|
||||
if 'server' in data:
|
||||
server_data = data['server']
|
||||
config.server = ServerConfig(
|
||||
@@ -89,7 +88,7 @@ class Config:
|
||||
default_root=server_data.get('default_root', config.server.default_root),
|
||||
redirect_instructions=server_data.get('redirect_instructions', {})
|
||||
)
|
||||
|
||||
|
||||
if 'ssl' in data:
|
||||
ssl_data = data['ssl']
|
||||
config.ssl = SSLConfig(
|
||||
@@ -97,7 +96,7 @@ class Config:
|
||||
cert_file=ssl_data.get('cert_file', config.ssl.cert_file),
|
||||
key_file=ssl_data.get('key_file', config.ssl.key_file)
|
||||
)
|
||||
|
||||
|
||||
if 'logging' in data:
|
||||
log_data = data['logging']
|
||||
config.logging = LoggingConfig(
|
||||
@@ -105,7 +104,7 @@ class Config:
|
||||
console_output=log_data.get('console_output', config.logging.console_output),
|
||||
log_file=log_data.get('log_file', config.logging.log_file)
|
||||
)
|
||||
|
||||
|
||||
if 'extensions' in data:
|
||||
for ext_data in data['extensions']:
|
||||
extension = ExtensionConfig(
|
||||
@@ -113,40 +112,39 @@ class Config:
|
||||
config=ext_data.get('config', {})
|
||||
)
|
||||
config.extensions.append(extension)
|
||||
|
||||
|
||||
return config
|
||||
|
||||
def validate(self) -> bool:
|
||||
errors = []
|
||||
|
||||
|
||||
if not os.path.exists(self.http.static_dir):
|
||||
errors.append(f"Статическая директория не существует: {self.http.static_dir}")
|
||||
|
||||
errors.append(f"Static directory does not exist: {self.http.static_dir}")
|
||||
|
||||
if self.ssl.enabled:
|
||||
if not os.path.exists(self.ssl.cert_file):
|
||||
errors.append(f"SSL сертификат не найден: {self.ssl.cert_file}")
|
||||
errors.append(f"SSL certificate not found: {self.ssl.cert_file}")
|
||||
if not os.path.exists(self.ssl.key_file):
|
||||
errors.append(f"SSL ключ не найден: {self.ssl.key_file}")
|
||||
|
||||
errors.append(f"SSL key not found: {self.ssl.key_file}")
|
||||
|
||||
if not (1 <= self.server.port <= 65535):
|
||||
errors.append(f"Некорректный порт: {self.server.port}")
|
||||
|
||||
errors.append(f"Invalid port: {self.server.port}")
|
||||
|
||||
log_dir = os.path.dirname(self.logging.log_file)
|
||||
if log_dir and not os.path.exists(log_dir):
|
||||
try:
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
except OSError as e:
|
||||
errors.append(f"Невозможно создать директорию для логов: {e}")
|
||||
|
||||
errors.append(f"Unable to create log directory: {e}")
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
logging.error(f"Ошибка конфигурации: {error}")
|
||||
logging.error(f"Configuration error: {error}")
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def setup_logging(self) -> None:
|
||||
"""Настройка системы логирования через кастомный менеджер"""
|
||||
config_dict = {
|
||||
'level': self.logging.level,
|
||||
'console_output': self.logging.console_output,
|
||||
|
||||
+47
-47
@@ -7,48 +7,48 @@ from .logging_utils import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Extension(ABC):
|
||||
class Extension(ABC):
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.config = config
|
||||
self.enabled = True
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def process_request(self, request: Request) -> Optional[Response]:
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def process_response(self, request: Request, response: Response) -> Response:
|
||||
pass
|
||||
|
||||
|
||||
def initialize(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def cleanup(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class RoutingExtension(Extension):
|
||||
class RoutingExtension(Extension):
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
super().__init__(config)
|
||||
from .routing import create_router_from_config
|
||||
|
||||
|
||||
regex_locations = config.get("regex_locations", {})
|
||||
self.router = create_router_from_config(regex_locations)
|
||||
from .routing import RequestHandler
|
||||
self.handler = RequestHandler(self.router)
|
||||
|
||||
|
||||
async def process_request(self, request: Request) -> Optional[Response]:
|
||||
try:
|
||||
return await self.handler.handle(request)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в RoutingExtension: {e}")
|
||||
logger.error(f"Error in RoutingExtension: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def process_response(self, request: Request, response: Response) -> Response:
|
||||
return response
|
||||
|
||||
|
||||
class SecurityExtension(Extension):
|
||||
class SecurityExtension(Extension):
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
super().__init__(config)
|
||||
self.allowed_ips = config.get("allowed_ips", [])
|
||||
@@ -58,76 +58,76 @@ class SecurityExtension(Extension):
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-XSS-Protection": "1; mode=block"
|
||||
})
|
||||
|
||||
|
||||
async def process_request(self, request: Request) -> Optional[Response]:
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
|
||||
|
||||
if self.blocked_ips and client_ip in self.blocked_ips:
|
||||
logger.warning(f"Заблокирован запрос от IP: {client_ip}")
|
||||
logger.warning(f"Blocked request from IP: {client_ip}")
|
||||
from starlette.responses import PlainTextResponse
|
||||
return PlainTextResponse("403 Forbidden", status_code=403)
|
||||
|
||||
|
||||
if self.allowed_ips and client_ip not in self.allowed_ips:
|
||||
logger.warning(f"Запрещен доступ для IP: {client_ip}")
|
||||
logger.warning(f"Access denied for IP: {client_ip}")
|
||||
from starlette.responses import PlainTextResponse
|
||||
return PlainTextResponse("403 Forbidden", status_code=403)
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def process_response(self, request: Request, response: Response) -> Response:
|
||||
for header, value in self.security_headers.items():
|
||||
response.headers[header] = value
|
||||
return response
|
||||
|
||||
|
||||
class CachingExtension(Extension):
|
||||
class CachingExtension(Extension):
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
super().__init__(config)
|
||||
self.cache: Dict[str, Any] = {}
|
||||
self.cache_patterns = config.get("cache_patterns", [])
|
||||
self.cache_ttl = config.get("cache_ttl", 3600)
|
||||
|
||||
|
||||
async def process_request(self, request: Request) -> Optional[Response]:
|
||||
# TODO: Реализовать проверку кэша
|
||||
# TODO: Implement cache check
|
||||
return None
|
||||
|
||||
|
||||
async def process_response(self, request: Request, response: Response) -> Response:
|
||||
# TODO: Реализовать кэширование ответов
|
||||
# TODO: Implement response caching
|
||||
return response
|
||||
|
||||
|
||||
class MonitoringExtension(Extension):
|
||||
class MonitoringExtension(Extension):
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
super().__init__(config)
|
||||
self.request_count = 0
|
||||
self.error_count = 0
|
||||
self.response_times = []
|
||||
self.enable_metrics = config.get("enable_metrics", True)
|
||||
|
||||
|
||||
async def process_request(self, request: Request) -> Optional[Response]:
|
||||
if self.enable_metrics:
|
||||
self.request_count += 1
|
||||
request.state.start_time = __import__('time').time()
|
||||
return None
|
||||
|
||||
|
||||
async def process_response(self, request: Request, response: Response) -> Response:
|
||||
if self.enable_metrics and hasattr(request.state, 'start_time'):
|
||||
response_time = __import__('time').time() - request.state.start_time
|
||||
self.response_times.append(response_time)
|
||||
|
||||
|
||||
if response.status_code >= 400:
|
||||
self.error_count += 1
|
||||
|
||||
|
||||
logger.info(f"Request: {request.method} {request.url.path} - "
|
||||
f"Status: {response.status_code} - "
|
||||
f"Time: {response_time:.3f}s")
|
||||
|
||||
f"Status: {response.status_code} - "
|
||||
f"Time: {response_time:.3f}s")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def get_metrics(self) -> Dict[str, Any]:
|
||||
avg_response_time = (sum(self.response_times) / len(self.response_times)
|
||||
if self.response_times else 0)
|
||||
|
||||
avg_response_time = (sum(self.response_times) / len(self.response_times)
|
||||
if self.response_times else 0)
|
||||
|
||||
return {
|
||||
"request_count": self.request_count,
|
||||
"error_count": self.error_count,
|
||||
@@ -137,7 +137,7 @@ class MonitoringExtension(Extension):
|
||||
}
|
||||
|
||||
|
||||
class ExtensionManager:
|
||||
class ExtensionManager:
|
||||
def __init__(self):
|
||||
self.extensions: List[Extension] = []
|
||||
self.extension_registry = {
|
||||
@@ -146,15 +146,15 @@ class ExtensionManager:
|
||||
"caching": CachingExtension,
|
||||
"monitoring": MonitoringExtension
|
||||
}
|
||||
|
||||
|
||||
def register_extension_type(self, name: str, extension_class: type) -> None:
|
||||
self.extension_registry[name] = extension_class
|
||||
|
||||
|
||||
def load_extension(self, extension_type: str, config: Dict[str, Any]) -> None:
|
||||
if extension_type not in self.extension_registry:
|
||||
logger.error(f"Неизвестный тип расширения: {extension_type}")
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
extension_class = self.extension_registry[extension_type]
|
||||
extension = extension_class(config)
|
||||
@@ -163,38 +163,38 @@ class ExtensionManager:
|
||||
logger.info(f"Загружено расширение: {extension_type}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка загрузки расширения {extension_type}: {e}")
|
||||
|
||||
|
||||
async def process_request(self, request: Request) -> Optional[Response]:
|
||||
for extension in self.extensions:
|
||||
if not extension.enabled:
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
response = await extension.process_request(request)
|
||||
if response is not None:
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в расширении {type(extension).__name__}: {e}")
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def process_response(self, request: Request, response: Response) -> Response:
|
||||
for extension in self.extensions:
|
||||
if not extension.enabled:
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
response = await extension.process_response(request, response)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в расширении {type(extension).__name__}: {e}")
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def cleanup(self) -> None:
|
||||
for extension in self.extensions:
|
||||
try:
|
||||
extension.cleanup()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при очистке расширения {type(extension).__name__}: {e}")
|
||||
|
||||
|
||||
self.extensions.clear()
|
||||
|
||||
+61
-60
@@ -13,7 +13,7 @@ from typing import Dict, Any, List
|
||||
from . import __version__
|
||||
|
||||
|
||||
class UvicornLogFilter(logging.Filter):
|
||||
class UvicornLogFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
if hasattr(record, 'name') and 'uvicorn.access' in record.name:
|
||||
if hasattr(record, 'getMessage'):
|
||||
@@ -27,31 +27,31 @@ class UvicornLogFilter(logging.Filter):
|
||||
method_path = request_part[0]
|
||||
status_part = request_part[1]
|
||||
record.msg = f"Access: {client_info} - {method_path} - {status_part}"
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class PyServeFormatter(logging.Formatter):
|
||||
class PyServeFormatter(logging.Formatter):
|
||||
COLORS = {
|
||||
'DEBUG': '\033[36m', # Cyan
|
||||
'INFO': '\033[32m', # Green
|
||||
'INFO': '\033[32m', # Green
|
||||
'WARNING': '\033[33m', # Yellow
|
||||
'ERROR': '\033[31m', # Red
|
||||
'CRITICAL': '\033[35m', # Magenta
|
||||
'RESET': '\033[0m' # Reset
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, use_colors: bool = True, show_module: bool = True, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.use_colors = use_colors and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty()
|
||||
self.show_module = show_module
|
||||
|
||||
|
||||
def format(self, record):
|
||||
if self.use_colors:
|
||||
levelname = record.levelname
|
||||
if levelname in self.COLORS:
|
||||
record.levelname = f"{self.COLORS[levelname]}{levelname}{self.COLORS['RESET']}"
|
||||
|
||||
|
||||
if self.show_module and hasattr(record, 'name'):
|
||||
name = record.name
|
||||
if name.startswith('uvicorn'):
|
||||
@@ -60,30 +60,30 @@ class PyServeFormatter(logging.Formatter):
|
||||
pass
|
||||
elif name.startswith('starlette'):
|
||||
record.name = 'starlette'
|
||||
|
||||
|
||||
return super().format(record)
|
||||
|
||||
|
||||
class AccessLogHandler(logging.Handler):
|
||||
class AccessLogHandler(logging.Handler):
|
||||
def __init__(self, logger_name: str = 'pyserve.access'):
|
||||
super().__init__()
|
||||
self.access_logger = logging.getLogger(logger_name)
|
||||
|
||||
|
||||
def emit(self, record):
|
||||
self.access_logger.handle(record)
|
||||
|
||||
|
||||
class PyServeLogManager:
|
||||
class PyServeLogManager:
|
||||
def __init__(self):
|
||||
self.configured = False
|
||||
self.handlers: Dict[str, logging.Handler] = {}
|
||||
self.loggers: Dict[str, logging.Logger] = {}
|
||||
self.original_handlers: Dict[str, List[logging.Handler]] = {}
|
||||
|
||||
|
||||
def setup_logging(self, config: Dict[str, Any]) -> None:
|
||||
if self.configured:
|
||||
return
|
||||
|
||||
|
||||
level = config.get('level', 'INFO').upper()
|
||||
console_output = config.get('console_output', True)
|
||||
log_file = config.get('log_file', './logs/pyserve.log')
|
||||
@@ -91,97 +91,97 @@ class PyServeLogManager:
|
||||
self._clear_all_handlers()
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
detailed_formatter = PyServeFormatter(
|
||||
use_colors=False,
|
||||
show_module=True,
|
||||
fmt='%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
|
||||
)
|
||||
|
||||
|
||||
console_formatter = PyServeFormatter(
|
||||
use_colors=True,
|
||||
show_module=True,
|
||||
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.addFilter(UvicornLogFilter())
|
||||
|
||||
|
||||
root_logger.addHandler(console_handler)
|
||||
self.handlers['console'] = console_handler
|
||||
|
||||
|
||||
if log_file:
|
||||
self._ensure_log_directory(log_file)
|
||||
|
||||
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=10*1024*1024, # 10MB
|
||||
maxBytes=10 * 1024 * 1024, # 10MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(detailed_formatter)
|
||||
file_handler.addFilter(UvicornLogFilter())
|
||||
|
||||
|
||||
root_logger.addHandler(file_handler)
|
||||
self.handlers['file'] = file_handler
|
||||
|
||||
|
||||
self._configure_library_loggers(level)
|
||||
|
||||
|
||||
self._intercept_uvicorn_logging()
|
||||
|
||||
|
||||
pyserve_logger = logging.getLogger('pyserve')
|
||||
pyserve_logger.setLevel(getattr(logging, level))
|
||||
self.loggers['pyserve'] = pyserve_logger
|
||||
|
||||
|
||||
pyserve_logger.info(f"PyServe v{__version__} - Система логирования инициализирована")
|
||||
pyserve_logger.info(f"Уровень логирования: {level}")
|
||||
pyserve_logger.info(f"Консольный вывод: {'включен' if console_output else 'отключен'}")
|
||||
pyserve_logger.info(f"Файл логов: {log_file if log_file else 'отключен'}")
|
||||
|
||||
|
||||
self.configured = True
|
||||
|
||||
|
||||
def _save_original_handlers(self) -> None:
|
||||
logger_names = ['', 'uvicorn', 'uvicorn.access', 'uvicorn.error', 'starlette']
|
||||
|
||||
|
||||
for name in logger_names:
|
||||
logger = logging.getLogger(name)
|
||||
self.original_handlers[name] = logger.handlers.copy()
|
||||
|
||||
|
||||
def _clear_all_handlers(self) -> None:
|
||||
root_logger = logging.getLogger()
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
|
||||
logger_names = ['uvicorn', 'uvicorn.access', 'uvicorn.error', 'starlette']
|
||||
for name in logger_names:
|
||||
logger = logging.getLogger(name)
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
|
||||
self.handlers.clear()
|
||||
|
||||
|
||||
def _ensure_log_directory(self, log_file: str) -> None:
|
||||
log_dir = Path(log_file).parent
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _configure_library_loggers(self, main_level: str) -> None:
|
||||
|
||||
def _configure_library_loggers(self, main_level: str) -> None:
|
||||
library_configs = {
|
||||
# Uvicorn и связанные - только в DEBUG режиме
|
||||
'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.asgi': 'DEBUG' if main_level == 'DEBUG' else 'WARNING',
|
||||
|
||||
|
||||
# Starlette - только в DEBUG режиме
|
||||
'starlette': 'DEBUG' if main_level == 'DEBUG' else 'WARNING',
|
||||
|
||||
|
||||
'asyncio': 'WARNING',
|
||||
'concurrent.futures': 'WARNING',
|
||||
'multiprocessing': 'WARNING',
|
||||
@@ -191,74 +191,75 @@ class PyServeLogManager:
|
||||
'pyserve.extensions': main_level,
|
||||
'pyserve.config': main_level,
|
||||
}
|
||||
|
||||
|
||||
for logger_name, level in library_configs.items():
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(getattr(logging, level))
|
||||
if logger_name.startswith('uvicorn') and logger_name != 'uvicorn':
|
||||
logger.propagate = False
|
||||
self.loggers[logger_name] = logger
|
||||
|
||||
|
||||
def _intercept_uvicorn_logging(self) -> None:
|
||||
uvicorn_logger = logging.getLogger('uvicorn')
|
||||
uvicorn_access_logger = logging.getLogger('uvicorn.access')
|
||||
|
||||
|
||||
for handler in uvicorn_logger.handlers[:]:
|
||||
uvicorn_logger.removeHandler(handler)
|
||||
|
||||
|
||||
for handler in uvicorn_access_logger.handlers[:]:
|
||||
uvicorn_access_logger.removeHandler(handler)
|
||||
|
||||
|
||||
uvicorn_logger.propagate = True
|
||||
uvicorn_access_logger.propagate = True
|
||||
|
||||
|
||||
def get_logger(self, name: str) -> logging.Logger:
|
||||
if name not in self.loggers:
|
||||
logger = logging.getLogger(name)
|
||||
self.loggers[name] = logger
|
||||
|
||||
|
||||
return self.loggers[name]
|
||||
|
||||
|
||||
def set_level(self, logger_name: str, level: str) -> None:
|
||||
if logger_name in self.loggers:
|
||||
self.loggers[logger_name].setLevel(getattr(logging, level.upper()))
|
||||
|
||||
|
||||
def add_handler(self, name: str, handler: logging.Handler) -> None:
|
||||
if name not in self.handlers:
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.addHandler(handler)
|
||||
self.handlers[name] = handler
|
||||
|
||||
|
||||
def remove_handler(self, name: str) -> None:
|
||||
if name in self.handlers:
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.removeHandler(self.handlers[name])
|
||||
self.handlers[name].close()
|
||||
del self.handlers[name]
|
||||
|
||||
def create_access_log(self, method: str, path: str, status_code: int,
|
||||
response_time: float, client_ip: str, user_agent: str = "") -> None:
|
||||
|
||||
def create_access_log(self, method: str, path: str, status_code: int,
|
||||
response_time: float, client_ip: str, user_agent: str = "") -> None:
|
||||
access_logger = self.get_logger('pyserve.access')
|
||||
|
||||
|
||||
log_message = f'{client_ip} - - [{time.strftime("%d/%b/%Y:%H:%M:%S %z")}] ' \
|
||||
f'"{method} {path} HTTP/1.1" {status_code} - ' \
|
||||
f'"{user_agent}" {response_time:.3f}s'
|
||||
|
||||
f'"{method} {path} HTTP/1.1" {status_code} - ' \
|
||||
f'"{user_agent}" {response_time:.3f}s'
|
||||
|
||||
access_logger.info(log_message)
|
||||
|
||||
|
||||
def shutdown(self) -> None:
|
||||
for handler in self.handlers.values():
|
||||
handler.close()
|
||||
self.handlers.clear()
|
||||
|
||||
|
||||
for logger_name, handlers in self.original_handlers.items():
|
||||
logger = logging.getLogger(logger_name)
|
||||
for handler in handlers:
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
self.loggers.clear()
|
||||
self.configured = False
|
||||
|
||||
|
||||
log_manager = PyServeLogManager()
|
||||
|
||||
|
||||
@@ -270,8 +271,8 @@ def get_logger(name: str) -> logging.Logger:
|
||||
return log_manager.get_logger(name)
|
||||
|
||||
|
||||
def create_access_log(method: str, path: str, status_code: int,
|
||||
response_time: float, client_ip: str, user_agent: str = "") -> None:
|
||||
def create_access_log(method: str, path: str, status_code: int,
|
||||
response_time: float, client_ip: str, user_agent: str = "") -> None:
|
||||
log_manager.create_access_log(method, path, status_code, response_time, client_ip, user_agent)
|
||||
|
||||
|
||||
|
||||
+6
-6
@@ -24,12 +24,12 @@ class Router:
|
||||
if pattern.startswith("="):
|
||||
exact_path = pattern[1:]
|
||||
self.exact_routes[exact_path] = config
|
||||
logger.debug(f"Добавлен exact маршрут: {exact_path}")
|
||||
logger.debug(f"Added exact route: {exact_path}")
|
||||
return
|
||||
|
||||
if pattern == "__default__":
|
||||
self.default_route = config
|
||||
logger.debug("Добавлен default маршрут")
|
||||
logger.debug("Added default route")
|
||||
return
|
||||
|
||||
if pattern.startswith("~"):
|
||||
@@ -40,9 +40,9 @@ class Router:
|
||||
try:
|
||||
compiled_pattern = re.compile(regex_pattern, flags)
|
||||
self.routes[compiled_pattern] = config
|
||||
logger.debug(f"Добавлен regex маршрут: {pattern}")
|
||||
logger.debug(f"Added regex route: {pattern}")
|
||||
except re.error as e:
|
||||
logger.error(f"Ошибка компиляции regex {pattern}: {e}")
|
||||
logger.error(f"Regex compilation error {pattern}: {e}")
|
||||
|
||||
def match(self, path: str) -> Optional[RouteMatch]:
|
||||
if path in self.exact_routes:
|
||||
@@ -77,7 +77,7 @@ class RequestHandler:
|
||||
try:
|
||||
return await self._process_route(request, route_match)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки запроса {path}: {e}")
|
||||
logger.error(f"Request processing error {path}: {e}")
|
||||
return PlainTextResponse("500 Internal Server Error", status_code=500)
|
||||
|
||||
async def _process_route(self, request: Request, route_match: RouteMatch) -> Response:
|
||||
@@ -169,7 +169,7 @@ class RequestHandler:
|
||||
for key, value in params.items():
|
||||
proxy_url = proxy_url.replace(f"{{{key}}}", value)
|
||||
|
||||
logger.info(f"Проксирование запроса на: {proxy_url}")
|
||||
logger.info(f"Proxying request to: {proxy_url}")
|
||||
|
||||
return PlainTextResponse(f"Proxy to: {proxy_url}", status_code=200)
|
||||
|
||||
|
||||
+21
-25
@@ -89,8 +89,8 @@ class PyServeServer:
|
||||
ext_metrics = getattr(extension, 'get_metrics')()
|
||||
metrics.update(ext_metrics)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения метрик от {type(extension).__name__}: {e}")
|
||||
|
||||
logger.error(f"Error getting metrics from {type(extension).__name__}: {e}")
|
||||
|
||||
import json
|
||||
return Response(
|
||||
json.dumps(metrics, ensure_ascii=False, indent=2),
|
||||
@@ -105,11 +105,11 @@ class PyServeServer:
|
||||
return None
|
||||
|
||||
if not Path(self.config.ssl.cert_file).exists():
|
||||
logger.error(f"SSL сертификат не найден: {self.config.ssl.cert_file}")
|
||||
logger.error(f"SSL certificate not found: {self.config.ssl.cert_file}")
|
||||
return None
|
||||
|
||||
if not Path(self.config.ssl.key_file).exists():
|
||||
logger.error(f"SSL ключ не найден: {self.config.ssl.key_file}")
|
||||
logger.error(f"SSL key not found: {self.config.ssl.key_file}")
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -118,22 +118,18 @@ class PyServeServer:
|
||||
self.config.ssl.cert_file,
|
||||
self.config.ssl.key_file
|
||||
)
|
||||
logger.info("SSL контекст создан успешно")
|
||||
logger.info("SSL context created successfully")
|
||||
return context
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания SSL контекста: {e}")
|
||||
logger.error(f"Error creating SSL context: {e}")
|
||||
return None
|
||||
|
||||
def run(self) -> None:
|
||||
"""Запуск сервера"""
|
||||
if not self.config.validate():
|
||||
logger.error("Конфигурация невалидна, сервер не может быть запущен")
|
||||
logger.error("Configuration is invalid, server cannot be started")
|
||||
return
|
||||
|
||||
# Создаем директории если их нет
|
||||
self._ensure_directories()
|
||||
|
||||
# SSL конфигурация
|
||||
ssl_context = self._create_ssl_context()
|
||||
|
||||
uvicorn_config = {
|
||||
@@ -154,21 +150,21 @@ class PyServeServer:
|
||||
protocol = "https"
|
||||
else:
|
||||
protocol = "http"
|
||||
|
||||
logger.info(f"Запуск PyServe сервера на {protocol}://{self.config.server.host}:{self.config.server.port}")
|
||||
|
||||
|
||||
logger.info(f"Starting PyServe server at {protocol}://{self.config.server.host}:{self.config.server.port}")
|
||||
|
||||
try:
|
||||
uvicorn.run(**uvicorn_config)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Получен сигнал остановки")
|
||||
logger.info("Received shutdown signal")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запуска сервера: {e}")
|
||||
logger.error(f"Error starting server: {e}")
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
async def run_async(self) -> None:
|
||||
if not self.config.validate():
|
||||
logger.error("Конфигурация невалидна, сервер не может быть запущен")
|
||||
logger.error("Configuration is invalid, server cannot be started")
|
||||
return
|
||||
|
||||
self._ensure_directories()
|
||||
@@ -201,17 +197,17 @@ class PyServeServer:
|
||||
|
||||
for directory in directories:
|
||||
Path(directory).mkdir(parents=True, exist_ok=True)
|
||||
logger.debug(f"Создана/проверена директория: {directory}")
|
||||
|
||||
logger.debug(f"Created/checked directory: {directory}")
|
||||
|
||||
def shutdown(self) -> None:
|
||||
logger.info("Завершение работы PyServe сервера")
|
||||
logger.info("Shutting down PyServe server")
|
||||
self.extension_manager.cleanup()
|
||||
|
||||
from .logging_utils import shutdown_logging
|
||||
shutdown_logging()
|
||||
|
||||
logger.info("Сервер остановлен")
|
||||
|
||||
|
||||
logger.info("Server stopped")
|
||||
|
||||
def add_extension(self, extension_type: str, config: Dict[str, Any]) -> None:
|
||||
self.extension_manager.load_extension(extension_type, config)
|
||||
|
||||
@@ -224,8 +220,8 @@ class PyServeServer:
|
||||
ext_metrics = getattr(extension, 'get_metrics')()
|
||||
metrics.update(ext_metrics)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения метрик от {type(extension).__name__}: {e}")
|
||||
|
||||
logger.error(f"Error getting metrics from {type(extension).__name__}: {e}")
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user