From c51ee08fd617d6ea9fdb9a450211f454c1d50888 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 09:53:24 +0200 Subject: [PATCH 01/19] Avoid cursor.connection.encoding access to prevent connection pool interference --- sentry_sdk/tracing_utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 552f4fd59a..50b27081ed 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -354,7 +354,12 @@ def _format_sql(cursor, sql): if hasattr(cursor, "mogrify"): real_sql = cursor.mogrify(sql) if isinstance(real_sql, bytes): - real_sql = real_sql.decode(cursor.connection.encoding) + # Use UTF-8 as default, with latin1 fallback for edge cases + try: + real_sql = real_sql.decode("utf-8") + except UnicodeDecodeError: + # If UTF-8 fails, try latin1 as fallback + real_sql = real_sql.decode("latin1", errors="replace") except Exception: real_sql = None From 8182704e6e44d84a120f970e09aa60e25659f47f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 10:08:19 +0200 Subject: [PATCH 02/19] trying to fetch connection parameters on startup --- sentry_sdk/integrations/django/__init__.py | 156 +++++++++++++++------ 1 file changed, 110 insertions(+), 46 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 2041598fa0..f242fd18cd 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -1,4 +1,3 @@ -import inspect import sys import threading import weakref @@ -30,6 +29,9 @@ RequestExtractor, ) +# Global cache for database configurations to avoid connection pool interference +_cached_db_configs = {} # type: Dict[str, Dict[str, Any]] + try: from django import VERSION as DJANGO_VERSION from django.conf import settings as django_settings @@ -156,6 +158,9 @@ def setup_once(): # type: () -> None _check_minimum_version(DjangoIntegration, DJANGO_VERSION) + # Cache database configurations to avoid connection pool interference + _cache_database_configurations() + install_sql_hook() # Patch in our custom middleware. @@ -614,6 +619,53 @@ def _set_user_info(request, event): pass +def _cache_database_configurations(): + # type: () -> None + """Cache database configurations from Django settings to avoid connection pool interference.""" + global _cached_db_configs + + try: + from django.conf import settings + from django.db import connections + + for alias, db_config in settings.DATABASES.items(): + if not db_config: # Skip empty default configs + continue + + try: + # Get database wrapper to access vendor info + db_wrapper = connections[alias] + + _cached_db_configs[alias] = { + "db_name": db_config.get("NAME"), + "host": db_config.get("HOST", "localhost"), + "port": db_config.get("PORT"), + "vendor": db_wrapper.vendor, + "unix_socket": db_config.get("OPTIONS", {}).get("unix_socket"), + "engine": db_config.get("ENGINE"), + } + + logger.debug( + "Cached database configuration for %s: %s", + alias, + { + k: v + for k, v in _cached_db_configs[alias].items() + if k != "vendor" + }, + ) + + except Exception as e: + logger.debug( + "Failed to cache database configuration for %s: %s", alias, e + ) + + except Exception as e: + logger.debug("Failed to cache database configurations: %s", e) + # Graceful fallback - cache remains empty + _cached_db_configs = {} + + def install_sql_hook(): # type: () -> None """If installed this causes Django's queries to be captured.""" @@ -698,54 +750,66 @@ def connect(self): def _set_db_data(span, cursor_or_db): # type: (Span, Any) -> None - db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db - vendor = db.vendor - span.set_data(SPANDATA.DB_SYSTEM, vendor) - - # Some custom backends override `__getattr__`, making it look like `cursor_or_db` - # actually has a `connection` and the `connection` has a `get_dsn_parameters` - # attribute, only to throw an error once you actually want to call it. - # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable - # function. - is_psycopg2 = ( - hasattr(cursor_or_db, "connection") - and hasattr(cursor_or_db.connection, "get_dsn_parameters") - and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) - ) - if is_psycopg2: - connection_params = cursor_or_db.connection.get_dsn_parameters() - else: - try: - # psycopg3, only extract needed params as get_parameters - # can be slow because of the additional logic to filter out default - # values - connection_params = { - "dbname": cursor_or_db.connection.info.dbname, - "port": cursor_or_db.connection.info.port, - } - # PGhost returns host or base dir of UNIX socket as an absolute path - # starting with /, use it only when it contains host - pg_host = cursor_or_db.connection.info.host - if pg_host and not pg_host.startswith("/"): - connection_params["host"] = pg_host - except Exception: - connection_params = db.get_connection_params() - - db_name = connection_params.get("dbname") or connection_params.get("database") - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) + """ + Improved version that avoids connection pool interference by using cached + database configurations and specific exception handling. + """ + from django.core.exceptions import ImproperlyConfigured - server_address = connection_params.get("host") - if server_address is not None: - span.set_data(SPANDATA.SERVER_ADDRESS, server_address) + # Get database wrapper + db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db - server_port = connection_params.get("port") - if server_port is not None: - span.set_data(SPANDATA.SERVER_PORT, str(server_port)) + # Set vendor (always safe, no connection access) + span.set_data(SPANDATA.DB_SYSTEM, db.vendor) + + # Get the database alias (supports .using() queries) + db_alias = db.alias + + # Method 1: Use pre-cached configuration (FASTEST, NO CONNECTION ACCESS) + cached_config = _cached_db_configs.get(db_alias) + if cached_config: + if cached_config["db_name"]: + span.set_data(SPANDATA.DB_NAME, cached_config["db_name"]) + if cached_config["host"]: + span.set_data(SPANDATA.SERVER_ADDRESS, cached_config["host"]) + if cached_config["port"]: + span.set_data(SPANDATA.SERVER_PORT, str(cached_config["port"])) + if cached_config["unix_socket"]: + span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, cached_config["unix_socket"]) + return - server_socket_address = connection_params.get("unix_socket") - if server_socket_address is not None: - span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) + # Method 2: Dynamic fallback using get_connection_params() (NO CONNECTION ACCESS) + try: + connection_params = db.get_connection_params() + + # Extract database name + db_name = connection_params.get("dbname") or connection_params.get("database") + if db_name: + span.set_data(SPANDATA.DB_NAME, db_name) + + # Extract host + host = connection_params.get("host") + if host: + span.set_data(SPANDATA.SERVER_ADDRESS, host) + + # Extract port + port = connection_params.get("port") + if port: + span.set_data(SPANDATA.SERVER_PORT, str(port)) + + # Extract unix socket + unix_socket = connection_params.get("unix_socket") + if unix_socket: + span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, unix_socket) + + except (KeyError, ImproperlyConfigured, AttributeError) as e: + # Specific exceptions that get_connection_params() can raise: + # - KeyError: Missing required database settings (most common) + # - ImproperlyConfigured: Django configuration problems + # - AttributeError: Missing methods on database wrapper + logger.debug("Failed to get database connection params for %s: %s", db_alias, e) + # Skip database metadata rather than risk connection issues + pass def add_template_context_repr_sequence(): From 7ca7e462d8e8fe9f3adb961f8f139c483c3532f8 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 10:50:05 +0200 Subject: [PATCH 03/19] Collect db data after connection is done. --- sentry_sdk/integrations/django/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index f242fd18cd..454f870bc4 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -640,7 +640,9 @@ def _cache_database_configurations(): "db_name": db_config.get("NAME"), "host": db_config.get("HOST", "localhost"), "port": db_config.get("PORT"), - "vendor": db_wrapper.vendor, + "vendor": ( + db_wrapper.vendor if hasattr(db_wrapper, "vendor") else None + ), "unix_socket": db_config.get("OPTIONS", {}).get("unix_socket"), "engine": db_config.get("ENGINE"), } @@ -739,8 +741,9 @@ def connect(self): name="connect", origin=DjangoIntegration.origin_db, ) as span: + connection = real_connect(self) _set_db_data(span, self) - return real_connect(self) + return connection CursorWrapper.execute = execute CursorWrapper.executemany = executemany From e4c13085195e1d8e4fe8676474a1e406b8894774 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 11:09:41 +0200 Subject: [PATCH 04/19] Cleanup --- sentry_sdk/integrations/django/__init__.py | 68 +++++++--------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 454f870bc4..03a326b87e 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -29,7 +29,6 @@ RequestExtractor, ) -# Global cache for database configurations to avoid connection pool interference _cached_db_configs = {} # type: Dict[str, Dict[str, Any]] try: @@ -157,12 +156,9 @@ def __init__( def setup_once(): # type: () -> None _check_minimum_version(DjangoIntegration, DJANGO_VERSION) - - # Cache database configurations to avoid connection pool interference _cache_database_configurations() install_sql_hook() - # Patch in our custom middleware. # logs an error for every 500 ignore_logger("django.server") @@ -621,7 +617,9 @@ def _set_user_info(request, event): def _cache_database_configurations(): # type: () -> None - """Cache database configurations from Django settings to avoid connection pool interference.""" + """ + Cache database configurations from Django settings to avoid connection pool interference. + """ global _cached_db_configs try: @@ -632,39 +630,24 @@ def _cache_database_configurations(): if not db_config: # Skip empty default configs continue - try: - # Get database wrapper to access vendor info - db_wrapper = connections[alias] - + with capture_internal_exceptions(): _cached_db_configs[alias] = { "db_name": db_config.get("NAME"), "host": db_config.get("HOST", "localhost"), "port": db_config.get("PORT"), - "vendor": ( - db_wrapper.vendor if hasattr(db_wrapper, "vendor") else None - ), "unix_socket": db_config.get("OPTIONS", {}).get("unix_socket"), "engine": db_config.get("ENGINE"), } - logger.debug( - "Cached database configuration for %s: %s", - alias, - { - k: v - for k, v in _cached_db_configs[alias].items() - if k != "vendor" - }, - ) + db_wrapper = connections.get(alias) + if db_wrapper is None: + continue - except Exception as e: - logger.debug( - "Failed to cache database configuration for %s: %s", alias, e - ) + if hasattr(db_wrapper, "vendor"): + _cached_db_configs[alias]["vendor"] = db_wrapper.vendor except Exception as e: logger.debug("Failed to cache database configurations: %s", e) - # Graceful fallback - cache remains empty _cached_db_configs = {} @@ -722,7 +705,6 @@ def executemany(self, sql, param_list): span_origin=DjangoIntegration.origin_db, ) as span: _set_db_data(span, self) - result = real_executemany(self, sql, param_list) with capture_internal_exceptions(): @@ -754,22 +736,22 @@ def connect(self): def _set_db_data(span, cursor_or_db): # type: (Span, Any) -> None """ - Improved version that avoids connection pool interference by using cached - database configurations and specific exception handling. + Set database connection data to the span. + + Tries first to use pre-cached configuration. + If that fails, it uses get_connection_params() to get the database connection + parameters. """ from django.core.exceptions import ImproperlyConfigured - # Get database wrapper db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db + db_alias = db.alias if hasattr(db, "alias") else None - # Set vendor (always safe, no connection access) - span.set_data(SPANDATA.DB_SYSTEM, db.vendor) - - # Get the database alias (supports .using() queries) - db_alias = db.alias + if hasattr(db, "vendor"): + span.set_data(SPANDATA.DB_SYSTEM, db.vendor) - # Method 1: Use pre-cached configuration (FASTEST, NO CONNECTION ACCESS) - cached_config = _cached_db_configs.get(db_alias) + # Use pre-cached configuration + cached_config = _cached_db_configs.get(db_alias) if db_alias else None if cached_config: if cached_config["db_name"]: span.set_data(SPANDATA.DB_NAME, cached_config["db_name"]) @@ -781,38 +763,28 @@ def _set_db_data(span, cursor_or_db): span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, cached_config["unix_socket"]) return - # Method 2: Dynamic fallback using get_connection_params() (NO CONNECTION ACCESS) + # Fallback to using get_connection_params() try: connection_params = db.get_connection_params() - # Extract database name db_name = connection_params.get("dbname") or connection_params.get("database") if db_name: span.set_data(SPANDATA.DB_NAME, db_name) - # Extract host host = connection_params.get("host") if host: span.set_data(SPANDATA.SERVER_ADDRESS, host) - # Extract port port = connection_params.get("port") if port: span.set_data(SPANDATA.SERVER_PORT, str(port)) - # Extract unix socket unix_socket = connection_params.get("unix_socket") if unix_socket: span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, unix_socket) except (KeyError, ImproperlyConfigured, AttributeError) as e: - # Specific exceptions that get_connection_params() can raise: - # - KeyError: Missing required database settings (most common) - # - ImproperlyConfigured: Django configuration problems - # - AttributeError: Missing methods on database wrapper logger.debug("Failed to get database connection params for %s: %s", db_alias, e) - # Skip database metadata rather than risk connection issues - pass def add_template_context_repr_sequence(): From cc939a16791052a8c1b6e75144f3bba54101f775 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 11:35:23 +0200 Subject: [PATCH 05/19] updated fallback --- sentry_sdk/integrations/django/__init__.py | 41 ++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 03a326b87e..2b95b57f01 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -1,6 +1,7 @@ import sys import threading import weakref +import inspect from importlib import import_module import sentry_sdk @@ -763,24 +764,52 @@ def _set_db_data(span, cursor_or_db): span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, cached_config["unix_socket"]) return - # Fallback to using get_connection_params() + # Fallback to using the database connection to get the data. + # This is the edge case where db configuration is not in Django's `DATABASES` setting. try: - connection_params = db.get_connection_params() + # Some custom backends override `__getattr__`, making it look like `cursor_or_db` + # actually has a `connection` and the `connection` has a `get_dsn_parameters` + # attribute, only to throw an error once you actually want to call it. + # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable + # function. + is_psycopg2 = ( + hasattr(cursor_or_db, "connection") + and hasattr(cursor_or_db.connection, "get_dsn_parameters") + and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) + ) + if is_psycopg2: + connection_params = cursor_or_db.connection.get_dsn_parameters() + else: + try: + # psycopg3, only extract needed params as get_parameters + # can be slow because of the additional logic to filter out default + # values + connection_params = { + "dbname": cursor_or_db.connection.info.dbname, + "port": cursor_or_db.connection.info.port, + } + # PGhost returns host or base dir of UNIX socket as an absolute path + # starting with /, use it only when it contains host + host = cursor_or_db.connection.info.host + if host and not host.startswith("/"): + connection_params["host"] = host + except Exception: + connection_params = db.get_connection_params() db_name = connection_params.get("dbname") or connection_params.get("database") - if db_name: + if db_name is not None: span.set_data(SPANDATA.DB_NAME, db_name) host = connection_params.get("host") - if host: + if host is not None: span.set_data(SPANDATA.SERVER_ADDRESS, host) port = connection_params.get("port") - if port: + if port is not None: span.set_data(SPANDATA.SERVER_PORT, str(port)) unix_socket = connection_params.get("unix_socket") - if unix_socket: + if unix_socket is not None: span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, unix_socket) except (KeyError, ImproperlyConfigured, AttributeError) as e: From 43d2b7548ae42fb0ccc982a8175cd2642b22855a Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 12:41:33 +0200 Subject: [PATCH 06/19] Better fallback --- sentry_sdk/integrations/django/__init__.py | 94 +++++++++++++++------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 2b95b57f01..74d7065b8f 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -762,25 +762,58 @@ def _set_db_data(span, cursor_or_db): span.set_data(SPANDATA.SERVER_PORT, str(cached_config["port"])) if cached_config["unix_socket"]: span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, cached_config["unix_socket"]) - return + return # Success - exit early - # Fallback to using the database connection to get the data. + # Fallback to dynamic database metadata collection. # This is the edge case where db configuration is not in Django's `DATABASES` setting. try: - # Some custom backends override `__getattr__`, making it look like `cursor_or_db` - # actually has a `connection` and the `connection` has a `get_dsn_parameters` - # attribute, only to throw an error once you actually want to call it. - # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable - # function. - is_psycopg2 = ( - hasattr(cursor_or_db, "connection") - and hasattr(cursor_or_db.connection, "get_dsn_parameters") - and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) + # Method 1: Try db.get_connection_params() first (NO CONNECTION ACCESS) + logger.debug( + "Cached db connection config retrieval failed for %s. Trying db.get_connection_params().", + db_alias, ) - if is_psycopg2: - connection_params = cursor_or_db.connection.get_dsn_parameters() - else: - try: + try: + connection_params = db.get_connection_params() + + db_name = connection_params.get("dbname") or connection_params.get( + "database" + ) + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + host = connection_params.get("host") + if host is not None: + span.set_data(SPANDATA.SERVER_ADDRESS, host) + + port = connection_params.get("port") + if port is not None: + span.set_data(SPANDATA.SERVER_PORT, str(port)) + + unix_socket = connection_params.get("unix_socket") + if unix_socket is not None: + span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, unix_socket) + return # Success - exit early to avoid connection access + + except (KeyError, ImproperlyConfigured, AttributeError): + # Method 2: Last resort - direct connection access (CONNECTION POOL RISK) + logger.debug( + "db.get_connection_params() failed for %s, trying direct connection access", + db_alias, + ) + + # Some custom backends override `__getattr__`, making it look like `cursor_or_db` + # actually has a `connection` and the `connection` has a `get_dsn_parameters` + # attribute, only to throw an error once you actually want to call it. + # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable + # function. + is_psycopg2 = ( + hasattr(cursor_or_db, "connection") + and hasattr(cursor_or_db.connection, "get_dsn_parameters") + and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) + ) + if is_psycopg2: + connection_params = cursor_or_db.connection.get_dsn_parameters() + else: # psycopg3, only extract needed params as get_parameters # can be slow because of the additional logic to filter out default # values @@ -793,27 +826,28 @@ def _set_db_data(span, cursor_or_db): host = cursor_or_db.connection.info.host if host and not host.startswith("/"): connection_params["host"] = host - except Exception: - connection_params = db.get_connection_params() - db_name = connection_params.get("dbname") or connection_params.get("database") - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) + db_name = connection_params.get("dbname") or connection_params.get( + "database" + ) + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) - host = connection_params.get("host") - if host is not None: - span.set_data(SPANDATA.SERVER_ADDRESS, host) + host = connection_params.get("host") + if host is not None: + span.set_data(SPANDATA.SERVER_ADDRESS, host) - port = connection_params.get("port") - if port is not None: - span.set_data(SPANDATA.SERVER_PORT, str(port)) + port = connection_params.get("port") + if port is not None: + span.set_data(SPANDATA.SERVER_PORT, str(port)) - unix_socket = connection_params.get("unix_socket") - if unix_socket is not None: - span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, unix_socket) + unix_socket = connection_params.get("unix_socket") + if unix_socket is not None: + span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, unix_socket) - except (KeyError, ImproperlyConfigured, AttributeError) as e: + except Exception as e: logger.debug("Failed to get database connection params for %s: %s", db_alias, e) + # Skip database metadata rather than risk further connection issues def add_template_context_repr_sequence(): From fba0b46dc5ca7234054ffb127fd615ac4977eaa2 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 12:53:37 +0200 Subject: [PATCH 07/19] typing --- sentry_sdk/tracing_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 50b27081ed..d535f536c4 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -355,11 +355,12 @@ def _format_sql(cursor, sql): real_sql = cursor.mogrify(sql) if isinstance(real_sql, bytes): # Use UTF-8 as default, with latin1 fallback for edge cases + bytes_sql = real_sql # make pypy happy (type narrowing to bytes) try: - real_sql = real_sql.decode("utf-8") + real_sql = bytes_sql.decode("utf-8") except UnicodeDecodeError: # If UTF-8 fails, try latin1 as fallback - real_sql = real_sql.decode("latin1", errors="replace") + real_sql = bytes_sql.decode("latin1", errors="replace") except Exception: real_sql = None From 7e19dc248d3a4ffe75aa13f526057dcb8c324cfa Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 12:55:02 +0200 Subject: [PATCH 08/19] . --- sentry_sdk/integrations/django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 74d7065b8f..ea284051eb 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -1,7 +1,7 @@ +import inspect import sys import threading import weakref -import inspect from importlib import import_module import sentry_sdk From 40884d1841cd86a70f36f18ea0077b2764b73ed5 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 13:06:26 +0200 Subject: [PATCH 09/19] make thread safe --- sentry_sdk/integrations/django/__init__.py | 59 ++++++++++++++-------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index ea284051eb..9706efbfc0 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -30,7 +30,10 @@ RequestExtractor, ) +# Global cache for database configurations to avoid connection pool interference _cached_db_configs = {} # type: Dict[str, Dict[str, Any]] +_cache_initialized = False # type: bool +_cache_lock = threading.Lock() try: from django import VERSION as DJANGO_VERSION @@ -621,35 +624,47 @@ def _cache_database_configurations(): """ Cache database configurations from Django settings to avoid connection pool interference. """ - global _cached_db_configs + global _cached_db_configs, _cache_initialized - try: - from django.conf import settings - from django.db import connections + # Fast path: if already initialized, return immediately + if _cache_initialized: + return - for alias, db_config in settings.DATABASES.items(): - if not db_config: # Skip empty default configs - continue + # Slow path: acquire lock and check again (double-checked locking) + with _cache_lock: + if _cache_initialized: + return - with capture_internal_exceptions(): - _cached_db_configs[alias] = { - "db_name": db_config.get("NAME"), - "host": db_config.get("HOST", "localhost"), - "port": db_config.get("PORT"), - "unix_socket": db_config.get("OPTIONS", {}).get("unix_socket"), - "engine": db_config.get("ENGINE"), - } + try: + from django.conf import settings + from django.db import connections - db_wrapper = connections.get(alias) - if db_wrapper is None: + for alias, db_config in settings.DATABASES.items(): + if not db_config: # Skip empty default configs continue - if hasattr(db_wrapper, "vendor"): - _cached_db_configs[alias]["vendor"] = db_wrapper.vendor + with capture_internal_exceptions(): + _cached_db_configs[alias] = { + "db_name": db_config.get("NAME"), + "host": db_config.get("HOST", "localhost"), + "port": db_config.get("PORT"), + "unix_socket": db_config.get("OPTIONS", {}).get("unix_socket"), + "engine": db_config.get("ENGINE"), + } - except Exception as e: - logger.debug("Failed to cache database configurations: %s", e) - _cached_db_configs = {} + db_wrapper = connections.get(alias) + if db_wrapper is None: + continue + + if hasattr(db_wrapper, "vendor"): + _cached_db_configs[alias]["vendor"] = db_wrapper.vendor + + # Mark as initialized only after successful completion + _cache_initialized = True + + except Exception as e: + logger.debug("Failed to cache database configurations: %s", e) + _cached_db_configs = {} def install_sql_hook(): From c2d372d2a80d15c405434e8c3c7ab257828b20ae Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 13:22:28 +0200 Subject: [PATCH 10/19] back --- sentry_sdk/tracing_utils.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 0e4ad79008..447a708d4d 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -354,13 +354,7 @@ def _format_sql(cursor, sql): if hasattr(cursor, "mogrify"): real_sql = cursor.mogrify(sql) if isinstance(real_sql, bytes): - # Use UTF-8 as default, with latin1 fallback for edge cases - bytes_sql = real_sql # make pypy happy (type narrowing to bytes) - try: - real_sql = bytes_sql.decode("utf-8") - except UnicodeDecodeError: - # If UTF-8 fails, try latin1 as fallback - real_sql = bytes_sql.decode("latin1", errors="replace") + real_sql = real_sql.decode(cursor.connection.encoding) except Exception: real_sql = None From 2c16abd64f6a5f22152187b158cbcb75cc9e9760 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 13:25:47 +0200 Subject: [PATCH 11/19] error handling --- sentry_sdk/integrations/django/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 9706efbfc0..3e2685b654 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -740,8 +740,10 @@ def connect(self): origin=DjangoIntegration.origin_db, ) as span: connection = real_connect(self) - _set_db_data(span, self) - return connection + with capture_internal_exceptions(): + _set_db_data(span, self) + + return connection CursorWrapper.execute = execute CursorWrapper.executemany = executemany From a29b06ea8299d3d51dd6e36b66998eb207a39f13 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 14:48:14 +0200 Subject: [PATCH 12/19] tests --- .../integrations/django/test_db_query_data.py | 565 ++++++++++++++++++ 1 file changed, 565 insertions(+) diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index 41ad9d5e1c..ad6c46964b 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -524,3 +524,568 @@ def test_db_span_origin_executemany(sentry_init, client, capture_events): assert event["contexts"]["trace"]["origin"] == "manual" assert event["spans"][0]["origin"] == "auto.db.django" + + +# Tests for _set_db_data function and its caching mechanism +@pytest.mark.forked +def test_set_db_data_with_cached_config(sentry_init): + """Test _set_db_data uses cached database configuration when available.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + from unittest.mock import Mock + + sentry_init(integrations=[DjangoIntegration()]) + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper + mock_db = Mock() + mock_db.alias = "test_db" + mock_db.vendor = "postgresql" + + # Mock cursor with db attribute + mock_cursor = Mock() + mock_cursor.db = mock_db + + # Set up cached configuration + _cached_db_configs["test_db"] = { + "db_name": "test_database", + "host": "localhost", + "port": 5432, + "unix_socket": None, + "engine": "django.db.backends.postgresql", + "vendor": "postgresql", + } + + # Call _set_db_data + _set_db_data(span, mock_cursor) + + # Verify span data was set correctly from cache + expected_calls = [ + mock.call("db.system", "postgresql"), + mock.call("db.name", "test_database"), + mock.call("server.address", "localhost"), + mock.call("server.port", "5432"), + ] + + for call in expected_calls: + assert call in span.set_data.call_args_list + + # Verify unix_socket was not set (it's None) + assert mock.call("server.socket.address", None) not in span.set_data.call_args_list + + +@pytest.mark.forked +def test_set_db_data_with_cached_config_unix_socket(sentry_init): + """Test _set_db_data handles unix socket from cached config.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from unittest.mock import Mock + + sentry_init(integrations=[DjangoIntegration()]) + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper without vendor + mock_db = Mock() + mock_db.alias = "test_db" + del mock_db.vendor # Remove vendor attribute + + # Mock cursor with db attribute + mock_cursor = Mock() + mock_cursor.db = mock_db + + # Set up cached configuration with unix socket + _cached_db_configs["test_db"] = { + "db_name": "test_database", + "host": None, + "port": None, + "unix_socket": "/tmp/postgres.sock", + "engine": "django.db.backends.postgresql", + "vendor": "postgresql", + } + + # Call _set_db_data + _set_db_data(span, mock_cursor) + + # Verify span data was set correctly from cache + expected_calls = [ + mock.call("db.name", "test_database"), + mock.call("server.socket.address", "/tmp/postgres.sock"), + ] + + for call in expected_calls: + assert call in span.set_data.call_args_list + + # Verify host and port were not set (they're None) + assert mock.call("server.address", None) not in span.set_data.call_args_list + assert mock.call("server.port", None) not in span.set_data.call_args_list + + +@pytest.mark.forked +def test_set_db_data_fallback_to_connection_params(sentry_init): + """Test _set_db_data falls back to db.get_connection_params() when cache misses.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from unittest.mock import Mock, patch + + sentry_init(integrations=[DjangoIntegration()]) + + # Clear cache to force fallback + _cached_db_configs.clear() + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper + mock_db = Mock() + mock_db.alias = "uncached_db" + mock_db.vendor = "mysql" + mock_db.get_connection_params.return_value = { + "database": "fallback_db", + "host": "mysql.example.com", + "port": 3306, + "unix_socket": None, + } + + # Mock cursor with db attribute + mock_cursor = Mock() + mock_cursor.db = mock_db + + # Call _set_db_data + with patch("sentry_sdk.integrations.django.logger") as mock_logger: + _set_db_data(span, mock_cursor) + + # Verify fallback was used + mock_db.get_connection_params.assert_called_once() + mock_logger.debug.assert_called_with( + "Cached db connection config retrieval failed for %s. Trying db.get_connection_params().", + "uncached_db", + ) + + # Verify span data was set correctly from connection params + expected_calls = [ + mock.call("db.system", "mysql"), + mock.call("db.name", "fallback_db"), + mock.call("server.address", "mysql.example.com"), + mock.call("server.port", "3306"), + ] + + for call in expected_calls: + assert call in span.set_data.call_args_list + + +@pytest.mark.forked +def test_set_db_data_fallback_to_connection_params_with_dbname(sentry_init): + """Test _set_db_data handles 'dbname' key in connection params.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from unittest.mock import Mock + + sentry_init(integrations=[DjangoIntegration()]) + + # Clear cache to force fallback + _cached_db_configs.clear() + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper + mock_db = Mock() + mock_db.alias = "postgres_db" + mock_db.vendor = "postgresql" + mock_db.get_connection_params.return_value = { + "dbname": "postgres_fallback", # PostgreSQL uses 'dbname' instead of 'database' + "host": "postgres.example.com", + "port": 5432, + "unix_socket": "/var/run/postgresql/.s.PGSQL.5432", + } + + # Mock cursor with db attribute + mock_cursor = Mock() + mock_cursor.db = mock_db + + # Call _set_db_data + _set_db_data(span, mock_cursor) + + # Verify span data was set correctly, preferring 'dbname' over 'database' + expected_calls = [ + mock.call("db.system", "postgresql"), + mock.call("db.name", "postgres_fallback"), + mock.call("server.address", "postgres.example.com"), + mock.call("server.port", "5432"), + mock.call("server.socket.address", "/var/run/postgresql/.s.PGSQL.5432"), + ] + + for call in expected_calls: + assert call in span.set_data.call_args_list + + +@pytest.mark.forked +def test_set_db_data_fallback_to_direct_connection_psycopg2(sentry_init): + """Test _set_db_data falls back to direct connection access for psycopg2.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from django.core.exceptions import ImproperlyConfigured + from unittest.mock import Mock, patch + + sentry_init(integrations=[DjangoIntegration()]) + + # Clear cache to force fallback + _cached_db_configs.clear() + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper that fails get_connection_params + mock_db = Mock() + mock_db.alias = "direct_db" + mock_db.vendor = "postgresql" + mock_db.get_connection_params.side_effect = ImproperlyConfigured("Config error") + + # Mock psycopg2-style connection + mock_connection = Mock() + mock_connection.get_dsn_parameters.return_value = { + "dbname": "direct_access_db", + "host": "direct.example.com", + "port": "5432", + } + + # Mock cursor with psycopg2-style connection + mock_cursor = Mock() + mock_cursor.db = mock_db + mock_cursor.connection = mock_connection + + # Call _set_db_data + with patch("sentry_sdk.integrations.django.logger") as mock_logger, patch( + "sentry_sdk.integrations.django.inspect.isroutine", return_value=True + ): + _set_db_data(span, mock_cursor) + + # Verify both fallbacks were attempted + mock_db.get_connection_params.assert_called_once() + mock_connection.get_dsn_parameters.assert_called_once() + + # Verify logging + assert mock_logger.debug.call_count == 2 + mock_logger.debug.assert_any_call( + "Cached db connection config retrieval failed for %s. Trying db.get_connection_params().", + "direct_db", + ) + mock_logger.debug.assert_any_call( + "db.get_connection_params() failed for %s, trying direct connection access", + "direct_db", + ) + + # Verify span data was set from direct connection access + expected_calls = [ + mock.call("db.system", "postgresql"), + mock.call("db.name", "direct_access_db"), + mock.call("server.address", "direct.example.com"), + mock.call("server.port", "5432"), + ] + + for call in expected_calls: + assert call in span.set_data.call_args_list + + +@pytest.mark.forked +def test_set_db_data_fallback_to_direct_connection_psycopg3(sentry_init): + """Test _set_db_data falls back to direct connection access for psycopg3.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from unittest.mock import Mock, patch + + sentry_init(integrations=[DjangoIntegration()]) + + # Clear cache to force fallback + _cached_db_configs.clear() + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper that fails get_connection_params + mock_db = Mock() + mock_db.alias = "psycopg3_db" + mock_db.vendor = "postgresql" + mock_db.get_connection_params.side_effect = AttributeError( + "No get_connection_params" + ) + + # Mock psycopg3-style connection (no get_dsn_parameters method) + mock_connection_info = Mock() + mock_connection_info.dbname = "psycopg3_db" + mock_connection_info.port = 5433 + mock_connection_info.host = "psycopg3.example.com" # Non-Unix socket host + + mock_connection = Mock() + mock_connection.info = mock_connection_info + # Remove get_dsn_parameters to simulate psycopg3 + del mock_connection.get_dsn_parameters + + # Mock cursor with psycopg3-style connection + mock_cursor = Mock() + mock_cursor.db = mock_db + mock_cursor.connection = mock_connection + + # Call _set_db_data + with patch("sentry_sdk.integrations.django.inspect.isroutine", return_value=False): + _set_db_data(span, mock_cursor) + + # Verify span data was set from psycopg3 connection info + expected_calls = [ + mock.call("db.system", "postgresql"), + mock.call("db.name", "psycopg3_db"), + mock.call("server.address", "psycopg3.example.com"), + mock.call("server.port", "5433"), + ] + + for call in expected_calls: + assert call in span.set_data.call_args_list + + +@pytest.mark.forked +def test_set_db_data_psycopg3_unix_socket_filtered(sentry_init): + """Test _set_db_data filters out Unix socket paths in psycopg3.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from unittest.mock import Mock, patch + + sentry_init(integrations=[DjangoIntegration()]) + + # Clear cache to force fallback + _cached_db_configs.clear() + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper that fails get_connection_params + mock_db = Mock() + mock_db.alias = "unix_socket_db" + mock_db.vendor = "postgresql" + mock_db.get_connection_params.side_effect = KeyError("Missing key") + + # Mock psycopg3-style connection with Unix socket path + mock_connection_info = Mock() + mock_connection_info.dbname = "unix_socket_db" + mock_connection_info.port = 5432 + mock_connection_info.host = ( + "/var/run/postgresql" # Unix socket path starting with / + ) + + mock_connection = Mock() + mock_connection.info = mock_connection_info + del mock_connection.get_dsn_parameters + + # Mock cursor with psycopg3-style connection + mock_cursor = Mock() + mock_cursor.db = mock_db + mock_cursor.connection = mock_connection + + # Call _set_db_data + with patch("sentry_sdk.integrations.django.inspect.isroutine", return_value=False): + _set_db_data(span, mock_cursor) + + # Verify span data was set but host was filtered out (Unix socket) + expected_calls = [ + mock.call("db.system", "postgresql"), + mock.call("db.name", "unix_socket_db"), + mock.call("server.port", "5432"), + ] + + for call in expected_calls: + assert call in span.set_data.call_args_list + + # Verify host was NOT set (Unix socket path filtered out) + assert ( + mock.call("server.address", "/var/run/postgresql") + not in span.set_data.call_args_list + ) + + +@pytest.mark.forked +def test_set_db_data_no_alias_db(sentry_init): + """Test _set_db_data handles database without alias attribute.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from unittest.mock import Mock + + sentry_init(integrations=[DjangoIntegration()]) + + # Clear cache + _cached_db_configs.clear() + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper without alias + mock_db = Mock() + del mock_db.alias # Remove alias attribute + mock_db.vendor = "sqlite" + mock_db.get_connection_params.return_value = {"database": "test.db"} + + # Mock cursor with db attribute + mock_cursor = Mock() + mock_cursor.db = mock_db + + # Call _set_db_data + _set_db_data(span, mock_cursor) + + # Verify it worked despite no alias + expected_calls = [ + mock.call("db.system", "sqlite"), + mock.call("db.name", "test.db"), + ] + + for call in expected_calls: + assert call in span.set_data.call_args_list + + +@pytest.mark.forked +def test_set_db_data_direct_db_object(sentry_init): + """Test _set_db_data handles direct database object (not cursor).""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from unittest.mock import Mock + + sentry_init(integrations=[DjangoIntegration()]) + + # Clear cache + _cached_db_configs.clear() + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper passed directly (not cursor.db) - use configure_mock for proper setup + mock_db = Mock() + mock_db.configure_mock(alias="direct_db", vendor="oracle") + mock_db.get_connection_params.return_value = { + "database": "orcl", + "host": "oracle.example.com", + "port": 1521, + } + + # Call _set_db_data with db directly (no .db attribute) + _set_db_data(span, mock_db) + + # Verify it handled direct db object correctly by checking that span.set_data was called + assert span.set_data.called + call_args_list = span.set_data.call_args_list + assert len(call_args_list) >= 1 # At least db.system should be called + + # Extract the keys that were called + call_keys = [call[0][0] for call in call_args_list] + + # Verify that db.system was called (this comes from mock_db.vendor) + assert "db.system" in call_keys + + +@pytest.mark.forked +def test_set_db_data_exception_handling(sentry_init): + """Test _set_db_data handles exceptions gracefully.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from unittest.mock import Mock, patch + + sentry_init(integrations=[DjangoIntegration()]) + + # Clear cache + _cached_db_configs.clear() + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper that raises exception + mock_db = Mock() + mock_db.alias = "error_db" + mock_db.vendor = "postgresql" + mock_db.get_connection_params.side_effect = Exception("Database error") + + # Mock cursor that also raises exception on connection access + mock_cursor = Mock() + mock_cursor.db = mock_db + mock_cursor.connection.get_dsn_parameters.side_effect = Exception( + "Connection error" + ) + + # Call _set_db_data - should not raise exception + with patch("sentry_sdk.integrations.django.logger") as mock_logger: + _set_db_data(span, mock_cursor) + + # Verify only vendor was set (from initial db.vendor access) + expected_calls = [ + mock.call("db.system", "postgresql"), + ] + + for call in expected_calls: + assert call in span.set_data.call_args_list + + # Verify error was logged + mock_logger.debug.assert_called_with( + "Failed to get database connection params for %s: %s", "error_db", mock.ANY + ) + + +@pytest.mark.forked +def test_set_db_data_empty_cached_values(sentry_init): + """Test _set_db_data handles empty/None values in cached config.""" + from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs + + from unittest.mock import Mock + + sentry_init(integrations=[DjangoIntegration()]) + + # Mock span + span = Mock() + span.set_data = Mock() + + # Mock database wrapper + mock_db = Mock() + mock_db.alias = "empty_config_db" + mock_db.vendor = "postgresql" + + # Mock cursor with db attribute + mock_cursor = Mock() + mock_cursor.db = mock_db + + # Set up cached configuration with empty/None values + _cached_db_configs["empty_config_db"] = { + "db_name": None, # Should not be set + "host": "", # Should not be set (empty string) + "port": None, # Should not be set + "unix_socket": None, # Should not be set + "engine": "django.db.backends.postgresql", + "vendor": "postgresql", + } + + # Call _set_db_data + _set_db_data(span, mock_cursor) + + # Verify only vendor was set (other values are empty/None) + expected_calls = [ + mock.call("db.system", "postgresql"), + ] + + assert span.set_data.call_args_list == expected_calls + + # Verify empty/None values were not set + not_expected_calls = [ + mock.call("db.name", None), + mock.call("server.address", ""), + mock.call("server.port", None), + mock.call("server.socket.address", None), + ] + + for call in not_expected_calls: + assert call not in span.set_data.call_args_list From 06b0703867ccd8609ba0c1ccee191afd34e96526 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 15:04:02 +0200 Subject: [PATCH 13/19] cleanup --- .../integrations/django/test_db_query_data.py | 231 +++++------------- 1 file changed, 64 insertions(+), 167 deletions(-) diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index ad6c46964b..0b7bb4304d 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -5,6 +5,7 @@ from unittest import mock from django import VERSION as DJANGO_VERSION +from django.core.exceptions import ImproperlyConfigured from django.db import connections try: @@ -16,7 +17,11 @@ from sentry_sdk import start_transaction from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.django import ( + DjangoIntegration, + _set_db_data, + _cached_db_configs, +) from sentry_sdk.tracing_utils import record_sql_queries from tests.conftest import unpack_werkzeug_response @@ -526,29 +531,20 @@ def test_db_span_origin_executemany(sentry_init, client, capture_events): assert event["spans"][0]["origin"] == "auto.db.django" -# Tests for _set_db_data function and its caching mechanism -@pytest.mark.forked def test_set_db_data_with_cached_config(sentry_init): """Test _set_db_data uses cached database configuration when available.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - from unittest.mock import Mock - sentry_init(integrations=[DjangoIntegration()]) - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper - mock_db = Mock() + mock_db = mock.Mock() mock_db.alias = "test_db" mock_db.vendor = "postgresql" - # Mock cursor with db attribute - mock_cursor = Mock() + mock_cursor = mock.Mock() mock_cursor.db = mock_db - # Set up cached configuration _cached_db_configs["test_db"] = { "db_name": "test_database", "host": "localhost", @@ -558,7 +554,6 @@ def test_set_db_data_with_cached_config(sentry_init): "vendor": "postgresql", } - # Call _set_db_data _set_db_data(span, mock_cursor) # Verify span data was set correctly from cache @@ -576,29 +571,20 @@ def test_set_db_data_with_cached_config(sentry_init): assert mock.call("server.socket.address", None) not in span.set_data.call_args_list -@pytest.mark.forked def test_set_db_data_with_cached_config_unix_socket(sentry_init): """Test _set_db_data handles unix socket from cached config.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from unittest.mock import Mock - sentry_init(integrations=[DjangoIntegration()]) - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper without vendor - mock_db = Mock() + mock_db = mock.Mock() mock_db.alias = "test_db" del mock_db.vendor # Remove vendor attribute - # Mock cursor with db attribute - mock_cursor = Mock() + mock_cursor = mock.Mock() mock_cursor.db = mock_db - # Set up cached configuration with unix socket _cached_db_configs["test_db"] = { "db_name": "test_database", "host": None, @@ -608,7 +594,6 @@ def test_set_db_data_with_cached_config_unix_socket(sentry_init): "vendor": "postgresql", } - # Call _set_db_data _set_db_data(span, mock_cursor) # Verify span data was set correctly from cache @@ -625,24 +610,16 @@ def test_set_db_data_with_cached_config_unix_socket(sentry_init): assert mock.call("server.port", None) not in span.set_data.call_args_list -@pytest.mark.forked def test_set_db_data_fallback_to_connection_params(sentry_init): """Test _set_db_data falls back to db.get_connection_params() when cache misses.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from unittest.mock import Mock, patch - sentry_init(integrations=[DjangoIntegration()]) - # Clear cache to force fallback _cached_db_configs.clear() - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper - mock_db = Mock() + mock_db = mock.Mock() mock_db.alias = "uncached_db" mock_db.vendor = "mysql" mock_db.get_connection_params.return_value = { @@ -652,12 +629,10 @@ def test_set_db_data_fallback_to_connection_params(sentry_init): "unix_socket": None, } - # Mock cursor with db attribute - mock_cursor = Mock() + mock_cursor = mock.Mock() mock_cursor.db = mock_db - # Call _set_db_data - with patch("sentry_sdk.integrations.django.logger") as mock_logger: + with mock.patch("sentry_sdk.integrations.django.logger") as mock_logger: _set_db_data(span, mock_cursor) # Verify fallback was used @@ -679,24 +654,16 @@ def test_set_db_data_fallback_to_connection_params(sentry_init): assert call in span.set_data.call_args_list -@pytest.mark.forked def test_set_db_data_fallback_to_connection_params_with_dbname(sentry_init): """Test _set_db_data handles 'dbname' key in connection params.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from unittest.mock import Mock - sentry_init(integrations=[DjangoIntegration()]) - # Clear cache to force fallback _cached_db_configs.clear() - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper - mock_db = Mock() + mock_db = mock.Mock() mock_db.alias = "postgres_db" mock_db.vendor = "postgresql" mock_db.get_connection_params.return_value = { @@ -706,11 +673,9 @@ def test_set_db_data_fallback_to_connection_params_with_dbname(sentry_init): "unix_socket": "/var/run/postgresql/.s.PGSQL.5432", } - # Mock cursor with db attribute - mock_cursor = Mock() + mock_cursor = mock.Mock() mock_cursor.db = mock_db - # Call _set_db_data _set_db_data(span, mock_cursor) # Verify span data was set correctly, preferring 'dbname' over 'database' @@ -726,44 +691,32 @@ def test_set_db_data_fallback_to_connection_params_with_dbname(sentry_init): assert call in span.set_data.call_args_list -@pytest.mark.forked def test_set_db_data_fallback_to_direct_connection_psycopg2(sentry_init): """Test _set_db_data falls back to direct connection access for psycopg2.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from django.core.exceptions import ImproperlyConfigured - from unittest.mock import Mock, patch - sentry_init(integrations=[DjangoIntegration()]) - # Clear cache to force fallback _cached_db_configs.clear() - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper that fails get_connection_params - mock_db = Mock() + mock_db = mock.Mock() mock_db.alias = "direct_db" mock_db.vendor = "postgresql" mock_db.get_connection_params.side_effect = ImproperlyConfigured("Config error") - # Mock psycopg2-style connection - mock_connection = Mock() + mock_connection = mock.Mock() mock_connection.get_dsn_parameters.return_value = { "dbname": "direct_access_db", "host": "direct.example.com", "port": "5432", } - # Mock cursor with psycopg2-style connection - mock_cursor = Mock() + mock_cursor = mock.Mock() mock_cursor.db = mock_db mock_cursor.connection = mock_connection - # Call _set_db_data - with patch("sentry_sdk.integrations.django.logger") as mock_logger, patch( + with mock.patch("sentry_sdk.integrations.django.logger") as mock_logger, mock.patch( "sentry_sdk.integrations.django.inspect.isroutine", return_value=True ): _set_db_data(span, mock_cursor) @@ -795,48 +748,40 @@ def test_set_db_data_fallback_to_direct_connection_psycopg2(sentry_init): assert call in span.set_data.call_args_list -@pytest.mark.forked def test_set_db_data_fallback_to_direct_connection_psycopg3(sentry_init): """Test _set_db_data falls back to direct connection access for psycopg3.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from unittest.mock import Mock, patch - sentry_init(integrations=[DjangoIntegration()]) - # Clear cache to force fallback _cached_db_configs.clear() - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper that fails get_connection_params - mock_db = Mock() + mock_db = mock.Mock() mock_db.alias = "psycopg3_db" mock_db.vendor = "postgresql" mock_db.get_connection_params.side_effect = AttributeError( "No get_connection_params" ) - # Mock psycopg3-style connection (no get_dsn_parameters method) - mock_connection_info = Mock() + mock_connection_info = mock.Mock() mock_connection_info.dbname = "psycopg3_db" mock_connection_info.port = 5433 mock_connection_info.host = "psycopg3.example.com" # Non-Unix socket host - mock_connection = Mock() + mock_connection = mock.Mock() mock_connection.info = mock_connection_info # Remove get_dsn_parameters to simulate psycopg3 del mock_connection.get_dsn_parameters - # Mock cursor with psycopg3-style connection - mock_cursor = Mock() + # mock.Mock cursor with psycopg3-style connection + mock_cursor = mock.Mock() mock_cursor.db = mock_db mock_cursor.connection = mock_connection - # Call _set_db_data - with patch("sentry_sdk.integrations.django.inspect.isroutine", return_value=False): + with mock.patch( + "sentry_sdk.integrations.django.inspect.isroutine", return_value=False + ): _set_db_data(span, mock_cursor) # Verify span data was set from psycopg3 connection info @@ -851,47 +796,38 @@ def test_set_db_data_fallback_to_direct_connection_psycopg3(sentry_init): assert call in span.set_data.call_args_list -@pytest.mark.forked def test_set_db_data_psycopg3_unix_socket_filtered(sentry_init): """Test _set_db_data filters out Unix socket paths in psycopg3.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from unittest.mock import Mock, patch - sentry_init(integrations=[DjangoIntegration()]) - # Clear cache to force fallback _cached_db_configs.clear() - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper that fails get_connection_params - mock_db = Mock() + mock_db = mock.Mock() mock_db.alias = "unix_socket_db" mock_db.vendor = "postgresql" mock_db.get_connection_params.side_effect = KeyError("Missing key") - # Mock psycopg3-style connection with Unix socket path - mock_connection_info = Mock() + mock_connection_info = mock.Mock() mock_connection_info.dbname = "unix_socket_db" mock_connection_info.port = 5432 mock_connection_info.host = ( "/var/run/postgresql" # Unix socket path starting with / ) - mock_connection = Mock() + mock_connection = mock.Mock() mock_connection.info = mock_connection_info del mock_connection.get_dsn_parameters - # Mock cursor with psycopg3-style connection - mock_cursor = Mock() + mock_cursor = mock.Mock() mock_cursor.db = mock_db mock_cursor.connection = mock_connection - # Call _set_db_data - with patch("sentry_sdk.integrations.django.inspect.isroutine", return_value=False): + with mock.patch( + "sentry_sdk.integrations.django.inspect.isroutine", return_value=False + ): _set_db_data(span, mock_cursor) # Verify span data was set but host was filtered out (Unix socket) @@ -911,33 +847,24 @@ def test_set_db_data_psycopg3_unix_socket_filtered(sentry_init): ) -@pytest.mark.forked def test_set_db_data_no_alias_db(sentry_init): """Test _set_db_data handles database without alias attribute.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from unittest.mock import Mock - sentry_init(integrations=[DjangoIntegration()]) # Clear cache _cached_db_configs.clear() - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper without alias - mock_db = Mock() + mock_db = mock.Mock() del mock_db.alias # Remove alias attribute mock_db.vendor = "sqlite" mock_db.get_connection_params.return_value = {"database": "test.db"} - # Mock cursor with db attribute - mock_cursor = Mock() + mock_cursor = mock.Mock() mock_cursor.db = mock_db - # Call _set_db_data _set_db_data(span, mock_cursor) # Verify it worked despite no alias @@ -950,24 +877,17 @@ def test_set_db_data_no_alias_db(sentry_init): assert call in span.set_data.call_args_list -@pytest.mark.forked def test_set_db_data_direct_db_object(sentry_init): """Test _set_db_data handles direct database object (not cursor).""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from unittest.mock import Mock - sentry_init(integrations=[DjangoIntegration()]) # Clear cache _cached_db_configs.clear() - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper passed directly (not cursor.db) - use configure_mock for proper setup - mock_db = Mock() + mock_db = mock.Mock() mock_db.configure_mock(alias="direct_db", vendor="oracle") mock_db.get_connection_params.return_value = { "database": "orcl", @@ -975,7 +895,6 @@ def test_set_db_data_direct_db_object(sentry_init): "port": 1521, } - # Call _set_db_data with db directly (no .db attribute) _set_db_data(span, mock_db) # Verify it handled direct db object correctly by checking that span.set_data was called @@ -986,41 +905,30 @@ def test_set_db_data_direct_db_object(sentry_init): # Extract the keys that were called call_keys = [call[0][0] for call in call_args_list] - # Verify that db.system was called (this comes from mock_db.vendor) assert "db.system" in call_keys -@pytest.mark.forked def test_set_db_data_exception_handling(sentry_init): """Test _set_db_data handles exceptions gracefully.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from unittest.mock import Mock, patch - sentry_init(integrations=[DjangoIntegration()]) - # Clear cache _cached_db_configs.clear() - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper that raises exception - mock_db = Mock() + mock_db = mock.Mock() mock_db.alias = "error_db" mock_db.vendor = "postgresql" mock_db.get_connection_params.side_effect = Exception("Database error") - # Mock cursor that also raises exception on connection access - mock_cursor = Mock() + mock_cursor = mock.Mock() mock_cursor.db = mock_db mock_cursor.connection.get_dsn_parameters.side_effect = Exception( "Connection error" ) - # Call _set_db_data - should not raise exception - with patch("sentry_sdk.integrations.django.logger") as mock_logger: + with mock.patch("sentry_sdk.integrations.django.logger") as mock_logger: _set_db_data(span, mock_cursor) # Verify only vendor was set (from initial db.vendor access) @@ -1031,35 +939,25 @@ def test_set_db_data_exception_handling(sentry_init): for call in expected_calls: assert call in span.set_data.call_args_list - # Verify error was logged mock_logger.debug.assert_called_with( "Failed to get database connection params for %s: %s", "error_db", mock.ANY ) -@pytest.mark.forked def test_set_db_data_empty_cached_values(sentry_init): """Test _set_db_data handles empty/None values in cached config.""" - from sentry_sdk.integrations.django import _set_db_data, _cached_db_configs - - from unittest.mock import Mock - sentry_init(integrations=[DjangoIntegration()]) - # Mock span - span = Mock() - span.set_data = Mock() + span = mock.Mock() + span.set_data = mock.Mock() - # Mock database wrapper - mock_db = Mock() + mock_db = mock.Mock() mock_db.alias = "empty_config_db" mock_db.vendor = "postgresql" - # Mock cursor with db attribute - mock_cursor = Mock() + mock_cursor = mock.Mock() mock_cursor.db = mock_db - # Set up cached configuration with empty/None values _cached_db_configs["empty_config_db"] = { "db_name": None, # Should not be set "host": "", # Should not be set (empty string) @@ -1069,7 +967,6 @@ def test_set_db_data_empty_cached_values(sentry_init): "vendor": "postgresql", } - # Call _set_db_data _set_db_data(span, mock_cursor) # Verify only vendor was set (other values are empty/None) From cb7e107a59c5c4d14d734b86014396af5f44554f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 15:31:21 +0200 Subject: [PATCH 14/19] tests --- sentry_sdk/integrations/django/__init__.py | 5 +- .../integrations/django/test_db_query_data.py | 154 ++++++++++++++++++ 2 files changed, 157 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 3e2685b654..756f3d6f4d 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -652,8 +652,9 @@ def _cache_database_configurations(): "engine": db_config.get("ENGINE"), } - db_wrapper = connections.get(alias) - if db_wrapper is None: + try: + db_wrapper = connections[alias] + except (KeyError, Exception): continue if hasattr(db_wrapper, "vendor"): diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index 0b7bb4304d..f26051b161 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -1,4 +1,5 @@ import os +import threading import pytest from datetime import datetime @@ -8,6 +9,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db import connections + try: from django.urls import reverse except ImportError: @@ -17,10 +19,12 @@ from sentry_sdk import start_transaction from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import django as django_integration from sentry_sdk.integrations.django import ( DjangoIntegration, _set_db_data, _cached_db_configs, + _cache_database_configurations, ) from sentry_sdk.tracing_utils import record_sql_queries @@ -986,3 +990,153 @@ def test_set_db_data_empty_cached_values(sentry_init): for call in not_expected_calls: assert call not in span.set_data.call_args_list + + +def test_cache_database_configurations_basic(sentry_init): + """Test _cache_database_configurations caches Django database settings.""" + sentry_init(integrations=[DjangoIntegration()]) + + _cached_db_configs.clear() + django_integration._cache_initialized = False + + _cache_database_configurations() + + # Verify cache was populated + assert django_integration._cache_initialized is True + assert len(_cached_db_configs) > 0 + + # Verify default database was cached + assert "default" in _cached_db_configs + default_config = _cached_db_configs["default"] + + # Check expected keys exist + expected_keys = ["db_name", "host", "port", "unix_socket", "engine"] + for key in expected_keys: + assert key in default_config + + # Verify the vendor was added from the database wrapper + if "vendor" in default_config: + assert isinstance(default_config["vendor"], str) + + +def test_cache_database_configurations_idempotent(sentry_init): + """Test _cache_database_configurations is idempotent and thread-safe.""" + sentry_init(integrations=[DjangoIntegration()]) + + _cached_db_configs.clear() + django_integration._cache_initialized = False + + _cache_database_configurations() + first_call_result = dict(_cached_db_configs) + first_call_initialized = django_integration._cache_initialized + + _cache_database_configurations() + second_call_result = dict(_cached_db_configs) + second_call_initialized = django_integration._cache_initialized + + # Verify idempotency + assert first_call_initialized is True + assert second_call_initialized is True + assert first_call_result == second_call_result + + +def test_cache_database_configurations_thread_safety(sentry_init): + """Test _cache_database_configurations is thread-safe.""" + sentry_init(integrations=[DjangoIntegration()]) + + _cached_db_configs.clear() + django_integration._cache_initialized = False + + results = [] + exceptions = [] + + def cache_in_thread(): + try: + _cache_database_configurations() + results.append(dict(_cached_db_configs)) + except Exception as e: + exceptions.append(e) + + threads = [] + for _ in range(5): + thread = threading.Thread(target=cache_in_thread) + threads.append(thread) + + for thread in threads: + thread.start() + + for thread in threads: + thread.join() + + assert len(exceptions) == 0, f"Exceptions occurred: {exceptions}" + + assert len(results) == 5 + first_result = results[0] + for result in results[1:]: + assert result == first_result + + assert django_integration._cache_initialized is True + + +def test_cache_database_configurations_with_custom_settings(sentry_init): + """Test _cache_database_configurations handles custom database settings.""" + sentry_init(integrations=[DjangoIntegration()]) + + # Mock custom database settings + with mock.patch("django.conf.settings") as mock_settings: + mock_settings.DATABASES = { + "custom_db": { + "NAME": "test_db", + "HOST": "db.example.com", + "PORT": 5432, + "ENGINE": "django.db.backends.postgresql", + "OPTIONS": {"unix_socket": "/tmp/postgres.sock"}, + }, + "empty_db": {}, # Should be skipped + } + + # Mock connections to avoid actual database access + with mock.patch("django.db.connections") as mock_connections: + mock_wrapper = mock.Mock() + mock_wrapper.vendor = "postgresql" + mock_connections.__getitem__.return_value = mock_wrapper + + # Reset cache and call function + _cached_db_configs.clear() + django_integration._cache_initialized = False + + _cache_database_configurations() + + # Verify custom database was cached correctly + assert "custom_db" in _cached_db_configs + custom_config = _cached_db_configs["custom_db"] + + assert custom_config["db_name"] == "test_db" + assert custom_config["host"] == "db.example.com" + assert custom_config["port"] == 5432 + assert custom_config["unix_socket"] == "/tmp/postgres.sock" + assert custom_config["engine"] == "django.db.backends.postgresql" + assert custom_config["vendor"] == "postgresql" + + # Verify empty database was skipped + assert "empty_db" not in _cached_db_configs + + +def test_cache_database_configurations_exception_handling(sentry_init): + """Test _cache_database_configurations handles exceptions gracefully.""" + sentry_init(integrations=[DjangoIntegration()]) + + # Mock settings to raise an exception + with mock.patch("django.conf.settings") as mock_settings: + mock_settings.DATABASES.items.side_effect = Exception("Settings error") + + # Reset cache and call function + _cached_db_configs.clear() + django_integration._cache_initialized = False + + # Should not raise an exception + _cache_database_configurations() + + # Verify cache was cleared on exception + assert _cached_db_configs == {} + assert django_integration._cache_initialized is False From 902a320fa17aafcb8bffb41f1f248f93bced467f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 16:30:25 +0200 Subject: [PATCH 15/19] cleanup --- sentry_sdk/integrations/django/__init__.py | 51 +++++++++---------- .../integrations/django/test_db_query_data.py | 10 ++-- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 756f3d6f4d..2d7bec24a2 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -626,11 +626,9 @@ def _cache_database_configurations(): """ global _cached_db_configs, _cache_initialized - # Fast path: if already initialized, return immediately if _cache_initialized: return - # Slow path: acquire lock and check again (double-checked locking) with _cache_lock: if _cache_initialized: return @@ -644,23 +642,24 @@ def _cache_database_configurations(): continue with capture_internal_exceptions(): + try: + db_wrapper = connections[alias] + except (KeyError, Exception): + db_wrapper = None + _cached_db_configs[alias] = { "db_name": db_config.get("NAME"), "host": db_config.get("HOST", "localhost"), "port": db_config.get("PORT"), "unix_socket": db_config.get("OPTIONS", {}).get("unix_socket"), "engine": db_config.get("ENGINE"), + "vendor": ( + db_wrapper.vendor + if db_wrapper and hasattr(db_wrapper, "vendor") + else None + ), } - try: - db_wrapper = connections[alias] - except (KeyError, Exception): - continue - - if hasattr(db_wrapper, "vendor"): - _cached_db_configs[alias]["vendor"] = db_wrapper.vendor - - # Mark as initialized only after successful completion _cache_initialized = True except Exception as e: @@ -772,22 +771,22 @@ def _set_db_data(span, cursor_or_db): # Use pre-cached configuration cached_config = _cached_db_configs.get(db_alias) if db_alias else None if cached_config: - if cached_config["db_name"]: + if cached_config.get("db_name"): span.set_data(SPANDATA.DB_NAME, cached_config["db_name"]) - if cached_config["host"]: + if cached_config.get("host"): span.set_data(SPANDATA.SERVER_ADDRESS, cached_config["host"]) - if cached_config["port"]: + if cached_config.get("port"): span.set_data(SPANDATA.SERVER_PORT, str(cached_config["port"])) - if cached_config["unix_socket"]: + if cached_config.get("unix_socket"): span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, cached_config["unix_socket"]) return # Success - exit early # Fallback to dynamic database metadata collection. # This is the edge case where db configuration is not in Django's `DATABASES` setting. try: - # Method 1: Try db.get_connection_params() first (NO CONNECTION ACCESS) + # Fallback 1: Try db.get_connection_params() first (NO CONNECTION ACCESS) logger.debug( - "Cached db connection config retrieval failed for %s. Trying db.get_connection_params().", + "Cached db connection params retrieval failed for %s. Trying db.get_connection_params().", db_alias, ) try: @@ -796,24 +795,24 @@ def _set_db_data(span, cursor_or_db): db_name = connection_params.get("dbname") or connection_params.get( "database" ) - if db_name is not None: + if db_name: span.set_data(SPANDATA.DB_NAME, db_name) host = connection_params.get("host") - if host is not None: + if host: span.set_data(SPANDATA.SERVER_ADDRESS, host) port = connection_params.get("port") - if port is not None: + if port: span.set_data(SPANDATA.SERVER_PORT, str(port)) unix_socket = connection_params.get("unix_socket") - if unix_socket is not None: + if unix_socket: span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, unix_socket) return # Success - exit early to avoid connection access except (KeyError, ImproperlyConfigured, AttributeError): - # Method 2: Last resort - direct connection access (CONNECTION POOL RISK) + # Fallback 2: Last resort - direct connection access (CONNECTION POOL RISK) logger.debug( "db.get_connection_params() failed for %s, trying direct connection access", db_alias, @@ -848,19 +847,19 @@ def _set_db_data(span, cursor_or_db): db_name = connection_params.get("dbname") or connection_params.get( "database" ) - if db_name is not None: + if db_name: span.set_data(SPANDATA.DB_NAME, db_name) host = connection_params.get("host") - if host is not None: + if host: span.set_data(SPANDATA.SERVER_ADDRESS, host) port = connection_params.get("port") - if port is not None: + if port: span.set_data(SPANDATA.SERVER_PORT, str(port)) unix_socket = connection_params.get("unix_socket") - if unix_socket is not None: + if unix_socket: span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, unix_socket) except Exception as e: diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index f26051b161..57de1a26a6 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -963,10 +963,10 @@ def test_set_db_data_empty_cached_values(sentry_init): mock_cursor.db = mock_db _cached_db_configs["empty_config_db"] = { - "db_name": None, # Should not be set - "host": "", # Should not be set (empty string) - "port": None, # Should not be set - "unix_socket": None, # Should not be set + "db_name": None, + "host": None, + "port": None, + "unix_socket": None, "engine": "django.db.backends.postgresql", "vendor": "postgresql", } @@ -983,7 +983,7 @@ def test_set_db_data_empty_cached_values(sentry_init): # Verify empty/None values were not set not_expected_calls = [ mock.call("db.name", None), - mock.call("server.address", ""), + mock.call("server.address", None), mock.call("server.port", None), mock.call("server.socket.address", None), ] From c3f4ab71e50d0e070444bd5e313921a44523394d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 16:43:33 +0200 Subject: [PATCH 16/19] typo --- tests/integrations/django/test_db_query_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index 57de1a26a6..2d17709b19 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -642,7 +642,7 @@ def test_set_db_data_fallback_to_connection_params(sentry_init): # Verify fallback was used mock_db.get_connection_params.assert_called_once() mock_logger.debug.assert_called_with( - "Cached db connection config retrieval failed for %s. Trying db.get_connection_params().", + "Cached db connection params retrieval failed for %s. Trying db.get_connection_params().", "uncached_db", ) @@ -732,7 +732,7 @@ def test_set_db_data_fallback_to_direct_connection_psycopg2(sentry_init): # Verify logging assert mock_logger.debug.call_count == 2 mock_logger.debug.assert_any_call( - "Cached db connection config retrieval failed for %s. Trying db.get_connection_params().", + "Cached db connection params retrieval failed for %s. Trying db.get_connection_params().", "direct_db", ) mock_logger.debug.assert_any_call( From 2ee6f4aba898e6122d0dd36c35d3422f905c8be3 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 17:09:50 +0200 Subject: [PATCH 17/19] fixes --- sentry_sdk/integrations/django/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 2d7bec24a2..eea4c1fa46 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -649,7 +649,7 @@ def _cache_database_configurations(): _cached_db_configs[alias] = { "db_name": db_config.get("NAME"), - "host": db_config.get("HOST", "localhost"), + "host": db_config.get("HOST"), "port": db_config.get("PORT"), "unix_socket": db_config.get("OPTIONS", {}).get("unix_socket"), "engine": db_config.get("ENGINE"), @@ -743,7 +743,7 @@ def connect(self): with capture_internal_exceptions(): _set_db_data(span, self) - return connection + return connection CursorWrapper.execute = execute CursorWrapper.executemany = executemany From 9439ba492a3165a525c03d734a6735f4406a7e46 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 17:26:34 +0200 Subject: [PATCH 18/19] vendor from cached config --- sentry_sdk/integrations/django/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index eea4c1fa46..f29f1d76c6 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -779,6 +779,9 @@ def _set_db_data(span, cursor_or_db): span.set_data(SPANDATA.SERVER_PORT, str(cached_config["port"])) if cached_config.get("unix_socket"): span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, cached_config["unix_socket"]) + if cached_config.get("vendor"): + span.set_data(SPANDATA.DB_SYSTEM, cached_config["vendor"]) + return # Success - exit early # Fallback to dynamic database metadata collection. From 9d46f9bdf66773b50dce76188751d150f2e8a7b7 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 7 Aug 2025 17:31:49 +0200 Subject: [PATCH 19/19] better --- sentry_sdk/integrations/django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index f29f1d76c6..86adcd18e5 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -779,7 +779,7 @@ def _set_db_data(span, cursor_or_db): span.set_data(SPANDATA.SERVER_PORT, str(cached_config["port"])) if cached_config.get("unix_socket"): span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, cached_config["unix_socket"]) - if cached_config.get("vendor"): + if cached_config.get("vendor") and span._data.get(SPANDATA.DB_SYSTEM) is None: span.set_data(SPANDATA.DB_SYSTEM, cached_config["vendor"]) return # Success - exit early