authentik.lib.sentry

authentik sentry integration

  1"""authentik sentry integration"""
  2
  3from asyncio.exceptions import CancelledError
  4from typing import Any
  5
  6from django.conf import settings
  7from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError
  8from django.db import DatabaseError, InternalError, OperationalError, ProgrammingError
  9from django.http.response import Http404
 10from docker.errors import DockerException
 11from dramatiq.errors import Retry
 12from h11 import LocalProtocolError
 13from ldap3.core.exceptions import LDAPException
 14from psycopg.errors import Error
 15from rest_framework.exceptions import APIException
 16from sentry_sdk import HttpTransport, get_current_scope
 17from sentry_sdk import init as sentry_sdk_init
 18from sentry_sdk.api import set_tag
 19from sentry_sdk.integrations.argv import ArgvIntegration
 20from sentry_sdk.integrations.django import DjangoIntegration
 21from sentry_sdk.integrations.dramatiq import DramatiqIntegration
 22from sentry_sdk.integrations.socket import SocketIntegration
 23from sentry_sdk.integrations.stdlib import StdlibIntegration
 24from sentry_sdk.integrations.threading import ThreadingIntegration
 25from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME
 26from structlog.stdlib import get_logger
 27from websockets.exceptions import WebSocketException
 28
 29from authentik import authentik_build_hash, authentik_version
 30from authentik.lib.config import CONFIG
 31from authentik.lib.utils.http import authentik_user_agent
 32from authentik.lib.utils.reflection import get_env
 33
 34LOGGER = get_logger()
 35_root_path = CONFIG.get("web.path", "/")
 36
 37
 38class SentryIgnoredException(Exception):
 39    """Base Class for all errors that are suppressed, and not sent to sentry."""
 40
 41
 42ignored_classes = (
 43    # Inbuilt types
 44    KeyboardInterrupt,
 45    ConnectionResetError,
 46    OSError,
 47    PermissionError,
 48    # Django Errors
 49    Error,
 50    ImproperlyConfigured,
 51    DatabaseError,
 52    OperationalError,
 53    InternalError,
 54    ProgrammingError,
 55    SuspiciousOperation,
 56    ValidationError,
 57    # websocket errors
 58    WebSocketException,
 59    LocalProtocolError,
 60    # rest_framework error
 61    APIException,
 62    # dramatiq errors
 63    Retry,
 64    # custom baseclass
 65    SentryIgnoredException,
 66    # ldap errors
 67    LDAPException,
 68    # Docker errors
 69    DockerException,
 70    # End-user errors
 71    Http404,
 72    # AsyncIO
 73    CancelledError,
 74)
 75
 76
 77class SentryTransport(HttpTransport):
 78    """Custom sentry transport with custom user-agent"""
 79
 80    def __init__(self, options: dict[str, Any]) -> None:
 81        super().__init__(options)
 82        self._auth = self.parsed_dsn.to_auth(authentik_user_agent())
 83
 84
 85def sentry_init(**sentry_init_kwargs):
 86    """Configure sentry SDK"""
 87    sentry_env = CONFIG.get("error_reporting.environment", "customer")
 88    kwargs = {
 89        "environment": sentry_env,
 90        "send_default_pii": CONFIG.get_bool("error_reporting.send_pii", False),
 91        "_experiments": {
 92            "profiles_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.1)),
 93        },
 94        **sentry_init_kwargs,
 95        **CONFIG.get_dict_from_b64_json("error_reporting.extra_args", {}),
 96    }
 97
 98    sentry_sdk_init(
 99        dsn=CONFIG.get("error_reporting.sentry_dsn"),
100        integrations=[
101            ArgvIntegration(),
102            DjangoIntegration(transaction_style="function_name", cache_spans=True),
103            DramatiqIntegration(),
104            SocketIntegration(),
105            StdlibIntegration(),
106            ThreadingIntegration(propagate_hub=True),
107        ],
108        before_send=before_send,
109        traces_sampler=traces_sampler,
110        release=f"authentik@{authentik_version()}",
111        transport=SentryTransport,
112        **kwargs,
113    )
114    set_tag("authentik.build_hash", authentik_build_hash("tagged"))
115    set_tag("authentik.env", get_env())
116    set_tag("authentik.component", "backend")
117
118
119def traces_sampler(sampling_context: dict) -> float:
120    """Custom sampler to ignore certain routes"""
121    path = sampling_context.get("asgi_scope", {}).get("path", "")
122    _type = sampling_context.get("asgi_scope", {}).get("type", "")
123    # Ignore all healthcheck routes
124    if path.startswith(f"{_root_path}-/health") or path.startswith(f"{_root_path}-/metrics"):
125        return 0
126    if _type == "websocket":
127        return 0
128    if CONFIG.get_bool("debug"):
129        return 1
130    return float(CONFIG.get("error_reporting.sample_rate", 0.1))
131
132
133def should_ignore_exception(exc: Exception) -> bool:
134    """Check if an exception should be dropped"""
135    return isinstance(exc, ignored_classes)
136
137
138def before_send(event: dict, hint: dict) -> dict | None:
139    """Check if error is database error, and ignore if so"""
140    exc_value = None
141    if "exc_info" in hint:
142        _, exc_value, _ = hint["exc_info"]
143        if should_ignore_exception(exc_value):
144            LOGGER.debug("dropping exception", exc=exc_value)
145            return None
146    if "logger" in event:
147        if event["logger"] in [
148            "asyncio",
149            "multiprocessing",
150            "django.security.DisallowedHost",
151            "paramiko.transport",
152        ]:
153            return None
154    LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None))
155    if settings.DEBUG:
156        return None
157    return event
158
159
160def get_http_meta():
161    """Get sentry-related meta key-values"""
162    scope = get_current_scope()
163    meta = {
164        SENTRY_TRACE_HEADER_NAME: scope.get_traceparent() or "",
165    }
166    if bag := scope.get_baggage():
167        meta[BAGGAGE_HEADER_NAME] = bag.serialize()
168    return meta
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
class SentryIgnoredException(builtins.Exception):
39class SentryIgnoredException(Exception):
40    """Base Class for all errors that are suppressed, and not sent to sentry."""

Base Class for all errors that are suppressed, and not sent to sentry.

ignored_classes = (<class 'KeyboardInterrupt'>, <class 'ConnectionResetError'>, <class 'OSError'>, <class 'PermissionError'>, <class 'psycopg.Error'>, <class 'django.core.exceptions.ImproperlyConfigured'>, <class 'django.db.utils.DatabaseError'>, <class 'django.db.utils.OperationalError'>, <class 'django.db.utils.InternalError'>, <class 'django.db.utils.ProgrammingError'>, <class 'django.core.exceptions.SuspiciousOperation'>, <class 'django.core.exceptions.ValidationError'>, <class 'websockets.exceptions.WebSocketException'>, <class 'h11._util.LocalProtocolError'>, <class 'rest_framework.exceptions.APIException'>, <class 'dramatiq.errors.Retry'>, <class 'SentryIgnoredException'>, <class 'ldap3.core.exceptions.LDAPException'>, <class 'docker.errors.DockerException'>, <class 'django.http.response.Http404'>, <class 'asyncio.exceptions.CancelledError'>)
class SentryTransport(sentry_sdk.transport.HttpTransport):
78class SentryTransport(HttpTransport):
79    """Custom sentry transport with custom user-agent"""
80
81    def __init__(self, options: dict[str, Any]) -> None:
82        super().__init__(options)
83        self._auth = self.parsed_dsn.to_auth(authentik_user_agent())

Custom sentry transport with custom user-agent

SentryTransport(options: dict[str, typing.Any])
81    def __init__(self, options: dict[str, Any]) -> None:
82        super().__init__(options)
83        self._auth = self.parsed_dsn.to_auth(authentik_user_agent())
def sentry_init(**sentry_init_kwargs):
 86def sentry_init(**sentry_init_kwargs):
 87    """Configure sentry SDK"""
 88    sentry_env = CONFIG.get("error_reporting.environment", "customer")
 89    kwargs = {
 90        "environment": sentry_env,
 91        "send_default_pii": CONFIG.get_bool("error_reporting.send_pii", False),
 92        "_experiments": {
 93            "profiles_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.1)),
 94        },
 95        **sentry_init_kwargs,
 96        **CONFIG.get_dict_from_b64_json("error_reporting.extra_args", {}),
 97    }
 98
 99    sentry_sdk_init(
100        dsn=CONFIG.get("error_reporting.sentry_dsn"),
101        integrations=[
102            ArgvIntegration(),
103            DjangoIntegration(transaction_style="function_name", cache_spans=True),
104            DramatiqIntegration(),
105            SocketIntegration(),
106            StdlibIntegration(),
107            ThreadingIntegration(propagate_hub=True),
108        ],
109        before_send=before_send,
110        traces_sampler=traces_sampler,
111        release=f"authentik@{authentik_version()}",
112        transport=SentryTransport,
113        **kwargs,
114    )
115    set_tag("authentik.build_hash", authentik_build_hash("tagged"))
116    set_tag("authentik.env", get_env())
117    set_tag("authentik.component", "backend")

Configure sentry SDK

def traces_sampler(sampling_context: dict) -> float:
120def traces_sampler(sampling_context: dict) -> float:
121    """Custom sampler to ignore certain routes"""
122    path = sampling_context.get("asgi_scope", {}).get("path", "")
123    _type = sampling_context.get("asgi_scope", {}).get("type", "")
124    # Ignore all healthcheck routes
125    if path.startswith(f"{_root_path}-/health") or path.startswith(f"{_root_path}-/metrics"):
126        return 0
127    if _type == "websocket":
128        return 0
129    if CONFIG.get_bool("debug"):
130        return 1
131    return float(CONFIG.get("error_reporting.sample_rate", 0.1))

Custom sampler to ignore certain routes

def should_ignore_exception(exc: Exception) -> bool:
134def should_ignore_exception(exc: Exception) -> bool:
135    """Check if an exception should be dropped"""
136    return isinstance(exc, ignored_classes)

Check if an exception should be dropped

def before_send(event: dict, hint: dict) -> dict | None:
139def before_send(event: dict, hint: dict) -> dict | None:
140    """Check if error is database error, and ignore if so"""
141    exc_value = None
142    if "exc_info" in hint:
143        _, exc_value, _ = hint["exc_info"]
144        if should_ignore_exception(exc_value):
145            LOGGER.debug("dropping exception", exc=exc_value)
146            return None
147    if "logger" in event:
148        if event["logger"] in [
149            "asyncio",
150            "multiprocessing",
151            "django.security.DisallowedHost",
152            "paramiko.transport",
153        ]:
154            return None
155    LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None))
156    if settings.DEBUG:
157        return None
158    return event

Check if error is database error, and ignore if so

def get_http_meta():
161def get_http_meta():
162    """Get sentry-related meta key-values"""
163    scope = get_current_scope()
164    meta = {
165        SENTRY_TRACE_HEADER_NAME: scope.get_traceparent() or "",
166    }
167    if bag := scope.get_baggage():
168        meta[BAGGAGE_HEADER_NAME] = bag.serialize()
169    return meta

Get sentry-related meta key-values