feat(observability): add Sentry error and performance monitoring integration
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+9
-1
@@ -1,7 +1,7 @@
|
||||
# app/celery_app.py
|
||||
|
||||
from celery import Celery
|
||||
from celery.signals import task_failure
|
||||
from celery.signals import task_failure, worker_ready
|
||||
|
||||
from app.config import settings
|
||||
|
||||
@@ -22,6 +22,14 @@ celery.conf.task_routes = {
|
||||
}
|
||||
|
||||
|
||||
@worker_ready.connect
|
||||
def init_sentry_on_worker_ready(**kwargs):
|
||||
"""Initialise Sentry SDK in the Celery worker process."""
|
||||
from app.utils.sentry import init_sentry
|
||||
|
||||
init_sentry(integrations_extra=["celery"])
|
||||
|
||||
|
||||
@task_failure.connect
|
||||
def task_failure_handler(
|
||||
sender=None, task_id=None, exception=None, args=None, kwargs=None, traceback=None, einfo=None, **kw
|
||||
|
||||
@@ -941,6 +941,50 @@ class Settings(BaseSettings):
|
||||
description="Support e-mail address displayed on the Help Center page.",
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Observability – Sentry error & performance monitoring
|
||||
# ---------------------------------------------------------------------------
|
||||
sentry_dsn: Optional[str] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Sentry Data Source Name (DSN). When set, error reporting and "
|
||||
"performance tracing are enabled automatically. Leave blank (or unset) "
|
||||
"to disable Sentry entirely."
|
||||
),
|
||||
)
|
||||
sentry_environment: str = Field(
|
||||
default="production",
|
||||
description=(
|
||||
"Environment tag sent to Sentry (e.g. 'development', 'staging', 'production'). "
|
||||
"Helps you filter events in the Sentry dashboard."
|
||||
),
|
||||
)
|
||||
sentry_traces_sample_rate: float = Field(
|
||||
default=0.1,
|
||||
description=(
|
||||
"Fraction of transactions to capture for performance monitoring (0.0–1.0). "
|
||||
"Set to 0.0 to disable tracing, 1.0 to capture every transaction. "
|
||||
"Values above 0 may increase Sentry quota usage."
|
||||
),
|
||||
)
|
||||
sentry_profiles_sample_rate: float = Field(
|
||||
default=0.0,
|
||||
description=(
|
||||
"Fraction of profiled transactions to send to Sentry (0.0–1.0). "
|
||||
"Profiling is only active when traces_sample_rate > 0. "
|
||||
"Defaults to 0.0 (disabled) to minimise overhead."
|
||||
),
|
||||
)
|
||||
sentry_send_default_pii: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Whether to attach personally identifiable information (PII) – such as "
|
||||
"IP addresses and user agents – to Sentry events. Disabled by default "
|
||||
"for privacy compliance (GDPR / CCPA). Enable only if your Sentry "
|
||||
"project is configured to handle PII."
|
||||
),
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def strip_outer_quotes(cls, data: Any) -> Any:
|
||||
|
||||
@@ -27,6 +27,7 @@ from app.middleware.request_size_limit import RequestSizeLimitMiddleware
|
||||
from app.middleware.security_headers import SecurityHeadersMiddleware
|
||||
from app.utils.config_validator import check_all_configs
|
||||
from app.utils.notification import init_apprise, notify_shutdown, notify_startup
|
||||
from app.utils.sentry import init_sentry
|
||||
|
||||
# Import the routers - now using views directly instead of frontend
|
||||
from app.views import router as frontend_router
|
||||
@@ -47,6 +48,9 @@ SESSION_SECRET = (
|
||||
settings.session_secret or "INSECURE_DEFAULT_FOR_DEVELOPMENT_ONLY_DO_NOT_USE_IN_PRODUCTION_MINIMUM_32_CHARS"
|
||||
)
|
||||
|
||||
# Initialise Sentry as early as possible so that any startup errors are captured
|
||||
init_sentry()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Sentry integration utilities for DocuElevate.
|
||||
|
||||
Call ``init_sentry()`` early in your application entry point (before any
|
||||
request handling) to enable error tracking and performance monitoring. The
|
||||
function is a no-op when ``SENTRY_DSN`` is not configured, so it is safe to
|
||||
call unconditionally in all environments.
|
||||
|
||||
Example (FastAPI)::
|
||||
|
||||
from app.utils.sentry import init_sentry
|
||||
init_sentry()
|
||||
|
||||
Example (Celery worker)::
|
||||
|
||||
from app.utils.sentry import init_sentry
|
||||
init_sentry(integrations_extra=["celery"])
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def init_sentry(*, integrations_extra: list[str] | None = None) -> bool:
|
||||
"""
|
||||
Initialise the Sentry SDK if ``SENTRY_DSN`` is configured.
|
||||
|
||||
Args:
|
||||
integrations_extra: Optional list of additional integration names to
|
||||
activate. Currently recognised values: ``"celery"``. The
|
||||
``FastApiIntegration``, ``SqlalchemyIntegration``, and
|
||||
``LoggingIntegration`` are always included when the SDK is
|
||||
initialised.
|
||||
|
||||
Returns:
|
||||
``True`` when Sentry was successfully initialised, ``False`` otherwise
|
||||
(e.g. DSN not configured or SDK not installed).
|
||||
"""
|
||||
dsn = settings.sentry_dsn
|
||||
if not dsn:
|
||||
logger.debug("Sentry DSN not configured – error monitoring disabled")
|
||||
return False
|
||||
|
||||
try:
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||
from sentry_sdk.integrations.logging import LoggingIntegration
|
||||
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
|
||||
from sentry_sdk.integrations.starlette import StarletteIntegration
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"sentry-sdk is not installed. Install it with: pip install 'sentry-sdk[fastapi,celery,sqlalchemy]'"
|
||||
)
|
||||
return False
|
||||
|
||||
integrations = [
|
||||
StarletteIntegration(transaction_style="url"),
|
||||
FastApiIntegration(transaction_style="url"),
|
||||
SqlalchemyIntegration(),
|
||||
LoggingIntegration(
|
||||
level=logging.INFO, # Breadcrumbs from INFO+
|
||||
event_level=logging.ERROR, # Send Sentry events for ERROR+
|
||||
),
|
||||
]
|
||||
|
||||
if integrations_extra and "celery" in integrations_extra:
|
||||
try:
|
||||
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||
|
||||
integrations.append(CeleryIntegration(monitor_beat_tasks=True))
|
||||
except ImportError:
|
||||
logger.warning("CeleryIntegration not available – skipping")
|
||||
|
||||
# Clamp sample rates to [0.0, 1.0]
|
||||
traces_rate = max(0.0, min(1.0, settings.sentry_traces_sample_rate))
|
||||
profiles_rate = max(0.0, min(1.0, settings.sentry_profiles_sample_rate))
|
||||
|
||||
version = _get_app_version()
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=dsn,
|
||||
environment=settings.sentry_environment,
|
||||
release=version,
|
||||
integrations=integrations,
|
||||
traces_sample_rate=traces_rate,
|
||||
profiles_sample_rate=profiles_rate,
|
||||
send_default_pii=settings.sentry_send_default_pii,
|
||||
# Attach a request body snapshot to every event (helps debugging)
|
||||
max_request_body_size="medium",
|
||||
# Keep the SDK from attaching local variable values to stack frames
|
||||
# by default; enable explicitly if needed for deeper debugging.
|
||||
attach_stacktrace=True,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Sentry initialised (environment=%s, traces_sample_rate=%s)",
|
||||
settings.sentry_environment,
|
||||
traces_rate,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _get_app_version() -> str | None:
|
||||
"""Return the application version string for Sentry release tracking."""
|
||||
try:
|
||||
return settings.version or None
|
||||
except AttributeError: # pragma: no cover
|
||||
return None
|
||||
@@ -2264,6 +2264,64 @@ SETTING_METADATA = {
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
# Observability – Sentry
|
||||
"sentry_dsn": {
|
||||
"category": "Observability",
|
||||
"description": (
|
||||
"Sentry DSN (Data Source Name) URL. When set, runtime errors and "
|
||||
"performance traces are automatically sent to Sentry. "
|
||||
"Leave blank to disable Sentry entirely."
|
||||
),
|
||||
"type": "string",
|
||||
"sensitive": True,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"sentry_environment": {
|
||||
"category": "Observability",
|
||||
"description": (
|
||||
"Environment label attached to every Sentry event "
|
||||
"(e.g. 'development', 'staging', 'production'). "
|
||||
"Helps you filter events in the Sentry dashboard. Default: 'production'."
|
||||
),
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"sentry_traces_sample_rate": {
|
||||
"category": "Observability",
|
||||
"description": (
|
||||
"Fraction of transactions captured for Sentry performance monitoring (0.0–1.0). "
|
||||
"0.0 disables tracing; 1.0 captures every request. Default: 0.1 (10%)."
|
||||
),
|
||||
"type": "float",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"sentry_profiles_sample_rate": {
|
||||
"category": "Observability",
|
||||
"description": (
|
||||
"Fraction of profiled transactions sent to Sentry (0.0–1.0). "
|
||||
"Only active when sentry_traces_sample_rate > 0. Default: 0.0 (disabled)."
|
||||
),
|
||||
"type": "float",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"sentry_send_default_pii": {
|
||||
"category": "Observability",
|
||||
"description": (
|
||||
"Attach personally identifiable information (PII) such as IP addresses "
|
||||
"to Sentry events. Disabled by default for GDPR/CCPA compliance."
|
||||
),
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user