Skip to content

Commit 8deb2ef

Browse files
authored
fix issue with multiple hosts/ports in psycopg2 connect span (#1386)
* fix issue with multiple hosts/ports in psycopg2 connect span fixes #1383 * fix port type depending on input * update changelog and add some comments * fix test * can't use connection.info as psycopg2cffi misses it
1 parent 223c9c1 commit 8deb2ef

File tree

5 files changed

+69
-21
lines changed

5 files changed

+69
-21
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ endif::[]
4242
4343
* Fix an issue where compressed spans would count against `transaction_max_spans` {pull}1377[#1377]
4444
* Make sure HTTP connections are not re-used after a process fork {pull}1374[#1374]
45+
* Fix an issue with psycopg2 instrumentation when multiple hosts are defined {pull}1386[#1386]
4546
* Update the `User-Agent` header to the new https://github.com/elastic/apm/pull/514[spec] {pull}1378[#1378]
4647
* Improve status_code handling in AWS Lambda integration {pull}1382[#1382]
4748
* Fix `aiohttp` exception handling to allow for non-500 responses including `HTTPOk` {pull}1384[#1384]

elasticapm/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -653,11 +653,11 @@ def send(self, url, **kwargs):
653653
return None
654654

655655

656-
def get_client():
656+
def get_client() -> Client:
657657
return CLIENT_SINGLETON
658658

659659

660-
def set_client(client):
660+
def set_client(client: Client):
661661
global CLIENT_SINGLETON
662662
if CLIENT_SINGLETON:
663663
logger = get_logger("elasticapm")

elasticapm/instrumentation/packages/psycopg2.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3030
from __future__ import absolute_import
3131

32+
from typing import Optional, Union
33+
3234
from elasticapm.instrumentation.packages.dbapi2 import (
3335
ConnectionProxy,
3436
CursorProxy,
@@ -76,22 +78,11 @@ class Psycopg2Instrumentation(DbApi2Instrumentation):
7678
def call(self, module, method, wrapped, instance, args, kwargs):
7779
signature = "psycopg2.connect"
7880

79-
host = kwargs.get("host")
80-
if host:
81-
signature += " " + compat.text_type(host)
82-
83-
port = kwargs.get("port")
84-
if port:
85-
port = str(port)
86-
if int(port) != default_ports.get("postgresql"):
87-
host += ":" + port
88-
signature += " " + compat.text_type(host)
89-
else:
90-
# Parse connection string and extract host/port
91-
pass
81+
host, port = get_destination_info(kwargs.get("host"), kwargs.get("port"))
82+
signature = f"{signature} {host}:{port}"
9283
destination_info = {
93-
"address": kwargs.get("host", "localhost"),
94-
"port": int(kwargs.get("port", default_ports.get("postgresql"))),
84+
"address": host,
85+
"port": port,
9586
}
9687
with capture_span(
9788
signature,
@@ -140,3 +131,19 @@ def call(self, module, method, wrapped, instance, args, kwargs):
140131
kwargs["scope"] = kwargs["scope"].__wrapped__
141132

142133
return wrapped(*args, **kwargs)
134+
135+
136+
def get_destination_info(host: Optional[str], port: Union[None, str, int]) -> tuple:
137+
if host:
138+
if "," in host: # multiple hosts defined, take first
139+
host = host.split(",")[0]
140+
else:
141+
host = "localhost"
142+
if port:
143+
port = str(port)
144+
if "," in port: # multiple ports defined, take first
145+
port = port.split(",")[0]
146+
port = int(port)
147+
else:
148+
port = default_ports.get("postgresql")
149+
return host, port

tests/fixtures.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ def elasticapm_client(request):
201201
assert not client._transport.validation_errors
202202

203203

204+
@pytest.fixture()
205+
def elasticapm_transaction(elasticapm_client):
206+
"""
207+
Useful fixture if spans from other fixtures should be captured.
208+
This can be achieved by listing this fixture first.
209+
"""
210+
transaction = elasticapm_client.begin_transaction("test")
211+
yield transaction
212+
213+
204214
@pytest.fixture()
205215
def elasticapm_client_log_file(request):
206216
original_exceptionhook = sys.excepthook

tests/instrumentation/psycopg2_tests.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@
3131
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3232

3333
import os
34+
from typing import cast
3435

3536
import pytest
3637

37-
from elasticapm.conf.constants import TRANSACTION
38-
from elasticapm.instrumentation.packages.psycopg2 import PGCursorProxy, extract_signature
38+
from elasticapm import get_client
39+
from elasticapm.conf.constants import SPAN, TRANSACTION
40+
from elasticapm.instrumentation.packages.psycopg2 import PGCursorProxy, extract_signature, get_destination_info
3941
from elasticapm.utils import default_ports
42+
from tests.fixtures import TempStoreClient
4043

4144
psycopg2 = pytest.importorskip("psycopg2")
4245

@@ -162,10 +165,10 @@ def test_select_with_dollar_quotes_custom_token():
162165

163166

164167
def test_select_with_difficult_table_name():
165-
sql_statement = u"""SELECT id FROM "myta\n-æøåble" WHERE id = 2323"""
168+
sql_statement = """SELECT id FROM "myta\n-æøåble" WHERE id = 2323"""
166169
actual = extract_signature(sql_statement)
167170

168-
assert u"SELECT FROM myta\n-æøåble" == actual
171+
assert "SELECT FROM myta\n-æøåble" == actual
169172

170173

171174
def test_select_subselect():
@@ -507,3 +510,30 @@ def test_psycopg2_execute_values(instrument, postgres_connection, elasticapm_cli
507510
spans = elasticapm_client.spans_for_transaction(transactions[0])
508511
assert spans[0]["name"] == "INSERT INTO test"
509512
assert len(spans[0]["context"]["db"]["statement"]) == 10000, spans[0]["context"]["db"]["statement"]
513+
514+
515+
@pytest.mark.integrationtest
516+
def test_psycopg2_connection(instrument, elasticapm_transaction, postgres_connection):
517+
# elastciapm_client.events is only available on `TempStoreClient`, this keeps the type checkers happy
518+
elasticapm_client = cast(TempStoreClient, get_client())
519+
elasticapm_client.end_transaction("test", "success")
520+
span = elasticapm_client.events[SPAN][0]
521+
host = os.environ.get("POSTGRES_HOST", "localhost")
522+
assert span["name"] == f"psycopg2.connect {host}:5432"
523+
assert span["action"] == "connect"
524+
525+
526+
@pytest.mark.parametrize(
527+
"host_in,port_in,expected_host_out,expected_port_out",
528+
(
529+
(None, None, "localhost", 5432),
530+
(None, 5432, "localhost", 5432),
531+
("localhost", "5432", "localhost", 5432),
532+
("foo.bar", "5432", "foo.bar", 5432),
533+
("localhost,foo.bar", "5432,1234", "localhost", 5432), # multiple hosts
534+
),
535+
)
536+
def test_get_destination(host_in, port_in, expected_host_out, expected_port_out):
537+
host, port = get_destination_info(host_in, port_in)
538+
assert host == expected_host_out
539+
assert port == expected_port_out

0 commit comments

Comments
 (0)