From a3af016e6789b7c9beb400ff522ccf782459ed38 Mon Sep 17 00:00:00 2001 From: Yevgeny Kulikov <86586195+justgithubaccount@users.noreply.github.com> Date: Fri, 30 Oct 2026 07:13:42 +0000 Subject: [PATCH] chore(no-release): clean configuration comments --- app/api.py | 3 --- app/core/db.py | 7 +++--- app/core/llm_client.py | 36 ++-------------------------- app/integrations/behavior_manager.py | 3 +-- app/logger.py | 6 ----- app/main.py | 7 +----- app/models/chat.py | 1 - app/observability/tracing.py | 16 ++----------- app/services/chat_service.py | 4 ++-- 9 files changed, 11 insertions(+), 72 deletions(-) diff --git a/app/api.py b/app/api.py index dd2418f..2051c48 100644 --- a/app/api.py +++ b/app/api.py @@ -28,7 +28,6 @@ tags=["chat"] ) -# In-memory fallback (not used when database is configured) memory = ProjectMemory() meter = metrics.get_meter(__name__) chat_counter = meter.create_counter("chat_requests_total") @@ -75,7 +74,6 @@ async def chat_endpoint( except Exception as e: log.bind(event="chat_api_error", error=str(e)).error("Standard chat request failed") traceback.print_exc() - # raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="...") raise HTTPException(status_code=500, detail="AI сервис недоступен") @api_router.post("/projects", response_model=ProjectInfo) @@ -126,7 +124,6 @@ async def chat_in_project( span_id = format(ctx.span_id, "016x") try: - # Ensure project exists project = session.get(Project, project_id) if not project: raise ValueError(f"Проект {project_id} не найден") diff --git a/app/core/db.py b/app/core/db.py index 6ba1737..4374266 100644 --- a/app/core/db.py +++ b/app/core/db.py @@ -4,14 +4,13 @@ DATABASE_URL = get_settings().database_url -# Создаем engine с timeout настройками engine = create_engine( DATABASE_URL, echo=True, - pool_timeout=10, # Timeout для получения connection из pool - pool_recycle=100, # Переподключение каждый час + pool_timeout=10, # Wait at most 10 seconds for a pooled connection. + pool_recycle=100, # Recycle pooled connections after 100 seconds. connect_args={ - "connect_timeout": 10, # Timeout подключения к PostgreSQL + "connect_timeout": 10, # Bound the initial PostgreSQL connection. "application_name": "chat-api" } if DATABASE_URL.startswith("postgresql") else {} ) diff --git a/app/core/llm_client.py b/app/core/llm_client.py index 3483cc0..19b06ff 100644 --- a/app/core/llm_client.py +++ b/app/core/llm_client.py @@ -7,14 +7,11 @@ from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor from opentelemetry.trace import Status, StatusCode -# Инструментируем httpx для автоматического трейсинга HTTP запросов HTTPXClientInstrumentor().instrument() -# Получаем tracer и meter tracer = trace.get_tracer(__name__) meter = metrics.get_meter(__name__) -# Метрики llm_request_counter = meter.create_counter( name="llm_requests_total", description="Total number of LLM requests", @@ -39,10 +36,8 @@ class LLMProviderError(RuntimeError): class OpenRouterClient: def __init__(self): - # Из секрета self.api_key = os.getenv("OPENROUTER_API_KEY") - # Из конфига self.api_url = os.getenv("OPENROUTER_API_URL", "https://openrouter.ai/api/v1/chat/completions") self.default_model = os.getenv("OPENROUTER_MODEL", "anthropic/claude-opus-4") self.http_referer = os.getenv("OPENROUTER_HTTP_REFERER", "") @@ -59,7 +54,6 @@ async def chat_completion( """ model_name = model or self.default_model - # Создаем span для всей операции with tracer.start_as_current_span( "openrouter_chat_completion", attributes={ @@ -71,13 +65,11 @@ async def chat_completion( start_time = time.time() try: - # Подготовка заголовков headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } - # Опциональные заголовки для OpenRouter if self.http_referer: headers["HTTP-Referer"] = self.http_referer span.set_attribute("llm.http_referer", self.http_referer) @@ -85,18 +77,15 @@ async def chat_completion( headers["X-Title"] = self.x_title span.set_attribute("llm.x_title", self.x_title) - # Подготовка запроса request_body = { "model": model_name, "messages": messages, **kwargs } - # Логируем размер запроса request_size = len(str(messages)) span.set_attribute("llm.request_size", request_size) - # Создаем отдельный span для HTTP запроса with tracer.start_as_current_span("http_request"): async with httpx.AsyncClient() as client: response = await client.post( @@ -108,23 +97,19 @@ async def chat_completion( response.raise_for_status() result = response.json() - # Обработка успешного ответа duration = time.time() - start_time - # Извлекаем метрики из ответа usage = result.get("usage", {}) prompt_tokens = usage.get("prompt_tokens", 0) completion_tokens = usage.get("completion_tokens", 0) total_tokens = usage.get("total_tokens", 0) - # Устанавливаем атрибуты span span.set_attribute("llm.prompt_tokens", prompt_tokens) span.set_attribute("llm.completion_tokens", completion_tokens) span.set_attribute("llm.total_tokens", total_tokens) span.set_attribute("llm.duration_seconds", duration) span.set_attribute("llm.response_id", result.get("id", "")) - # Записываем метрики labels = {"model": model_name, "status": "success"} llm_request_counter.add(1, labels) llm_request_duration.record(duration, labels) @@ -136,7 +121,6 @@ async def chat_completion( return result except httpx.HTTPStatusError as e: - # Обработка HTTP ошибок duration = time.time() - start_time span.set_attribute("http.status_code", e.response.status_code) @@ -146,39 +130,33 @@ async def chat_completion( Status(StatusCode.ERROR, f"HTTP {e.response.status_code}: {str(e)}") ) - # Метрики для ошибок labels = {"model": model_name, "status": "error", "error_type": "http"} llm_request_counter.add(1, labels) llm_request_duration.record(duration, labels) - # Добавляем событие в span span.add_event( "HTTP Error", attributes={ "status_code": e.response.status_code, - "response_body": e.response.text[:500] # Первые 500 символов + "response_body": e.response.text[:500] # Bound provider data recorded in telemetry. } ) - # Возвращаем error response вместо raise return { "error": f"HTTP {e.response.status_code}: {str(e)}", "choices": [{"message": {"content": f"HTTP ошибка: {e.response.status_code}"}}] } except Exception as e: - # Обработка других ошибок duration = time.time() - start_time span.set_attribute("error.type", type(e).__name__) span.set_attribute("error.message", str(e)) span.set_status(Status(StatusCode.ERROR, str(e))) - # Метрики для ошибок labels = {"model": model_name, "status": "error", "error_type": "exception"} llm_request_counter.add(1, labels) llm_request_duration.record(duration, labels) - # Добавляем событие в span span.add_event( "Exception occurred", attributes={ @@ -186,7 +164,6 @@ async def chat_completion( "exception.message": str(e) } ) - # Возвращаем error response вместо raise return { "error": str(e), "choices": [{"message": {"content": f"Ошибка: {str(e)}"}}] @@ -194,7 +171,7 @@ async def chat_completion( async def generate_reply( self, - messages: List, # List[Message] из schemas + messages: List, user_api_key: Optional[str] = None, project_id: Optional[str] = None, trace_id: Optional[str] = None, @@ -203,7 +180,6 @@ async def generate_reply( Метод для совместимости с ChatService. Преобразует список Message объектов в формат OpenRouter. """ - # Добавляем атрибуты в текущий span если есть current_span = trace.get_current_span() if current_span.is_recording(): if project_id: @@ -211,38 +187,31 @@ async def generate_reply( if trace_id: current_span.set_attribute("external.trace_id", trace_id) - # Преобразуем Message объекты в dict для OpenRouter openrouter_messages = [] for msg in messages: if hasattr(msg, 'role') and hasattr(msg, 'content'): - # Если это объект Message openrouter_messages.append({ "role": msg.role, "content": msg.content }) elif isinstance(msg, dict): - # Если уже dict openrouter_messages.append(msg) else: - # Fallback openrouter_messages.append({ "role": "user", "content": str(msg) }) - # Используем переданный API ключ если есть original_key = self.api_key if user_api_key: self.api_key = user_api_key try: - # Вызываем основной метод response = await self.chat_completion( messages=openrouter_messages, model=self.default_model ) - # Проверяем на ошибки if "error" in response: raise LLMProviderError(str(response["error"])) @@ -260,7 +229,6 @@ async def generate_reply( return content finally: - # Восстанавливаем оригинальный ключ if user_api_key: self.api_key = original_key diff --git a/app/integrations/behavior_manager.py b/app/integrations/behavior_manager.py index add20e5..c180ca0 100644 --- a/app/integrations/behavior_manager.py +++ b/app/integrations/behavior_manager.py @@ -21,7 +21,7 @@ async def refresh(self) -> None: log.info("Behavior page retrieved") try: - # Expect first child to be a code block with YAML + # Use the first YAML code block returned by Notion. for block in data.get("results", []): if block.get("type") == "code": text = block["code"].get("rich_text", []) @@ -29,7 +29,6 @@ async def refresh(self) -> None: raw: Dict[str, Any] = yaml.safe_load(content) or {} self.behavior = BehaviorDefinition.model_validate(raw) - # Правильная типизация для логирования behavior_dict = self.behavior.model_dump() log.bind(event="behavior_parsed").debug(f"Parsed behavior: {behavior_dict}") log.bind(event="behavior_loaded").info("Behavior updated") diff --git a/app/logger.py b/app/logger.py index 752e263..b763e92 100644 --- a/app/logger.py +++ b/app/logger.py @@ -4,7 +4,6 @@ import structlog from opentelemetry import trace -# Context variables для хранения дополнительных атрибутов log_context: ContextVar[Dict[str, Any]] = ContextVar('log_context', default={}) class EnrichedLogger: @@ -23,14 +22,12 @@ def _add_trace_context(self, event_dict: Dict[str, Any]) -> Dict[str, Any]: event_dict['trace_id'] = format(span_context.trace_id, '032x') event_dict['span_id'] = format(span_context.span_id, '016x') - # Добавляем контекстные переменные event_dict.update(log_context.get()) return event_dict def bind(self, **kwargs) -> 'EnrichedLogger': """Добавляет контекст к логгеру.""" - # Приводим к нужному типу для MyPy bound_logger = cast(structlog.BoundLogger, self.logger.bind(**kwargs)) return EnrichedLogger(bound_logger) @@ -50,7 +47,6 @@ def debug(self, msg: str, **kwargs) -> None: kwargs = self._add_trace_context(kwargs) self.logger.debug(msg, **kwargs) -# Инициализация structlog structlog.configure( processors=[ structlog.stdlib.filter_by_level, @@ -68,7 +64,6 @@ def debug(self, msg: str, **kwargs) -> None: cache_logger_on_first_use=True, ) -# Базовый логгер base_logger = structlog.get_logger() def enrich_context(**kwargs) -> EnrichedLogger: @@ -76,7 +71,6 @@ def enrich_context(**kwargs) -> EnrichedLogger: Создает логгер с обогащенным контекстом. Совместимо с существующим кодом. """ - # Приводим к нужному типу для MyPy bound_logger = cast(structlog.BoundLogger, base_logger.bind(**kwargs)) return EnrichedLogger(bound_logger) diff --git a/app/main.py b/app/main.py index 861adbb..be5b923 100644 --- a/app/main.py +++ b/app/main.py @@ -15,7 +15,6 @@ @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Lifespan event handler for startup and shutdown.""" - # Startup: init database enrich_context(event="startup_init_db_start").info("Starting database initialization") try: init_db() @@ -26,9 +25,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: enrich_context(event="startup").info("Application initialized") - yield # Application runs here + yield - # Shutdown enrich_context(event="shutdown").info("Application shutting down") @@ -43,7 +41,6 @@ def create_app() -> FastAPI: enrich_context(event="middleware_setup_start").info("Setting up middleware") - # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -54,13 +51,11 @@ def create_app() -> FastAPI: enrich_context(event="routes_setup_start").info("Setting up routes") - # Routes app.include_router(api_router) app.include_router(health_router) enrich_context(event="tracing_setup_start").info("Setting up tracing") - # Tracing setup_tracing(app) @app.get("/") diff --git a/app/models/chat.py b/app/models/chat.py index 515aefe..820de55 100644 --- a/app/models/chat.py +++ b/app/models/chat.py @@ -4,7 +4,6 @@ from pydantic import BaseModel, ConfigDict, Field -# Импортируем Role из schemas, избегаем дублирования from ..schemas.chat import Role diff --git a/app/observability/tracing.py b/app/observability/tracing.py index c110fc5..75dfe96 100644 --- a/app/observability/tracing.py +++ b/app/observability/tracing.py @@ -3,8 +3,6 @@ from typing import Any, Dict from opentelemetry import trace - -# Правильный импорт для логов from opentelemetry._logs import set_logger_provider from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter @@ -46,26 +44,22 @@ def setup_tracing(app) -> None: logging.getLogger(__name__).info("OpenTelemetry export disabled") return - # Resource с метаданными сервиса и Kubernetes resource = Resource(attributes={ "service.name": os.getenv("OTEL_SERVICE_NAME", "chat-api"), "service.version": os.getenv("APP_VERSION", "0.1.0"), "deployment.environment": os.getenv("ENVIRONMENT", "production"), - # Kubernetes metadata - обязательные атрибуты для observability "k8s.pod.name": os.getenv("K8S_POD_NAME", "unknown"), "k8s.namespace": os.getenv("K8S_NAMESPACE", "chat"), "k8s.node.name": os.getenv("K8S_NODE_NAME", "unknown"), "k8s.deployment.name": os.getenv("K8S_DEPLOYMENT_NAME", "chat-api"), - # Дополнительные метаданные "k8s.container.name": os.getenv("K8S_CONTAINER_NAME", "chat-api"), "k8s.pod.uid": os.getenv("K8S_POD_UID", "unknown"), }) - # Для HTTP протокола добавляем правильные пути + # OTLP/HTTP exporters require a signal-specific path. traces_endpoint = f"{otlp_endpoint}/v1/traces" if not otlp_endpoint.endswith("/v1/traces") else otlp_endpoint logs_endpoint = f"{otlp_endpoint}/v1/logs" if not otlp_endpoint.endswith("/v1/logs") else otlp_endpoint - # === TRACES === provider = None if traces_enabled: provider = TracerProvider(resource=resource) @@ -86,7 +80,6 @@ def setup_tracing(app) -> None: provider.add_span_processor(trace_processor) trace.set_tracer_provider(provider) - # === LOGS === root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) if logs_enabled: @@ -109,7 +102,6 @@ def setup_tracing(app) -> None: set_logger_provider(log_provider) root_logger.addHandler(LoggingHandler(level=logging.INFO, logger_provider=log_provider)) - # JSON logging для production if os.getenv("ENVIRONMENT") == "production": import json @@ -123,11 +115,10 @@ def format(self, record: logging.LogRecord) -> str: "pathname": record.pathname, "lineno": record.lineno, } - # Добавляем trace context + # Correlate JSON log records with the active span. span = trace.get_current_span() if span.is_recording(): ctx = span.get_span_context() - # Приводим к нужным типам для MyPy trace_id: int = ctx.trace_id span_id: int = ctx.span_id log_obj["trace_id"] = format(trace_id, '032x') @@ -138,14 +129,12 @@ def format(self, record: logging.LogRecord) -> str: console_handler.setFormatter(JSONFormatter()) root_logger.addHandler(console_handler) else: - # Human-readable для dev console_handler = logging.StreamHandler() console_handler.setFormatter( logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') ) root_logger.addHandler(console_handler) - # FastAPI instrumentation if provider is not None: FastAPIInstrumentor().instrument_app( app, @@ -153,7 +142,6 @@ def format(self, record: logging.LogRecord) -> str: server_request_hook=server_request_hook, ) - # Log успешной инициализации logger = logging.getLogger(__name__) logger.info( "OpenTelemetry initialized", diff --git a/app/services/chat_service.py b/app/services/chat_service.py index a6bb587..26a3180 100644 --- a/app/services/chat_service.py +++ b/app/services/chat_service.py @@ -1,7 +1,7 @@ import time from typing import List, Optional -from app.logger import enrich_context # логгер с контекстом +from app.logger import enrich_context from ..core.llm_client import OpenRouterClient from ..schemas import Message @@ -32,7 +32,7 @@ async def get_ai_reply( user_message=user_message, model=model, trace_id=trace_id, - job="chat" # 👈 добавлено: метка job для Vector → Loki + job="chat" ) log.bind(event="llm_request").info("LLM request initiated")