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
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