Skip to content

Commit de7e178

Browse files
authored
Fix sqlalchemy_core patch errors for unencoded special characters in db url #416. (#418)
* Fix unit test and integration test * Fix issues #416
1 parent 19fb262 commit de7e178

File tree

6 files changed

+79
-22
lines changed

6 files changed

+79
-22
lines changed

.github/workflows/UnitTesting.yaml

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ jobs:
3434
run: |
3535
sudo /etc/init.d/mysql start
3636
mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
37-
37+
mysql -e 'CREATE DATABASE test_dburl;' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
38+
mysql -e "CREATE USER test_dburl_user@localhost IDENTIFIED BY 'test]password';" -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
39+
mysql -e "GRANT ALL PRIVILEGES ON test_dburl.* TO test_dburl_user@localhost;" -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
40+
mysql -e "FLUSH PRIVILEGES;" -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
3841
- name: Setup Python
3942
uses: actions/setup-python@v4
4043
with:

aws_xray_sdk/ext/sqlalchemy_core/patch.py

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
import sys
3-
from urllib.parse import urlparse, uses_netloc
3+
from urllib.parse import urlparse, uses_netloc, quote_plus
44

55
import wrapt
66
from sqlalchemy.sql.expression import ClauseElement
@@ -14,18 +14,21 @@
1414
def _sql_meta(engine_instance, args):
1515
try:
1616
metadata = {}
17-
url = urlparse(str(engine_instance.engine.url))
17+
# Workaround for https://github.com/sqlalchemy/sqlalchemy/issues/10662
18+
# sqlalchemy.engine.url.URL's __repr__ does not url encode username nor password.
19+
# This will continue to work once sqlalchemy fixes the bug.
20+
sa_url = engine_instance.engine.url
21+
username = sa_url.username
22+
sa_url = sa_url._replace(username=None, password=None)
23+
url = urlparse(str(sa_url))
24+
name = url.netloc
25+
if username:
26+
# Restore url encoded username
27+
quoted_username = quote_plus(username)
28+
url = url._replace(netloc='{}@{}'.format(quoted_username, url.netloc))
1829
# Add Scheme to uses_netloc or // will be missing from url.
1930
uses_netloc.append(url.scheme)
20-
if url.password is None:
21-
metadata['url'] = url.geturl()
22-
name = url.netloc
23-
else:
24-
# Strip password from URL
25-
host_info = url.netloc.rpartition('@')[-1]
26-
parts = url._replace(netloc='{}@{}'.format(url.username, host_info))
27-
metadata['url'] = parts.geturl()
28-
name = host_info
31+
metadata['url'] = url.geturl()
2932
metadata['user'] = url.username
3033
metadata['database_type'] = engine_instance.engine.name
3134
try:

sample-apps/flask/requirements.txt

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
boto3
2-
certifi
3-
chardet
4-
Flask
5-
idna
6-
requests
7-
urllib3
1+
boto3==1.34.26
2+
certifi==2023.11.17
3+
chardet==5.2.0
4+
Flask==2.3.3
5+
idna==3.6
6+
requests==2.31.0
7+
urllib3==1.26.18
88
Werkzeug==3.0.1
99
flask-sqlalchemy==2.5.1
1010
SQLAlchemy==1.4

terraform/eb.tf

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ resource "aws_elastic_beanstalk_application_version" "eb_app_version" {
5555
resource "aws_elastic_beanstalk_environment" "eb_env" {
5656
name = "${var.resource_prefix}-EB-App-Env"
5757
application = aws_elastic_beanstalk_application.eb_app.name
58-
solution_stack_name = "64bit Amazon Linux 2 v3.1.3 running Python 3.7"
58+
solution_stack_name = "64bit Amazon Linux 2 v3.5.10 running Python 3.8"
5959
tier = "WebServer"
6060
version_label = aws_elastic_beanstalk_application_version.eb_app_version.name
6161
cname_prefix = "${var.resource_prefix}-Eb-app-env"
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from sqlalchemy import create_engine
2+
import urllib
3+
import pytest
4+
5+
from aws_xray_sdk.core import xray_recorder, patch
6+
from aws_xray_sdk.ext.sqlalchemy_core import unpatch
7+
from aws_xray_sdk.core.context import Context
8+
9+
MYSQL_USER = "test_dburl_user"
10+
MYSQL_PASSWORD = "test]password"
11+
MYSQL_HOST = "localhost"
12+
MYSQL_PORT = 3306
13+
MYSQL_DB_NAME = "test_dburl"
14+
15+
patch(('sqlalchemy_core',))
16+
17+
@pytest.fixture(autouse=True)
18+
def construct_ctx():
19+
"""
20+
Clean up context storage on each test run and begin a segment
21+
so that later subsegment can be attached. After each test run
22+
it cleans up context storage again.
23+
"""
24+
xray_recorder.configure(service='test', sampling=False, context=Context())
25+
xray_recorder.clear_trace_entities()
26+
xray_recorder.begin_segment('name')
27+
yield
28+
xray_recorder.clear_trace_entities()
29+
30+
31+
def test_db_url_with_special_char():
32+
password = urllib.parse.quote_plus(MYSQL_PASSWORD)
33+
db_url = f"mysql+pymysql://{MYSQL_USER}:{password}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB_NAME}"
34+
35+
engine = create_engine(db_url)
36+
37+
conn = engine.connect()
38+
39+
conn.execute("select 1")
40+
41+
subsegment = xray_recorder.current_segment().subsegments[-1]
42+
43+
assert subsegment.name == f"{MYSQL_HOST}:{MYSQL_PORT}"
44+
sql = subsegment.sql
45+
assert sql['database_type'] == 'mysql'
46+
assert sql['user'] == MYSQL_USER
47+
assert sql['driver_version'] == 'pymysql'
48+
assert sql['database_version']

tox.ini

+5-2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ deps =
5656
wrapt
5757

5858
; Python 3.5+ only deps
59-
py{37,38,39,310,311,312}: pytest-asyncio
59+
py{37,38,39,310,311,312}: pytest-asyncio == 0.21.1
6060

6161
; For pkg_resources
6262
py{37,38,39,310,311,312}: setuptools
@@ -86,13 +86,15 @@ deps =
8686
ext-sqlalchemy_core: sqlalchemy >=1.0.0,<2.0.0
8787
ext-sqlalchemy_core: testing.postgresql
8888
ext-sqlalchemy_core: psycopg2
89+
ext-sqlalchemy_core: pymysql >= 1.0.0
90+
ext-sqlalchemy_core: cryptography
8991

9092
ext-django-2: Django >=2.0,<3.0
9193
ext-django-3: Django >=3.0,<4.0
9294
ext-django-4: Django >=4.0,<5.0
9395
ext-django: django-fake-model
9496

95-
py{37,38,39,310,311,312}-ext-pynamodb: pynamodb >=3.3.1
97+
py{37,38,39,310,311,312}-ext-pynamodb: pynamodb >=3.3.1,<6.0.0
9698

9799
ext-psycopg2: psycopg2
98100
ext-psycopg2: testing.postgresql
@@ -101,6 +103,7 @@ deps =
101103
ext-pg8000: testing.postgresql
102104

103105
py{37,38,39,310,311,312}-ext-pymysql: pymysql >= 1.0.0
106+
py{37,38,39,310,311,312}-ext-pymysql: cryptography
104107

105108
setenv =
106109
DJANGO_SETTINGS_MODULE = tests.ext.django.app.settings

0 commit comments

Comments
 (0)