feat(backend): sentry-sdk init для FastAPI + Celery через GlitchTip (#204 backend)
Подключает 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"
This commit is contained in:
parent
256909d28b
commit
2f79f7c40a
7 changed files with 146 additions and 18 deletions
|
|
@ -6,9 +6,11 @@ REDIS_URL=redis://redis:6379/0
|
||||||
CORS_ORIGINS=["http://localhost:3000"]
|
CORS_ORIGINS=["http://localhost:3000"]
|
||||||
ENVIRONMENT=dev
|
ENVIRONMENT=dev
|
||||||
|
|
||||||
# Sentry: create a project at https://sentry.io/, paste the DSN here.
|
# GlitchTip error tracking (https://errors.gendsgn.ru)
|
||||||
# Empty string = Sentry disabled. main.py initializes only if non-empty.
|
# Формат: https://<key>@errors.gendsgn.ru/<project_id>
|
||||||
SENTRY_DSN=
|
# Пустая строка = SDK не инициализируется.
|
||||||
|
GLITCHTIP_DSN=
|
||||||
|
GLITCHTIP_TRACES_SAMPLE_RATE=0.05
|
||||||
|
|
||||||
# External APIs (Stage 2)
|
# External APIs (Stage 2)
|
||||||
ROSREESTR_PKK_BASE_URL=https://pkk.rosreestr.ru/api/features/1
|
ROSREESTR_PKK_BASE_URL=https://pkk.rosreestr.ru/api/features/1
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ class Settings(BaseSettings):
|
||||||
database_url: str = "postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign"
|
database_url: str = "postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign"
|
||||||
redis_url: str = "redis://localhost:6379/0"
|
redis_url: str = "redis://localhost:6379/0"
|
||||||
cors_origins: list[str] = ["http://localhost:3000"]
|
cors_origins: list[str] = ["http://localhost:3000"]
|
||||||
sentry_dsn: str | None = None
|
# GlitchTip error tracking (Sentry-compatible self-hosted).
|
||||||
# Release tag для Sentry — обычно git short sha, проставляется
|
# Формат DSN: https://<key>@errors.gendsgn.ru/<project_id>
|
||||||
# deploy.yml в backend/.env.runtime (см. workflow). Локально оставляем
|
# Пустая строка / None = SDK не инициализируется (no-op).
|
||||||
# пустым — Sentry припишет 'unknown'.
|
glitchtip_dsn: str | None = None
|
||||||
sentry_release: str | None = None
|
glitchtip_traces_sample_rate: float = 0.05
|
||||||
environment: str = "dev"
|
environment: str = "dev"
|
||||||
|
|
||||||
# External APIs (Stage 2)
|
# External APIs (Stage 2)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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 (
|
from app.api.v1 import (
|
||||||
admin_cadastre,
|
admin_cadastre,
|
||||||
|
|
@ -18,16 +26,37 @@ from app.api.v1 import (
|
||||||
)
|
)
|
||||||
from app.core.config import settings
|
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
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
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
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,42 @@ Worker lifecycle hooks (process_init, worker_ready) → app/workers/lifecycle.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import sentry_sdk
|
||||||
from celery import Celery
|
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
|
from app.core.config import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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(
|
celery_app = Celery(
|
||||||
"gendesign",
|
"gendesign",
|
||||||
broker=settings.redis_url,
|
broker=settings.redis_url,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ dependencies = [
|
||||||
"pandas>=2.2.0",
|
"pandas>=2.2.0",
|
||||||
"numpy>=2.0.0",
|
"numpy>=2.0.0",
|
||||||
"scikit-learn>=1.5.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",
|
"rosreestr2coord>=5.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
59
backend/tests/test_sentry_init.py
Normal file
59
backend/tests/test_sentry_init.py
Normal file
|
|
@ -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
|
||||||
13
backend/uv.lock
generated
13
backend/uv.lock
generated
|
|
@ -579,7 +579,7 @@ dependencies = [
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
{ name = "rosreestr2coord" },
|
{ name = "rosreestr2coord" },
|
||||||
{ name = "scikit-learn" },
|
{ name = "scikit-learn" },
|
||||||
{ name = "sentry-sdk", extra = ["fastapi"] },
|
{ name = "sentry-sdk", extra = ["celery", "fastapi", "httpx", "sqlalchemy"] },
|
||||||
{ name = "shapely" },
|
{ name = "shapely" },
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
{ name = "tenacity" },
|
{ name = "tenacity" },
|
||||||
|
|
@ -618,7 +618,7 @@ requires-dist = [
|
||||||
{ name = "redis", specifier = ">=5.0.0" },
|
{ name = "redis", specifier = ">=5.0.0" },
|
||||||
{ name = "rosreestr2coord", specifier = ">=5.0.0" },
|
{ name = "rosreestr2coord", specifier = ">=5.0.0" },
|
||||||
{ name = "scikit-learn", specifier = ">=1.5.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 = "shapely", specifier = ">=2.0.0" },
|
||||||
{ name = "sqlalchemy", specifier = ">=2.0.30" },
|
{ name = "sqlalchemy", specifier = ">=2.0.30" },
|
||||||
{ name = "tenacity", specifier = ">=9.0.0" },
|
{ name = "tenacity", specifier = ">=9.0.0" },
|
||||||
|
|
@ -1910,9 +1910,18 @@ wheels = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
|
celery = [
|
||||||
|
{ name = "celery" },
|
||||||
|
]
|
||||||
fastapi = [
|
fastapi = [
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
]
|
]
|
||||||
|
httpx = [
|
||||||
|
{ name = "httpx" },
|
||||||
|
]
|
||||||
|
sqlalchemy = [
|
||||||
|
{ name = "sqlalchemy" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shapely"
|
name = "shapely"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue