Skip to content

fix(django): Db query recording improvements. #4681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

antonpirker
Copy link
Member

@antonpirker antonpirker commented Aug 7, 2025

Improve how we record db query connection parameters. This reads the database connection configuration on integration startup from the Django settings.py file and caches them in a thread safe manner.

When db operations are performed we read the connection data from the cache with a fallback to an improved version to the old logic. We first try to call db.get_connection_params() and if this also fails access the connection directly.

Copy link

codecov bot commented Aug 7, 2025

Codecov Report

❌ Patch coverage is 88.23529% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.12%. Comparing base (378fe81) to head (d951ffc).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
sentry_sdk/integrations/django/__init__.py 88.23% 5 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4681      +/-   ##
==========================================
+ Coverage   85.02%   85.12%   +0.10%     
==========================================
  Files         156      156              
  Lines       15856    15917      +61     
  Branches     2684     2699      +15     
==========================================
+ Hits        13481    13550      +69     
+ Misses       1595     1585      -10     
- Partials      780      782       +2     
Files with missing lines Coverage Δ
sentry_sdk/integrations/django/__init__.py 85.18% <88.23%> (+0.81%) ⬆️

... and 4 files with indirect coverage changes

@antonpirker antonpirker marked this pull request as ready for review August 7, 2025 11:08
@antonpirker antonpirker requested a review from a team as a code owner August 7, 2025 11:08
cursor[bot]

This comment was marked as outdated.

@antonpirker antonpirker marked this pull request as draft August 7, 2025 11:18
@antonpirker antonpirker marked this pull request as ready for review August 7, 2025 14:58
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@antonpirker
Copy link
Member Author

antonpirker commented Aug 8, 2025

I used this example benchmarking repo to see how these improvements do. Here are the results:

v1 = current master
v2 = this branches improvements.

v1 vs v2 (v2 averaged across 3 runs)

Scenario RPS v1 RPS v2 mean (sd) Δ RPS TPR v1 ms TPR v2 mean (sd) ms Δ TPR P95 v1 ms P95 v2 mean (sd) ms Δ P95
Async, no Sentry 2474.43 2807.11 (55.53) +13.5% 40.41 35.63 (0.71) -11.8% 50.0 45.0 (1.7) -10.0%
Async, with Sentry 1255.40 1388.96 (16.45) +10.6% 79.66 72.00 (0.85) -9.6% 104.0 89.3 (1.2) -14.3%
Sync, no Sentry 2091.47 2273.36 (50.57) +8.7% 47.81 44.00 (0.97) -8.0% 58.0 56.0 (2.0) -3.4%
Sync, with Sentry 1124.00 1267.03 (7.41) +12.7% 88.97 78.93 (0.46) -11.3% 102.0 97.7 (1.2) -4.2%

Notes:
TPR = ApacheBench “Time per request” (mean).
P95 = 95th percentile latency from the AB percentile section.
Sentry overhead vs no Sentry (throughput drop)
What this shows: The relative throughput loss when enabling Sentry compared to the same setup without Sentry. Computed as (RPS_with_sentry / RPS_no_sentry - 1) × 100%. More negative = larger drop; closer to 0% = better (less overhead).

Mode v1 drop v2 drop (3-run mean)
Async -49.3% -50.5%
Sync -46.2% -44.3%

Bottom line: v2 is consistently faster. Throughput +8–14%, mean latency −8–12%, tail latency improves 3–14%.

@antonpirker antonpirker changed the title Django db query recording improvements. fix(Django): Db query recording improvements. Aug 11, 2025
@antonpirker antonpirker changed the title fix(Django): Db query recording improvements. fix(django): Db query recording improvements. Aug 11, 2025
from django.db import connections

for alias, db_config in settings.DATABASES.items():
if not db_config: # Skip empty default configs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens with these, caching-wise? Will they always go through the slow fallback?

Comment on lines +774 to +783
if cached_config.get("db_name"):
span.set_data(SPANDATA.DB_NAME, cached_config["db_name"])
if cached_config.get("host"):
span.set_data(SPANDATA.SERVER_ADDRESS, cached_config["host"])
if cached_config.get("port"):
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") and span._data.get(SPANDATA.DB_SYSTEM) is None:
span.set_data(SPANDATA.DB_SYSTEM, cached_config["vendor"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move these to a separate function that just picks out the fields from a dictionary? Then we can use it here with cached_config as well as below with connection_params. Otherwise it's easy to miss that you have to update three places if you ever make a change to this

# Fallback to dynamic database metadata collection.
# This is the edge case where db configuration is not in Django's `DATABASES` setting.
try:
# Fallback 1: Try db.get_connection_params() first (NO CONNECTION ACCESS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we also cache the results of Fallback 1 and 2? Otherwise we keep potentially hitting the database in these cases

return # Success - exit early to avoid connection access

except (KeyError, ImproperlyConfigured, AttributeError):
# Fallback 2: Last resort - direct connection access (CONNECTION POOL RISK)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should definitely be cached IMO

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is a bit superfluous

Suggested change
return # Success - exit early
return

Comment on lines +791 to +794
logger.debug(
"Cached db connection params retrieval failed for %s. Trying db.get_connection_params().",
db_alias,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: logging and async code, would remove

if server_socket_address is not None:
span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address)
except Exception as e:
logger.debug("Failed to get database connection params for %s: %s", db_alias, e)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this log we could keep since it's just emitted if there was an error

assert mock.call("server.socket.address", None) not in span.set_data.call_args_list


def test_set_db_data_with_cached_config_unix_socket(sentry_init):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does setting unix_socket need a separate test case? This looks like the previous test case

"""Test _set_db_data falls back to db.get_connection_params() when cache misses."""
sentry_init(integrations=[DjangoIntegration()])

_cached_db_configs.clear()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be good as a fixture after every test case to ensure clean state

@@ -1,12 +1,15 @@
import os
Copy link
Contributor

@sentrivana sentrivana Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are mocking A LOT in these tests and they're very unit-testy, any way we can have some full end-to-end tests?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants