Source code for faust.contrib.sentry

import asyncio
import logging
import sys
import traceback
import typing
from functools import partial
from typing import Any, Iterable, Optional, Type

from faust.exceptions import ImproperlyConfigured
from faust.types import AppT

try:
    import raven
except ImportError:  # pragma: no cover
    raven = None  # noqa

try:
    import sentry_sdk
except ImportError:  # pragma: no cover
    sentry_sdk = None  # noqa
    _sdk_aiohttp = None  # noqa
else:
    import sentry_sdk.integrations.aiohttp as _sdk_aiohttp  # type: ignore

try:
    import raven_aiohttp
except ImportError:  # pragma: no cover
    raven_aiohttp = None  # noqa

if typing.TYPE_CHECKING:
    from raven.handlers.logging import SentryHandler as _SentryHandler
else:

    class _SentryHandler: ...  # noqa: E701


__all__ = ["handler_from_dsn", "setup"]

DEFAULT_LEVEL: int = logging.WARNING


def _build_sentry_handler() -> Type[_SentryHandler]:
    from raven.handlers import logging as _logging

    class FaustSentryHandler(_logging.SentryHandler):  # type: ignore
        # 1) We override SentryHandler to write internal log messages
        #    mto sys.__stderr__ instead of sys.stderr, as the latter
        #    is redirected to a logger, causing noisy internal logs
        #    to be sent to Sentry.
        #
        # 2) We augment can_record to skip logging for CancelledError
        #    exceptions that have a ``.is_expected`` attribute set to True.
        #    That way once a CancelledError showing up in Sentry is
        #    investigated and resolved, we can silence that particular
        #    error in the future.
        def can_record(self, record: logging.LogRecord) -> bool:
            return super().can_record(record) and not self._is_expected_cancel(record)

        def _is_expected_cancel(self, record: logging.LogRecord) -> bool:
            # Returns true if this log record is associated with a
            # CancelledError.is_expected exception.
            if record.exc_info and record.exc_info[0] is not None:
                return bool(
                    issubclass(record.exc_info[0], asyncio.CancelledError)
                    and getattr(record.exc_info[1], "is_expected", True)
                )
            return False

        def emit(self, record: logging.LogRecord) -> None:
            try:
                self.format(record)

                if self.can_record(record):
                    self._emit(record)
                else:
                    self.carp(record.message)
            except Exception:
                if self.client.raise_send_errors:
                    raise
                self.carp(
                    "Top level Sentry exception caught - failed " "creating log record"
                )
                self.carp(record.msg)
                self.carp(traceback.format_exc())

        def carp(self, obj: Any) -> None:
            print(_logging.to_string(obj), file=sys.__stderr__)

    return FaustSentryHandler


[docs]def handler_from_dsn( dsn: Optional[str] = None, workers: int = 5, include_paths: Iterable[str] = None, loglevel: Optional[int] = None, qsize: int = 1000, **kwargs: Any, ) -> Optional[logging.Handler]: if raven is None: raise ImproperlyConfigured("faust.contrib.sentry requires the `raven` library.") if raven_aiohttp is None: raise ImproperlyConfigured( "faust.contrib.sentry requires the `raven_aiohttp` library." ) level: int = loglevel if loglevel is not None else DEFAULT_LEVEL if dsn: client = raven.Client( dsn=dsn, include_paths=include_paths, transport=partial( raven_aiohttp.QueuedAioHttpTransport, workers=workers, qsize=qsize, ), disable_existing_loggers=False, **kwargs, ) handler = _build_sentry_handler()(client) handler.setLevel(level) return handler return None
[docs]def setup( app: AppT, *, dsn: Optional[str] = None, workers: int = 4, max_queue_size: int = 1000, loglevel: Optional[int] = None, **kwargs, ) -> None: sentry_handler = handler_from_dsn( dsn=dsn, workers=workers, qsize=max_queue_size, loglevel=loglevel, **kwargs ) if sentry_handler is not None: if sentry_sdk is None or _sdk_aiohttp is None: raise ImproperlyConfigured( "faust.contrib.sentry requires the `sentry_sdk` library." ) sentry_sdk.init( dsn=dsn, integrations=[_sdk_aiohttp.AioHttpIntegration()], ) app.conf.loghandlers.append(sentry_handler)