Context manager, který potlačí duplicitní Sentry log-eventy a pošle jednu výjimku s plným kontextem — informativní logy v aplikaci, čistý grouping v Sentry.
Problém
Sentry Python SDK s LoggingIntegration zachytává každý logger.error() / logger.warning() jako samostatný Sentry event. To přináší dva problémy najednou.
Duplikáty. Když chybu ošetříte a zalogujete, dostanete v Sentry dva eventy: log-event + exception event (pokud voláte i capture_exception). Jeden caught error vygeneruje 2–3 Sentry eventy, každý s jiným groupingem, a kvóta se plní zbytečně rychle.
Rozbitý grouping. Sentry u log-eventů vidí jen text zprávy. Nemá výjimku, nemá stacktrace — nemá se čeho chytit. Zprávy Download failed: url=AAA a Download failed: url=BBB jsou pro Sentry dva různé problémy, i když jde o tutéž chybu se stejným stacktrace. Naivní řešení je nedávat specifické informace do log message — ale to je přesně to, co v logu chceme. Informativní zpráva Download failed: url=https://example.com/api/v2/products je v logu neocenitelná, ale pro Sentry grouping je jed.
Co context manager dělá
with sentry_suppress_logs_capture_exception(exc):
logger.warning("Download failed: url=%s", url)
Uvnitř bloku:
logger.warning()/logger.error()jde jen do lokálního logu — Sentry log-event se zahodí (přesbefore_sendhook)- Můžete logovat kolik chcete, žádný z logů nevytvoří Sentry event
Po ukončení bloku:
- Sentry dostane přesně 1 event:
capture_exception(exc)s úrovníwarning - Event obsahuje plný stacktrace, exception chain, tagy a context
Jak to funguje uvnitř
Tři spolupracující komponenty:
1. Context variable
Signalizuje, že jsme uvnitř suppress bloku:
_suppress_ctx: contextvars.ContextVar[bool] = contextvars.ContextVar(
"sentry_suppress", default=False
)
2. Log record factory hook
Označí log záznamy z potlačeného bloku:
def factory(*args, **kwargs):
record = original_factory(*args, **kwargs)
if record.levelno >= logging.WARNING and _suppress_ctx.get():
record.sentry_skip = True
return record
3. before_send hook
Zahodí označené log-eventy:
def before_send(event, hint):
record = hint.get("log_record")
if record is not None and getattr(record, "sentry_skip", False):
return None
return event
Context manager samotný
@contextmanager
def sentry_suppress_logs_capture_exception(
exc: BaseException,
*,
tags: Mapping[str, str] | None = None,
context: Mapping[str, Any] | None = None,
) -> Iterator[None]:
token = _suppress_ctx.set(True)
try:
yield
finally:
_suppress_ctx.reset(token)
with sentry_sdk.new_scope() as scope:
scope.set_level("warning")
if tags:
for k, v in tags.items():
scope.set_tag(k, v)
scope.set_extra("exc_type", type(exc).__name__)
scope.set_extra("exc_message", str(exc))
if context:
scope.set_context("warn.context", dict(context))
scope.capture_exception(exc)
Typické použití
Caught error, který nechceme propagovat, ale chceme ho vidět v Sentry:
try:
page = await downloader.download(url=url)
except HTTPError as exc:
with sentry_suppress_logs_capture_exception(exc):
logger.warning("Download failed: url=%s", url)
page = DownloadPage(url=url, content="") # fallback
Ošetření chyby v error handleru — nechceme, aby sekundární selhání přebilo primární:
try:
task = await repository.find(id=task_id)
except Exception as refetch_err:
with sentry_suppress_logs_capture_exception(refetch_err):
logger.warning("Failed to re-fetch task %s: %s", task_id, refetch_err)
task = None
Race condition — objekt smazán jiným procesem:
except StaleDataError as exc:
with sentry_suppress_logs_capture_exception(exc):
logger.warning(
"Product %s deleted before update could commit, skipping.",
product_id,
)
Bez vs. s context managerem
| Bez | S |
|---|---|
logger.error() → 1 Sentry log-event (bez stacktrace) |
logger.warning() → jen lokální log |
capture_exception() → 1 Sentry exception event |
capture_exception() → 1 Sentry exception event |
| 2 eventy v Sentry, duplikátní šum | 1 event v Sentry, plný kontext |
Proč contextvars a ne thread-local
Aplikace je async (asyncio). Thread-local by nefungoval — coroutiny sdílejí vlákno. contextvars.ContextVar je asyncio-aware a správně izoluje kontext per-task.