|
1 | | -import inspect |
2 | 1 | import sys |
3 | 2 | import threading |
4 | 3 | import weakref |
|
30 | 29 | RequestExtractor, |
31 | 30 | ) |
32 | 31 |
|
| 32 | +# Global cache for database configurations to avoid connection pool interference |
| 33 | +_cached_db_configs = {} # type: Dict[str, Dict[str, Any]] |
| 34 | + |
33 | 35 | try: |
34 | 36 | from django import VERSION as DJANGO_VERSION |
35 | 37 | from django.conf import settings as django_settings |
@@ -156,6 +158,9 @@ def setup_once(): |
156 | 158 | # type: () -> None |
157 | 159 | _check_minimum_version(DjangoIntegration, DJANGO_VERSION) |
158 | 160 |
|
| 161 | + # Cache database configurations to avoid connection pool interference |
| 162 | + _cache_database_configurations() |
| 163 | + |
159 | 164 | install_sql_hook() |
160 | 165 | # Patch in our custom middleware. |
161 | 166 |
|
@@ -614,6 +619,53 @@ def _set_user_info(request, event): |
614 | 619 | pass |
615 | 620 |
|
616 | 621 |
|
| 622 | +def _cache_database_configurations(): |
| 623 | + # type: () -> None |
| 624 | + """Cache database configurations from Django settings to avoid connection pool interference.""" |
| 625 | + global _cached_db_configs |
| 626 | + |
| 627 | + try: |
| 628 | + from django.conf import settings |
| 629 | + from django.db import connections |
| 630 | + |
| 631 | + for alias, db_config in settings.DATABASES.items(): |
| 632 | + if not db_config: # Skip empty default configs |
| 633 | + continue |
| 634 | + |
| 635 | + try: |
| 636 | + # Get database wrapper to access vendor info |
| 637 | + db_wrapper = connections[alias] |
| 638 | + |
| 639 | + _cached_db_configs[alias] = { |
| 640 | + "db_name": db_config.get("NAME"), |
| 641 | + "host": db_config.get("HOST", "localhost"), |
| 642 | + "port": db_config.get("PORT"), |
| 643 | + "vendor": db_wrapper.vendor, |
| 644 | + "unix_socket": db_config.get("OPTIONS", {}).get("unix_socket"), |
| 645 | + "engine": db_config.get("ENGINE"), |
| 646 | + } |
| 647 | + |
| 648 | + logger.debug( |
| 649 | + "Cached database configuration for %s: %s", |
| 650 | + alias, |
| 651 | + { |
| 652 | + k: v |
| 653 | + for k, v in _cached_db_configs[alias].items() |
| 654 | + if k != "vendor" |
| 655 | + }, |
| 656 | + ) |
| 657 | + |
| 658 | + except Exception as e: |
| 659 | + logger.debug( |
| 660 | + "Failed to cache database configuration for %s: %s", alias, e |
| 661 | + ) |
| 662 | + |
| 663 | + except Exception as e: |
| 664 | + logger.debug("Failed to cache database configurations: %s", e) |
| 665 | + # Graceful fallback - cache remains empty |
| 666 | + _cached_db_configs = {} |
| 667 | + |
| 668 | + |
617 | 669 | def install_sql_hook(): |
618 | 670 | # type: () -> None |
619 | 671 | """If installed this causes Django's queries to be captured.""" |
@@ -698,54 +750,66 @@ def connect(self): |
698 | 750 |
|
699 | 751 | def _set_db_data(span, cursor_or_db): |
700 | 752 | # type: (Span, Any) -> None |
701 | | - db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db |
702 | | - vendor = db.vendor |
703 | | - span.set_data(SPANDATA.DB_SYSTEM, vendor) |
704 | | - |
705 | | - # Some custom backends override `__getattr__`, making it look like `cursor_or_db` |
706 | | - # actually has a `connection` and the `connection` has a `get_dsn_parameters` |
707 | | - # attribute, only to throw an error once you actually want to call it. |
708 | | - # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable |
709 | | - # function. |
710 | | - is_psycopg2 = ( |
711 | | - hasattr(cursor_or_db, "connection") |
712 | | - and hasattr(cursor_or_db.connection, "get_dsn_parameters") |
713 | | - and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) |
714 | | - ) |
715 | | - if is_psycopg2: |
716 | | - connection_params = cursor_or_db.connection.get_dsn_parameters() |
717 | | - else: |
718 | | - try: |
719 | | - # psycopg3, only extract needed params as get_parameters |
720 | | - # can be slow because of the additional logic to filter out default |
721 | | - # values |
722 | | - connection_params = { |
723 | | - "dbname": cursor_or_db.connection.info.dbname, |
724 | | - "port": cursor_or_db.connection.info.port, |
725 | | - } |
726 | | - # PGhost returns host or base dir of UNIX socket as an absolute path |
727 | | - # starting with /, use it only when it contains host |
728 | | - pg_host = cursor_or_db.connection.info.host |
729 | | - if pg_host and not pg_host.startswith("/"): |
730 | | - connection_params["host"] = pg_host |
731 | | - except Exception: |
732 | | - connection_params = db.get_connection_params() |
733 | | - |
734 | | - db_name = connection_params.get("dbname") or connection_params.get("database") |
735 | | - if db_name is not None: |
736 | | - span.set_data(SPANDATA.DB_NAME, db_name) |
| 753 | + """ |
| 754 | + Improved version that avoids connection pool interference by using cached |
| 755 | + database configurations and specific exception handling. |
| 756 | + """ |
| 757 | + from django.core.exceptions import ImproperlyConfigured |
737 | 758 |
|
738 | | - server_address = connection_params.get("host") |
739 | | - if server_address is not None: |
740 | | - span.set_data(SPANDATA.SERVER_ADDRESS, server_address) |
| 759 | + # Get database wrapper |
| 760 | + db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db |
741 | 761 |
|
742 | | - server_port = connection_params.get("port") |
743 | | - if server_port is not None: |
744 | | - span.set_data(SPANDATA.SERVER_PORT, str(server_port)) |
| 762 | + # Set vendor (always safe, no connection access) |
| 763 | + span.set_data(SPANDATA.DB_SYSTEM, db.vendor) |
| 764 | + |
| 765 | + # Get the database alias (supports .using() queries) |
| 766 | + db_alias = db.alias |
| 767 | + |
| 768 | + # Method 1: Use pre-cached configuration (FASTEST, NO CONNECTION ACCESS) |
| 769 | + cached_config = _cached_db_configs.get(db_alias) |
| 770 | + if cached_config: |
| 771 | + if cached_config["db_name"]: |
| 772 | + span.set_data(SPANDATA.DB_NAME, cached_config["db_name"]) |
| 773 | + if cached_config["host"]: |
| 774 | + span.set_data(SPANDATA.SERVER_ADDRESS, cached_config["host"]) |
| 775 | + if cached_config["port"]: |
| 776 | + span.set_data(SPANDATA.SERVER_PORT, str(cached_config["port"])) |
| 777 | + if cached_config["unix_socket"]: |
| 778 | + span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, cached_config["unix_socket"]) |
| 779 | + return |
745 | 780 |
|
746 | | - server_socket_address = connection_params.get("unix_socket") |
747 | | - if server_socket_address is not None: |
748 | | - span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) |
| 781 | + # Method 2: Dynamic fallback using get_connection_params() (NO CONNECTION ACCESS) |
| 782 | + try: |
| 783 | + connection_params = db.get_connection_params() |
| 784 | + |
| 785 | + # Extract database name |
| 786 | + db_name = connection_params.get("dbname") or connection_params.get("database") |
| 787 | + if db_name: |
| 788 | + span.set_data(SPANDATA.DB_NAME, db_name) |
| 789 | + |
| 790 | + # Extract host |
| 791 | + host = connection_params.get("host") |
| 792 | + if host: |
| 793 | + span.set_data(SPANDATA.SERVER_ADDRESS, host) |
| 794 | + |
| 795 | + # Extract port |
| 796 | + port = connection_params.get("port") |
| 797 | + if port: |
| 798 | + span.set_data(SPANDATA.SERVER_PORT, str(port)) |
| 799 | + |
| 800 | + # Extract unix socket |
| 801 | + unix_socket = connection_params.get("unix_socket") |
| 802 | + if unix_socket: |
| 803 | + span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, unix_socket) |
| 804 | + |
| 805 | + except (KeyError, ImproperlyConfigured, AttributeError) as e: |
| 806 | + # Specific exceptions that get_connection_params() can raise: |
| 807 | + # - KeyError: Missing required database settings (most common) |
| 808 | + # - ImproperlyConfigured: Django configuration problems |
| 809 | + # - AttributeError: Missing methods on database wrapper |
| 810 | + logger.debug("Failed to get database connection params for %s: %s", db_alias, e) |
| 811 | + # Skip database metadata rather than risk connection issues |
| 812 | + pass |
749 | 813 |
|
750 | 814 |
|
751 | 815 | def add_template_context_repr_sequence(): |
|
0 commit comments