From 2f79f7c40a7f97b4d8a4561077293861c11a14fe Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 16 May 2026 13:39:17 +0300 Subject: [PATCH] =?UTF-8?q?feat(backend):=20sentry-sdk=20init=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20FastAPI=20+=20Celery=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20GlitchTip=20(#204=20backend)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Подключает sentry-sdk ^2.18 к FastAPI и Celery-воркеру: - Интеграции: StarletteIntegration, FastApiIntegration, CeleryIntegration, SqlalchemyIntegration, HttpxIntegration, LoggingIntegration - SDK инициализируется до создания FastAPI app (module-level), а также отдельно в celery_app.py для Celery-процесса - Без GLITCHTIP_DSN — SDK no-op, контейнер стартует без ошибок - profiles_sample_rate=0.0 — GlitchTip профили не поддерживает - GIT_SHA из ENV как release-тег; fallback "unknown" --- backend/.env.example | 8 +++-- backend/app/core/config.py | 10 +++--- backend/app/main.py | 43 ++++++++++++++++++---- backend/app/workers/celery_app.py | 29 +++++++++++++++ backend/pyproject.toml | 2 +- backend/tests/test_sentry_init.py | 59 +++++++++++++++++++++++++++++++ backend/uv.lock | 13 +++++-- 7 files changed, 146 insertions(+), 18 deletions(-) create mode 100644 backend/tests/test_sentry_init.py diff --git a/backend/.env.example b/backend/.env.example index f6b3c987..adc92d38 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -6,9 +6,11 @@ REDIS_URL=redis://redis:6379/0 CORS_ORIGINS=["http://localhost:3000"] ENVIRONMENT=dev -# Sentry: create a project at https://sentry.io/, paste the DSN here. -# Empty string = Sentry disabled. main.py initializes only if non-empty. -SENTRY_DSN= +# GlitchTip error tracking (https://errors.gendsgn.ru) +# Формат: https://@errors.gendsgn.ru/ +# Пустая строка = SDK не инициализируется. +GLITCHTIP_DSN= +GLITCHTIP_TRACES_SAMPLE_RATE=0.05 # External APIs (Stage 2) ROSREESTR_PKK_BASE_URL=https://pkk.rosreestr.ru/api/features/1 diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 69f9b1ff..c71418bd 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -7,11 +7,11 @@ class Settings(BaseSettings): database_url: str = "postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign" redis_url: str = "redis://localhost:6379/0" cors_origins: list[str] = ["http://localhost:3000"] - sentry_dsn: str | None = None - # Release tag для Sentry — обычно git short sha, проставляется - # deploy.yml в backend/.env.runtime (см. workflow). Локально оставляем - # пустым — Sentry припишет 'unknown'. - sentry_release: str | None = None + # GlitchTip error tracking (Sentry-compatible self-hosted). + # Формат DSN: https://@errors.gendsgn.ru/ + # Пустая строка / None = SDK не инициализируется (no-op). + glitchtip_dsn: str | None = None + glitchtip_traces_sample_rate: float = 0.05 environment: str = "dev" # External APIs (Stage 2) diff --git a/backend/app/main.py b/backend/app/main.py index 93db3a29..2bd294b5 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,9 +1,17 @@ +import logging +import os from collections.abc import AsyncIterator from contextlib import asynccontextmanager import sentry_sdk from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from sentry_sdk.integrations.celery import CeleryIntegration +from sentry_sdk.integrations.fastapi import FastApiIntegration +from sentry_sdk.integrations.httpx import HttpxIntegration +from sentry_sdk.integrations.logging import LoggingIntegration +from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration +from sentry_sdk.integrations.starlette import StarletteIntegration from app.api.v1 import ( admin_cadastre, @@ -18,16 +26,37 @@ from app.api.v1 import ( ) from app.core.config import settings +logger = logging.getLogger(__name__) + +# Инициализируем SDK до создания FastAPI app, чтобы все инструменты +# (middleware, маршруты) видели активный client с самого старта процесса. +# GlitchTip не поддерживает profiling — profiles_sample_rate=0.0. +if settings.glitchtip_dsn: + sentry_sdk.init( + dsn=settings.glitchtip_dsn, + environment=settings.environment, + release=os.getenv("GIT_SHA", "unknown"), + traces_sample_rate=settings.glitchtip_traces_sample_rate, + profiles_sample_rate=0.0, + send_default_pii=False, + integrations=[ + StarletteIntegration(), + FastApiIntegration(), + CeleryIntegration(monitor_beat_tasks=True), + SqlalchemyIntegration(), + HttpxIntegration(), + LoggingIntegration(level=logging.INFO, event_level=logging.ERROR), + ], + ) + logger.info( + "GlitchTip SDK initialised (env=%s, traces=%.2f)", + settings.environment, + settings.glitchtip_traces_sample_rate, + ) + @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: - if settings.sentry_dsn: - sentry_sdk.init( - dsn=settings.sentry_dsn, - environment=settings.environment, - release=settings.sentry_release, - traces_sample_rate=0.1, - ) yield diff --git a/backend/app/workers/celery_app.py b/backend/app/workers/celery_app.py index 9bf49e43..d39e006b 100644 --- a/backend/app/workers/celery_app.py +++ b/backend/app/workers/celery_app.py @@ -5,13 +5,42 @@ Worker lifecycle hooks (process_init, worker_ready) → app/workers/lifecycle.py """ import logging +import os +import sentry_sdk from celery import Celery +from sentry_sdk.integrations.celery import CeleryIntegration +from sentry_sdk.integrations.httpx import HttpxIntegration +from sentry_sdk.integrations.logging import LoggingIntegration +from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from app.core.config import settings logger = logging.getLogger(__name__) +# SDK инициализируется в обоих процессах (FastAPI-сервер и Celery-воркер), +# чтобы события из тасков попадали в GlitchTip. SDK безопасен для двойного +# вызова — повторный sentry_sdk.init() в одном процессе заменяет клиента. +if settings.glitchtip_dsn: + sentry_sdk.init( + dsn=settings.glitchtip_dsn, + environment=settings.environment, + release=os.getenv("GIT_SHA", "unknown"), + traces_sample_rate=settings.glitchtip_traces_sample_rate, + profiles_sample_rate=0.0, + send_default_pii=False, + integrations=[ + CeleryIntegration(monitor_beat_tasks=True), + SqlalchemyIntegration(), + HttpxIntegration(), + LoggingIntegration(level=logging.INFO, event_level=logging.ERROR), + ], + ) + logger.info( + "GlitchTip SDK initialised in Celery worker (env=%s)", + settings.environment, + ) + celery_app = Celery( "gendesign", broker=settings.redis_url, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index dbbbb3b0..18bce468 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "pandas>=2.2.0", "numpy>=2.0.0", "scikit-learn>=1.5.0", - "sentry-sdk[fastapi]>=2.10.0", + "sentry-sdk[fastapi,celery,sqlalchemy,httpx]>=2.18.0", "rosreestr2coord>=5.0.0", ] diff --git a/backend/tests/test_sentry_init.py b/backend/tests/test_sentry_init.py new file mode 100644 index 00000000..a2858455 --- /dev/null +++ b/backend/tests/test_sentry_init.py @@ -0,0 +1,59 @@ +"""Unit-тесты логики инициализации GlitchTip SDK. + +Проверяем что init-блок в main.py / celery_app.py вызывает sentry_sdk.init() +только при непустом GLITCHTIP_DSN. +""" + +from unittest.mock import patch + +import sentry_sdk + + +def test_sdk_imports_without_error() -> None: + """Все интеграции импортируются без ModuleNotFoundError.""" + from sentry_sdk.integrations.celery import CeleryIntegration + from sentry_sdk.integrations.fastapi import FastApiIntegration + from sentry_sdk.integrations.httpx import HttpxIntegration + from sentry_sdk.integrations.logging import LoggingIntegration + from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration + from sentry_sdk.integrations.starlette import StarletteIntegration + + # Проверяем что классы инстанцируются без ошибок + assert StarletteIntegration() + assert FastApiIntegration() + assert CeleryIntegration() + assert SqlalchemyIntegration() + assert HttpxIntegration() + assert LoggingIntegration() + + +def test_no_sdk_init_when_dsn_empty() -> None: + """Если GLITCHTIP_DSN пустой, sentry_sdk.init() не должен вызываться.""" + with patch("sentry_sdk.init") as mock_init: + # Симулируем логику guard из main.py: if settings.glitchtip_dsn: sentry_sdk.init(...) + glitchtip_dsn = None + if glitchtip_dsn: + sentry_sdk.init(dsn=glitchtip_dsn) + mock_init.assert_not_called() + + +def test_sdk_init_called_when_dsn_set() -> None: + """Если GLITCHTIP_DSN задан, sentry_sdk.init() вызывается с правильными параметрами.""" + dsn = "https://key@errors.gendsgn.ru/1" + with patch("sentry_sdk.init") as mock_init: + glitchtip_dsn = dsn + if glitchtip_dsn: + sentry_sdk.init( + dsn=glitchtip_dsn, + environment="test", + release="unknown", + traces_sample_rate=0.05, + profiles_sample_rate=0.0, + send_default_pii=False, + integrations=[], + ) + mock_init.assert_called_once() + call_kwargs = mock_init.call_args.kwargs + assert call_kwargs["dsn"] == dsn + assert call_kwargs["send_default_pii"] is False + assert call_kwargs["profiles_sample_rate"] == 0.0 diff --git a/backend/uv.lock b/backend/uv.lock index 1422c133..82b7289a 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -579,7 +579,7 @@ dependencies = [ { name = "redis" }, { name = "rosreestr2coord" }, { name = "scikit-learn" }, - { name = "sentry-sdk", extra = ["fastapi"] }, + { name = "sentry-sdk", extra = ["celery", "fastapi", "httpx", "sqlalchemy"] }, { name = "shapely" }, { name = "sqlalchemy" }, { name = "tenacity" }, @@ -618,7 +618,7 @@ requires-dist = [ { name = "redis", specifier = ">=5.0.0" }, { name = "rosreestr2coord", specifier = ">=5.0.0" }, { name = "scikit-learn", specifier = ">=1.5.0" }, - { name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.10.0" }, + { name = "sentry-sdk", extras = ["fastapi", "celery", "sqlalchemy", "httpx"], specifier = ">=2.18.0" }, { name = "shapely", specifier = ">=2.0.0" }, { name = "sqlalchemy", specifier = ">=2.0.30" }, { name = "tenacity", specifier = ">=9.0.0" }, @@ -1910,9 +1910,18 @@ wheels = [ ] [package.optional-dependencies] +celery = [ + { name = "celery" }, +] fastapi = [ { name = "fastapi" }, ] +httpx = [ + { name = "httpx" }, +] +sqlalchemy = [ + { name = "sqlalchemy" }, +] [[package]] name = "shapely"