Skip to content

Commit 33adb7d

Browse files
authored
refactor httpx instrumentation (#1403)
* refactor httpx instrumentation httpx 0.21 and httpcore 0.14 once again changed how it works, necessitating more special handling. To make this a tad less cumbersome, most of this code has moved into a utils file. To make the structure somewhat sane, httpx got its own package in instrumentation * update changelog
1 parent aaaa00a commit 33adb7d

File tree

13 files changed

+201
-96
lines changed

13 files changed

+201
-96
lines changed

.ci/.jenkins_framework_full.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ FRAMEWORK:
7777
- graphene-2
7878
- httpx-0.13
7979
- httpx-0.14
80+
- httpx-0.21
8081
- httpx-newest
8182
- httplib2-newest
8283
- sanic-20.12

CHANGELOG.asciidoc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ endif::[]
3838
3939
* Add support for Sanic framework {pull}1390[#1390]
4040
41-
//[float]
42-
//===== Bug fixes
41+
[float]
42+
===== Bug fixes
43+
44+
* fix compatibility issues with httpx 0.21 {pull}1403[#1403]
4345
4446
[[release-notes-6.x]]
4547
=== Python Agent version 6.x
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2021, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2021, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

elasticapm/instrumentation/packages/asyncio/httpcore.py renamed to elasticapm/instrumentation/packages/httpx/async/httpcore.py

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# BSD 3-Clause License
22
#
3-
# Copyright (c) 2019, Elasticsearch BV
3+
# Copyright (c) 2021, Elasticsearch BV
44
# All rights reserved.
55
#
66
# Redistribution and use in source and binary forms, with or without
@@ -28,9 +28,9 @@
2828
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2929
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3030

31-
from elasticapm.conf import constants
3231
from elasticapm.contrib.asyncio.traces import async_capture_span
3332
from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule
33+
from elasticapm.instrumentation.packages.httpx import utils
3434
from elasticapm.traces import DroppedSpan, execution_context
3535
from elasticapm.utils import default_ports
3636
from elasticapm.utils.disttracing import TracingOptions
@@ -54,27 +54,8 @@ class HTTPCoreAsyncInstrumentation(AsyncAbstractInstrumentedModule):
5454
]
5555

5656
async def call(self, module, method, wrapped, instance, args, kwargs):
57-
if "method" in kwargs:
58-
method = kwargs["method"].decode("utf-8")
59-
else:
60-
method = args[0].decode("utf-8")
61-
62-
# URL is a tuple of (scheme, host, port, path), we want path
63-
if "url" in kwargs:
64-
url = kwargs["url"][3].decode("utf-8")
65-
else:
66-
url = args[1][3].decode("utf-8")
67-
68-
headers = None
69-
if "headers" in kwargs:
70-
headers = kwargs["headers"]
71-
if headers is None:
72-
headers = []
73-
kwargs["headers"] = headers
74-
75-
scheme, host, port = instance.origin
76-
scheme = scheme.decode("utf-8")
77-
host = host.decode("utf-8")
57+
url, method, headers = utils.get_request_data(args, kwargs)
58+
scheme, host, port, target = url
7859

7960
if port != default_ports.get(scheme):
8061
host += ":" + str(port)
@@ -104,16 +85,9 @@ async def call(self, module, method, wrapped, instance, args, kwargs):
10485
trace_parent = transaction.trace_parent.copy_from(
10586
span_id=parent_id, trace_options=TracingOptions(recorded=True)
10687
)
107-
self._set_disttracing_headers(headers, trace_parent, transaction)
88+
utils.set_disttracing_headers(headers, trace_parent, transaction)
10889
response = await wrapped(*args, **kwargs)
109-
if len(response) > 4:
110-
# httpcore < 0.11.0
111-
# response = (http_version, status_code, reason_phrase, headers, stream)
112-
status_code = response[1]
113-
else:
114-
# httpcore >= 0.11.0
115-
# response = (status_code, headers, stream, ext)
116-
status_code = response[0]
90+
status_code = utils.get_status(response)
11791
if status_code:
11892
if span.context:
11993
span.context["http"]["status_code"] = status_code
@@ -125,18 +99,6 @@ def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kw
12599
trace_parent = transaction.trace_parent.copy_from(
126100
span_id=transaction.id, trace_options=TracingOptions(recorded=False)
127101
)
128-
if "headers" in kwargs:
129-
headers = kwargs["headers"]
130-
if headers is None:
131-
headers = []
132-
kwargs["headers"] = headers
133-
self._set_disttracing_headers(headers, trace_parent, transaction)
102+
headers = utils.get_request_data(args, kwargs)[2]
103+
utils.set_disttracing_headers(headers, trace_parent, transaction)
134104
return args, kwargs
135-
136-
def _set_disttracing_headers(self, headers, trace_parent, transaction):
137-
trace_parent_str = trace_parent.to_string()
138-
headers.append((bytes(constants.TRACEPARENT_HEADER_NAME, "utf-8"), bytes(trace_parent_str, "utf-8")))
139-
if transaction.tracer.config.use_elastic_traceparent_header:
140-
headers.append((bytes(constants.TRACEPARENT_LEGACY_HEADER_NAME, "utf-8"), bytes(trace_parent_str, "utf-8")))
141-
if trace_parent.tracestate:
142-
headers.append((bytes(constants.TRACESTATE_HEADER_NAME, "utf-8"), bytes(trace_parent.tracestate, "utf-8")))
File renamed without changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2021, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

elasticapm/instrumentation/packages/httpcore.py renamed to elasticapm/instrumentation/packages/httpx/sync/httpcore.py

Lines changed: 8 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# BSD 3-Clause License
22
#
3-
# Copyright (c) 2019, Elasticsearch BV
3+
# Copyright (c) 2021, Elasticsearch BV
44
# All rights reserved.
55
#
66
# Redistribution and use in source and binary forms, with or without
@@ -28,8 +28,8 @@
2828
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2929
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3030

31-
from elasticapm.conf import constants
3231
from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule
32+
from elasticapm.instrumentation.packages.httpx import utils
3333
from elasticapm.traces import DroppedSpan, capture_span, execution_context
3434
from elasticapm.utils import default_ports
3535
from elasticapm.utils.disttracing import TracingOptions
@@ -41,37 +41,18 @@ class HTTPCoreInstrumentation(AbstractInstrumentedModule):
4141
instrument_list = [
4242
("httpcore._sync.connection", "SyncHTTPConnection.request"), # < httpcore 0.13
4343
("httpcore._sync.connection", "SyncHTTPConnection.handle_request"), # >= httpcore 0.13
44+
("httpcore._sync.connection", "HTTPConnection.handle_request"), # httpcore >= 0.14 (hopefully...)
4445
]
4546

4647
def call(self, module, method, wrapped, instance, args, kwargs):
47-
if "method" in kwargs:
48-
method = kwargs["method"].decode("utf-8")
49-
else:
50-
method = args[0].decode("utf-8")
51-
52-
# URL is a tuple of (scheme, host, port, path), we want path
53-
if "url" in kwargs:
54-
url = kwargs["url"][3].decode("utf-8")
55-
else:
56-
url = args[1][3].decode("utf-8")
57-
58-
headers = None
59-
if "headers" in kwargs:
60-
headers = kwargs["headers"]
61-
if headers is None:
62-
headers = []
63-
kwargs["headers"] = headers
64-
65-
scheme, host, port = instance.origin
66-
scheme = scheme.decode("utf-8")
67-
host = host.decode("utf-8")
68-
48+
url, method, headers = utils.get_request_data(args, kwargs)
49+
scheme, host, port, target = url
6950
if port != default_ports.get(scheme):
7051
host += ":" + str(port)
7152

7253
signature = "%s %s" % (method.upper(), host)
7354

74-
url = "%s://%s%s" % (scheme, host, url)
55+
url = "%s://%s%s" % (scheme, host, target)
7556

7657
transaction = execution_context.get_transaction()
7758

@@ -94,18 +75,11 @@ def call(self, module, method, wrapped, instance, args, kwargs):
9475
trace_parent = transaction.trace_parent.copy_from(
9576
span_id=parent_id, trace_options=TracingOptions(recorded=True)
9677
)
97-
self._set_disttracing_headers(headers, trace_parent, transaction)
78+
utils.set_disttracing_headers(headers, trace_parent, transaction)
9879
if leaf_span:
9980
leaf_span.dist_tracing_propagated = True
10081
response = wrapped(*args, **kwargs)
101-
if len(response) > 4:
102-
# httpcore < 0.11.0
103-
# response = (http_version, status_code, reason_phrase, headers, stream)
104-
status_code = response[1]
105-
else:
106-
# httpcore >= 0.11.0
107-
# response = (status_code, headers, stream, ext)
108-
status_code = response[0]
82+
status_code = utils.get_status(response)
10983
if status_code:
11084
if span.context:
11185
span.context["http"]["status_code"] = status_code
@@ -124,11 +98,3 @@ def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kw
12498
kwargs["headers"] = headers
12599
self._set_disttracing_headers(headers, trace_parent, transaction)
126100
return args, kwargs
127-
128-
def _set_disttracing_headers(self, headers, trace_parent, transaction):
129-
trace_parent_str = trace_parent.to_string()
130-
headers.append((bytes(constants.TRACEPARENT_HEADER_NAME, "utf-8"), bytes(trace_parent_str, "utf-8")))
131-
if transaction.tracer.config.use_elastic_traceparent_header:
132-
headers.append((bytes(constants.TRACEPARENT_LEGACY_HEADER_NAME, "utf-8"), bytes(trace_parent_str, "utf-8")))
133-
if trace_parent.tracestate:
134-
headers.append((bytes(constants.TRACESTATE_HEADER_NAME, "utf-8"), bytes(trace_parent.tracestate, "utf-8")))
File renamed without changes.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2021, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
from typing import Tuple
31+
32+
from elasticapm.conf import constants
33+
34+
35+
def get_request_data(args, kwargs) -> Tuple[Tuple[str, str, int, str], str, list]:
36+
if len(args) == 1 and hasattr(args[0], "method"):
37+
# httpcore >= 0.14
38+
request = args[0]
39+
method = request.method.decode("utf-8")
40+
headers = request.headers
41+
url = (
42+
request.url.scheme.decode("utf-8"),
43+
request.url.host.decode("utf-8"),
44+
request.url.port,
45+
request.url.target.decode("utf-8"),
46+
)
47+
else:
48+
if "method" in kwargs:
49+
method = kwargs["method"].decode("utf-8")
50+
else:
51+
method = args[0].decode("utf-8")
52+
53+
# URL is a tuple of (scheme, host, port, path)
54+
if "url" in kwargs:
55+
url = kwargs["url"]
56+
else:
57+
url = args[1]
58+
url = (url[0].decode("utf-8"), url[1].decode("utf-8"), url[2], url[3].decode("utf-8"))
59+
print(kwargs)
60+
if "headers" in kwargs:
61+
headers = kwargs["headers"]
62+
else:
63+
headers = []
64+
return url, method, headers
65+
66+
67+
def get_status(response) -> int:
68+
if hasattr(response, "status"): # httpcore >= 0.14
69+
status_code = response.status
70+
elif len(response) > 4:
71+
# httpcore < 0.11.0
72+
# response = (http_version, status_code, reason_phrase, headers, stream)
73+
status_code = response[1]
74+
else:
75+
# httpcore >= 0.11.0
76+
# response = (status_code, headers, stream, ext)
77+
status_code = response[0]
78+
return status_code
79+
80+
81+
def set_disttracing_headers(headers, trace_parent, transaction):
82+
trace_parent_str = trace_parent.to_string()
83+
headers.append((bytes(constants.TRACEPARENT_HEADER_NAME, "utf-8"), bytes(trace_parent_str, "utf-8")))
84+
if transaction.tracer.config.use_elastic_traceparent_header:
85+
headers.append((bytes(constants.TRACEPARENT_LEGACY_HEADER_NAME, "utf-8"), bytes(trace_parent_str, "utf-8")))
86+
if trace_parent.tracestate:
87+
headers.append((bytes(constants.TRACESTATE_HEADER_NAME, "utf-8"), bytes(trace_parent.tracestate, "utf-8")))

0 commit comments

Comments
 (0)