initial commit
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
import ssl
|
||||
import uvicorn
|
||||
import time
|
||||
from starlette.applications import Starlette
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response, PlainTextResponse
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.routing import Route
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from .config import Config
|
||||
from .extensions import ExtensionManager
|
||||
from .logging_utils import get_logger
|
||||
from . import __version__
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PyServeMiddleware(BaseHTTPMiddleware):
|
||||
def __init__(self, app, extension_manager: ExtensionManager):
|
||||
super().__init__(app)
|
||||
self.extension_manager = extension_manager
|
||||
self.access_logger = get_logger('pyserve.access')
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await self.extension_manager.process_request(request)
|
||||
|
||||
if response is None:
|
||||
response = await call_next(request)
|
||||
|
||||
response = await self.extension_manager.process_response(request, response)
|
||||
|
||||
response.headers["Server"] = f"pyserve/{__version__}"
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
method = request.method
|
||||
path = str(request.url.path)
|
||||
query = str(request.url.query) if request.url.query else ""
|
||||
if query:
|
||||
path += f"?{query}"
|
||||
status_code = response.status_code
|
||||
process_time = round((time.time() - start_time) * 1000, 2)
|
||||
|
||||
self.access_logger.info(f"{client_ip} - {method} {path} - {status_code} - {process_time}ms")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class PyServeServer:
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.extension_manager = ExtensionManager()
|
||||
self.app: Optional[Starlette] = None
|
||||
self._setup_logging()
|
||||
self._load_extensions()
|
||||
self._create_app()
|
||||
|
||||
def _setup_logging(self) -> None:
|
||||
self.config.setup_logging()
|
||||
logger.info("PyServe сервер инициализирован")
|
||||
|
||||
def _load_extensions(self) -> None:
|
||||
for ext_config in self.config.extensions:
|
||||
self.extension_manager.load_extension(
|
||||
ext_config.type,
|
||||
ext_config.config
|
||||
)
|
||||
|
||||
def _create_app(self) -> None:
|
||||
routes = [
|
||||
Route("/health", self._health_check, methods=["GET"]),
|
||||
Route("/metrics", self._metrics, methods=["GET"]),
|
||||
Route("/{path:path}", self._catch_all, methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]),
|
||||
]
|
||||
|
||||
self.app = Starlette(routes=routes)
|
||||
self.app.add_middleware(PyServeMiddleware, extension_manager=self.extension_manager)
|
||||
|
||||
async def _health_check(self, request: Request) -> Response:
|
||||
return PlainTextResponse("OK", status_code=200)
|
||||
|
||||
async def _metrics(self, request: Request) -> Response:
|
||||
metrics = {}
|
||||
|
||||
for extension in self.extension_manager.extensions:
|
||||
if hasattr(extension, 'get_metrics'):
|
||||
try:
|
||||
ext_metrics = getattr(extension, 'get_metrics')()
|
||||
metrics.update(ext_metrics)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения метрик от {type(extension).__name__}: {e}")
|
||||
|
||||
import json
|
||||
return Response(
|
||||
json.dumps(metrics, ensure_ascii=False, indent=2),
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
async def _catch_all(self, request: Request) -> Response:
|
||||
return PlainTextResponse("404 Not Found", status_code=404)
|
||||
|
||||
def _create_ssl_context(self) -> Optional[ssl.SSLContext]:
|
||||
if not self.config.ssl.enabled:
|
||||
return None
|
||||
|
||||
if not Path(self.config.ssl.cert_file).exists():
|
||||
logger.error(f"SSL сертификат не найден: {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}")
|
||||
return None
|
||||
|
||||
try:
|
||||
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(
|
||||
self.config.ssl.cert_file,
|
||||
self.config.ssl.key_file
|
||||
)
|
||||
logger.info("SSL контекст создан успешно")
|
||||
return context
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания SSL контекста: {e}")
|
||||
return None
|
||||
|
||||
def run(self) -> None:
|
||||
"""Запуск сервера"""
|
||||
if not self.config.validate():
|
||||
logger.error("Конфигурация невалидна, сервер не может быть запущен")
|
||||
return
|
||||
|
||||
# Создаем директории если их нет
|
||||
self._ensure_directories()
|
||||
|
||||
# SSL конфигурация
|
||||
ssl_context = self._create_ssl_context()
|
||||
|
||||
uvicorn_config = {
|
||||
"app": self.app,
|
||||
"host": self.config.server.host,
|
||||
"port": self.config.server.port,
|
||||
"log_level": "critical",
|
||||
"access_log": False,
|
||||
"use_colors": False,
|
||||
"server_header": False,
|
||||
}
|
||||
|
||||
if ssl_context:
|
||||
uvicorn_config.update({
|
||||
"ssl_keyfile": self.config.ssl.key_file,
|
||||
"ssl_certfile": self.config.ssl.cert_file,
|
||||
})
|
||||
protocol = "https"
|
||||
else:
|
||||
protocol = "http"
|
||||
|
||||
logger.info(f"Запуск PyServe сервера на {protocol}://{self.config.server.host}:{self.config.server.port}")
|
||||
|
||||
try:
|
||||
uvicorn.run(**uvicorn_config)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Получен сигнал остановки")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запуска сервера: {e}")
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
async def run_async(self) -> None:
|
||||
if not self.config.validate():
|
||||
logger.error("Конфигурация невалидна, сервер не может быть запущен")
|
||||
return
|
||||
|
||||
self._ensure_directories()
|
||||
|
||||
config = uvicorn.Config(
|
||||
app=self.app, # type: ignore
|
||||
host=self.config.server.host,
|
||||
port=self.config.server.port,
|
||||
log_level="critical",
|
||||
access_log=False,
|
||||
use_colors=False,
|
||||
)
|
||||
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
try:
|
||||
await server.serve()
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
def _ensure_directories(self) -> None:
|
||||
directories = [
|
||||
self.config.http.static_dir,
|
||||
self.config.http.templates_dir,
|
||||
]
|
||||
|
||||
log_dir = Path(self.config.logging.log_file).parent
|
||||
if log_dir != Path("."):
|
||||
directories.append(str(log_dir))
|
||||
|
||||
for directory in directories:
|
||||
Path(directory).mkdir(parents=True, exist_ok=True)
|
||||
logger.debug(f"Создана/проверена директория: {directory}")
|
||||
|
||||
def shutdown(self) -> None:
|
||||
logger.info("Завершение работы PyServe сервера")
|
||||
self.extension_manager.cleanup()
|
||||
|
||||
from .logging_utils import shutdown_logging
|
||||
shutdown_logging()
|
||||
|
||||
logger.info("Сервер остановлен")
|
||||
|
||||
def add_extension(self, extension_type: str, config: Dict[str, Any]) -> None:
|
||||
self.extension_manager.load_extension(extension_type, config)
|
||||
|
||||
def get_metrics(self) -> Dict[str, Any]:
|
||||
metrics = {"server_status": "running"}
|
||||
|
||||
for extension in self.extension_manager.extensions:
|
||||
if hasattr(extension, 'get_metrics'):
|
||||
try:
|
||||
ext_metrics = getattr(extension, 'get_metrics')()
|
||||
metrics.update(ext_metrics)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения метрик от {type(extension).__name__}: {e}")
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def create_server(config_path: str = "config.yaml") -> PyServeServer:
|
||||
config = Config.from_yaml(config_path)
|
||||
return PyServeServer(config)
|
||||
|
||||
|
||||
def run_server(config_path: str = "config.yaml") -> None:
|
||||
server = create_server(config_path)
|
||||
server.run()
|
||||
Reference in New Issue
Block a user