diff --git a/Makefile b/Makefile index 1eca972..bdb40f6 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,55 @@ PACKAGE_NAME = pyserve GREEN = \033[0;32m YELLOW = \033[1;33m RED = \033[0;31m +CYAN = \033[0;36m NC = \033[0m help: - @echo "$(GREEN)Commands:$(NC)" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-20s$(NC) %s\n", $$1, $$2}' + @echo "$(GREEN)╔══════════════════════════════════════════════════════════════════════════════╗$(NC)" + @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: @echo "$(GREEN)Installing dependencies...$(NC)" @@ -118,7 +162,7 @@ config-create: echo "$(YELLOW)config.yaml already exists$(NC)"; \ fi -logs: +watch-logs: @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 diff --git a/config.yaml b/config.yaml index 2eb043d..6a52150 100644 --- a/config.yaml +++ b/config.yaml @@ -18,29 +18,73 @@ ssl: logging: level: DEBUG 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: - # Встроенное расширение для продвинутой маршрутизации + # Built-in extension for advanced routing - type: routing config: regex_locations: - # API маршруты с захватом версии + # API routes with version capture "~^/api/v(?P\\d+)/": proxy_pass: "http://localhost:9001" headers: - "API-Version: {version}" - "X-Forwarded-For: $remote_addr" - - # Статические файлы с долгим кэшем + + # Static files with long cache "~*\\.(js|css|png|jpg|gif|ico|svg|woff2?)$": root: "./static" cache_control: "public, max-age=31536000" headers: - "Access-Control-Allow-Origin: *" - - # Exact match для health check + + # Exact match for health check "=/health": return: "200 OK" content_type: "text/plain" @@ -48,13 +92,13 @@ extensions: "=/": root: "./static" index_file: "index.html" - - # SPA fallback для всех остальных маршрутов + + # SPA fallback for all other routes "__default__": spa_fallback: true root: "./static" index_file: "docs.html" - # Исключения для SPA (не попадают в fallback) + # Exceptions for SPA (do not hit fallback) exclude_patterns: - "/api/" - "/admin/" diff --git a/examples/config.example.logging.yaml b/examples/config.example.logging.yaml new file mode 100644 index 0000000..bbdaf07 --- /dev/null +++ b/examples/config.example.logging.yaml @@ -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 diff --git a/pyserve/__init__.py b/pyserve/__init__.py index e7c9002..2b56124 100644 --- a/pyserve/__init__.py +++ b/pyserve/__init__.py @@ -1,9 +1,9 @@ """ -PyServe - HTTP веб-сервер с функционалом nginx +PyServe - HTTP web server written on Python """ __version__ = "0.6.0" -__author__ = "Илья Глазунов" +__author__ = "Ilya Glazunov" from .server import PyServeServer from .config import Config diff --git a/pyserve/cli.py b/pyserve/cli.py index 5d5e033..9225e65 100644 --- a/pyserve/cli.py +++ b/pyserve/cli.py @@ -8,7 +8,7 @@ from . import PyServeServer, Config, __version__ def main() -> None: parser = argparse.ArgumentParser( description="PyServe - HTTP web server", - prog="pyserve" + prog="pyserve", ) parser.add_argument( "-c", "--config", diff --git a/pyserve/config.py b/pyserve/config.py index dc2284f..239137f 100644 --- a/pyserve/config.py +++ b/pyserve/config.py @@ -1,6 +1,6 @@ import yaml import os -from typing import Dict, Any, List +from typing import Dict, Any, List, cast from dataclasses import dataclass, field import logging from .logging_utils import setup_logging @@ -28,11 +28,37 @@ class SSLConfig: 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 class LoggingConfig: level: str = "INFO" 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 @@ -99,10 +125,85 @@ class Config: if 'logging' in data: 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( - level=log_data.get('level', config.logging.level), - console_output=log_data.get('console_output', config.logging.console_output), - log_file=log_data.get('log_file', config.logging.log_file) + level=log_data.get('level', 'INFO'), + console_output=log_data.get('console_output', True), + format=global_format, + console=console_config, + files=files_config ) if 'extensions' in data: @@ -130,12 +231,34 @@ class Config: if not (1 <= self.server.port <= 65535): 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"Unable to create log directory: {e}") + 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): + try: + os.makedirs(log_dir, exist_ok=True) + except OSError as e: + errors.append(f"Unable to create log directory for {file_config.path}: {e}") if errors: for error in errors: @@ -148,6 +271,38 @@ class Config: config_dict = { 'level': self.logging.level, '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) diff --git a/pyserve/extensions.py b/pyserve/extensions.py index 2e37c40..c2ade69 100644 --- a/pyserve/extensions.py +++ b/pyserve/extensions.py @@ -152,7 +152,7 @@ class ExtensionManager: 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}") + logger.error(f"Unknown extension type: {extension_type}") return try: @@ -160,9 +160,9 @@ class ExtensionManager: extension = extension_class(config) extension.initialize() self.extensions.append(extension) - logger.info(f"Загружено расширение: {extension_type}") + logger.info(f"Loaded extension: {extension_type}") 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]: for extension in self.extensions: @@ -174,7 +174,7 @@ class ExtensionManager: if response is not None: return response except Exception as e: - logger.error(f"Ошибка в расширении {type(extension).__name__}: {e}") + logger.error(f"Error in extension {type(extension).__name__}: {e}") return None @@ -186,7 +186,7 @@ class ExtensionManager: try: response = await extension.process_response(request, response) except Exception as e: - logger.error(f"Ошибка в расширении {type(extension).__name__}: {e}") + logger.error(f"Error in extension {type(extension).__name__}: {e}") return response @@ -195,6 +195,6 @@ class ExtensionManager: try: extension.cleanup() 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() diff --git a/pyserve/logging_utils.py b/pyserve/logging_utils.py index eb7869e..02575b7 100644 --- a/pyserve/logging_utils.py +++ b/pyserve/logging_utils.py @@ -2,12 +2,30 @@ import logging import logging.handlers import sys import time +import json from pathlib import Path from typing import Dict, Any, List 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): def filter(self, record: logging.LogRecord) -> bool: if hasattr(record, 'name') and 'uvicorn.access' in record.name: @@ -36,10 +54,12 @@ class PyServeFormatter(logging.Formatter): '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) self.use_colors = use_colors and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() self.show_module = show_module + self.timestamp_format = timestamp_format def format(self, record: logging.LogRecord) -> str: if self.use_colors: @@ -59,6 +79,37 @@ class PyServeFormatter(logging.Formatter): 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): def __init__(self, logger_name: str = 'pyserve.access'): super().__init__() @@ -75,68 +126,154 @@ class PyServeLogManager: self.loggers: Dict[str, logging.Logger] = {} 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: if self.configured: return - level = config.get('level', 'INFO').upper() + if 'format' not in config and 'console' not in config and 'files' not in config: + level = config.get('level', 'INFO').upper() + console_output = config.get('console_output', True) + 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) - log_file = config.get('log_file', './logs/pyserve.log') + + 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._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.setLevel(getattr(logging, console_level)) + + if console_format.get('type') == 'json': + console_formatter = self._create_formatter(console_format) + else: + console_formatter = PyServeFormatter( + use_colors=console_format.get('use_colors', 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' + ) + 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) + for i, file_config in enumerate(files_config): + 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( - log_file, - maxBytes=10 * 1024 * 1024, # 10MB - backupCount=5, + file_path, + maxBytes=max_bytes, + backupCount=backup_count, encoding='utf-8' ) - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(detailed_formatter) + file_handler.setLevel(getattr(logging, file_level)) + + 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()) + if file_loggers: + logger_filter = LoggerFilter(file_loggers) + file_handler.addFilter(logger_filter) root_logger.addHandler(file_handler) - self.handlers['file'] = file_handler - - self._configure_library_loggers(level) + self.handlers[f'file_{i}'] = file_handler + self._configure_library_loggers(main_level) self._intercept_uvicorn_logging() pyserve_logger = logging.getLogger('pyserve') - pyserve_logger.setLevel(getattr(logging, level)) + pyserve_logger.setLevel(getattr(logging, main_level)) self.loggers['pyserve'] = pyserve_logger 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"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 @@ -168,13 +305,13 @@ class PyServeLogManager: def _configure_library_loggers(self, main_level: str) -> None: library_configs = { - # Uvicorn и связанные - только в DEBUG режиме + # Uvicorn and related - only in DEBUG mode 'uvicorn': '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 - only in DEBUG mode 'starlette': 'DEBUG' if main_level == 'DEBUG' else 'WARNING', 'asyncio': 'WARNING', diff --git a/pyserve/routing.py b/pyserve/routing.py index af222b2..3eadb41 100644 --- a/pyserve/routing.py +++ b/pyserve/routing.py @@ -166,7 +166,7 @@ class RequestHandler: async def _handle_proxy(self, request: Request, config: Dict[str, Any], params: Dict[str, str]) -> Response: - # TODO: Реализовать полноценное проксирование + # TODO: implement real proxying proxy_url = config["proxy_pass"] for key, value in params.items(): diff --git a/pyserve/server.py b/pyserve/server.py index f31432f..724b8a4 100644 --- a/pyserve/server.py +++ b/pyserve/server.py @@ -208,9 +208,10 @@ class PyServeServer: self.config.http.templates_dir, ] - log_dir = Path(self.config.logging.log_file).parent - if log_dir != Path("."): - directories.append(str(log_dir)) + for file_config in self.config.logging.files: + log_dir = Path(file_config.path).parent + if log_dir != Path("."): + directories.append(str(log_dir)) for directory in directories: Path(directory).mkdir(parents=True, exist_ok=True)