Skip to content

Commit 5add449

Browse files
Merge branch 'main' into feature
2 parents 21acca8 + a5ed4da commit 5add449

File tree

30 files changed

+265
-256
lines changed

30 files changed

+265
-256
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Instrument all httpx versions >= 0.18. ([#1748](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1748))
1011
- Fix `Invalid type NoneType for attribute X (opentelemetry-instrumentation-aws-lambda)` error when some attributes do not exist
1112
([#1780](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1780))
1213

@@ -41,12 +42,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4142

4243
### Fixed
4344

45+
- Fix redis db.statements to be sanitized by default
46+
([#1778](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1778))
4447
- Fix elasticsearch db.statement attribute to be sanitized by default
4548
([#1758](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1758))
4649
- Fix `AttributeError` when AWS Lambda handler receives a list event
4750
([#1738](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1738))
4851
- Fix `None does not implement middleware` error when there are no middlewares registered
4952
([#1766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1766))
53+
- Fix Flask instrumentation to only close the span if it was created by the same request context.
54+
([#1692](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1692))
5055

5156
## Version 1.17.0/0.38b0 (2023-03-22)
5257

dev-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ bleach==4.1.0 # transient dependency for readme-renderer
1414
grpcio-tools==1.29.0
1515
mypy-protobuf>=1.23
1616
protobuf~=3.13
17-
markupsafe==2.0.1
17+
markupsafe>=2.0.1
1818
codespell==2.1.0
1919
requests==2.28.1
2020
ruamel.yaml==0.17.21

instrumentation/opentelemetry-instrumentation-aiohttp-client/pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ instruments = [
3838
]
3939
test = [
4040
"opentelemetry-instrumentation-aiohttp-client[instruments]",
41+
"http-server-mock"
4142
]
4243

4344
[project.entry-points.opentelemetry_instrumentor]

instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py

+22-11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import aiohttp
2424
import aiohttp.test_utils
2525
import yarl
26+
from http_server_mock import HttpServerMock
2627
from pkg_resources import iter_entry_points
2728

2829
from opentelemetry import context
@@ -313,18 +314,26 @@ async def request_handler(request):
313314
def test_credential_removal(self):
314315
trace_configs = [aiohttp_client.create_trace_config()]
315316

316-
url = "http://username:password@httpbin.org/status/200"
317-
with self.subTest(url=url):
317+
app = HttpServerMock("test_credential_removal")
318318

319-
async def do_request(url):
320-
async with aiohttp.ClientSession(
321-
trace_configs=trace_configs,
322-
) as session:
323-
async with session.get(url):
324-
pass
319+
@app.route("/status/200")
320+
def index():
321+
return "hello"
325322

326-
loop = asyncio.get_event_loop()
327-
loop.run_until_complete(do_request(url))
323+
url = "http://username:password@localhost:5000/status/200"
324+
325+
with app.run("localhost", 5000):
326+
with self.subTest(url=url):
327+
328+
async def do_request(url):
329+
async with aiohttp.ClientSession(
330+
trace_configs=trace_configs,
331+
) as session:
332+
async with session.get(url):
333+
pass
334+
335+
loop = asyncio.get_event_loop()
336+
loop.run_until_complete(do_request(url))
328337

329338
self.assert_spans(
330339
[
@@ -333,7 +342,9 @@ async def do_request(url):
333342
(StatusCode.UNSET, None),
334343
{
335344
SpanAttributes.HTTP_METHOD: "GET",
336-
SpanAttributes.HTTP_URL: "http://httpbin.org/status/200",
345+
SpanAttributes.HTTP_URL: (
346+
"http://localhost:5000/status/200"
347+
),
337348
SpanAttributes.HTTP_STATUS_CODE: int(HTTPStatus.OK),
338349
},
339350
)

instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -705,11 +705,11 @@ def test_response_attributes_invalid_status_code(self):
705705
self.assertEqual(self.span.set_status.call_count, 1)
706706

707707
def test_credential_removal(self):
708-
self.scope["server"] = ("username:password@httpbin.org", 80)
708+
self.scope["server"] = ("username:password@mock", 80)
709709
self.scope["path"] = "/status/200"
710710
attrs = otel_asgi.collect_request_attributes(self.scope)
711711
self.assertEqual(
712-
attrs[SpanAttributes.HTTP_URL], "http://httpbin.org/status/200"
712+
attrs[SpanAttributes.HTTP_URL], "http://mock/status/200"
713713
)
714714

715715
def test_collect_target_attribute_missing(self):

instrumentation/opentelemetry-instrumentation-flask/pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies = [
3030
"opentelemetry-instrumentation-wsgi == 0.40b0.dev",
3131
"opentelemetry-semantic-conventions == 0.40b0.dev",
3232
"opentelemetry-util-http == 0.40b0.dev",
33+
"packaging >= 21.0",
3334
]
3435

3536
[project.optional-dependencies]
@@ -38,7 +39,7 @@ instruments = [
3839
]
3940
test = [
4041
"opentelemetry-instrumentation-flask[instruments]",
41-
"markupsafe==2.0.1",
42+
"markupsafe==2.1.2",
4243
"opentelemetry-test-utils == 0.40b0.dev",
4344
]
4445

instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,14 @@ def response_hook(span: Span, status: str, response_headers: List):
238238
API
239239
---
240240
"""
241+
import weakref
241242
from logging import getLogger
242-
from threading import get_ident
243243
from time import time_ns
244244
from timeit import default_timer
245245
from typing import Collection
246246

247247
import flask
248+
from packaging import version as package_version
248249

249250
import opentelemetry.instrumentation.wsgi as otel_wsgi
250251
from opentelemetry import context, trace
@@ -265,11 +266,21 @@ def response_hook(span: Span, status: str, response_headers: List):
265266
_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
266267
_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
267268
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
268-
_ENVIRON_THREAD_ID_KEY = "opentelemetry-flask.thread_id_key"
269+
_ENVIRON_REQCTX_REF_KEY = "opentelemetry-flask.reqctx_ref_key"
269270
_ENVIRON_TOKEN = "opentelemetry-flask.token"
270271

271272
_excluded_urls_from_env = get_excluded_urls("FLASK")
272273

274+
if package_version.parse(flask.__version__) >= package_version.parse("2.2.0"):
275+
276+
def _request_ctx_ref() -> weakref.ReferenceType:
277+
return weakref.ref(flask.globals.request_ctx._get_current_object())
278+
279+
else:
280+
281+
def _request_ctx_ref() -> weakref.ReferenceType:
282+
return weakref.ref(flask._request_ctx_stack.top)
283+
273284

274285
def get_default_span_name():
275286
try:
@@ -399,7 +410,7 @@ def _before_request():
399410
activation = trace.use_span(span, end_on_exit=True)
400411
activation.__enter__() # pylint: disable=E1101
401412
flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation
402-
flask_request_environ[_ENVIRON_THREAD_ID_KEY] = get_ident()
413+
flask_request_environ[_ENVIRON_REQCTX_REF_KEY] = _request_ctx_ref()
403414
flask_request_environ[_ENVIRON_SPAN_KEY] = span
404415
flask_request_environ[_ENVIRON_TOKEN] = token
405416

@@ -439,17 +450,22 @@ def _teardown_request(exc):
439450
return
440451

441452
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
442-
thread_id = flask.request.environ.get(_ENVIRON_THREAD_ID_KEY)
443-
if not activation or thread_id != get_ident():
453+
454+
original_reqctx_ref = flask.request.environ.get(
455+
_ENVIRON_REQCTX_REF_KEY
456+
)
457+
current_reqctx_ref = _request_ctx_ref()
458+
if not activation or original_reqctx_ref != current_reqctx_ref:
444459
# This request didn't start a span, maybe because it was created in
445460
# a way that doesn't run `before_request`, like when it is created
446461
# with `app.test_request_context`.
447462
#
448-
# Similarly, check the thread_id against the current thread to ensure
449-
# tear down only happens on the original thread. This situation can
450-
# arise if the original thread handling the request spawn children
451-
# threads and then uses something like copy_current_request_context
452-
# to copy the request context.
463+
# Similarly, check that the request_ctx that created the span
464+
# matches the current request_ctx, and only tear down if they match.
465+
# This situation can arise if the original request_ctx handling
466+
# the request calls functions that push new request_ctx's,
467+
# like any decorated with `flask.copy_current_request_context`.
468+
453469
return
454470
if exc is None:
455471
activation.__exit__(None, None, None)

instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from werkzeug.test import Client
2020
from werkzeug.wrappers import Response
2121

22-
from opentelemetry import context
22+
from opentelemetry import context, trace
2323

2424

2525
class InstrumentationTest:
@@ -37,6 +37,21 @@ def _sqlcommenter_endpoint():
3737
)
3838
return sqlcommenter_flask_values
3939

40+
@staticmethod
41+
def _copy_context_endpoint():
42+
@flask.copy_current_request_context
43+
def _extract_header():
44+
return flask.request.headers["x-req"]
45+
46+
# Despite `_extract_header` copying the request context,
47+
# calling it shouldn't detach the parent Flask span's contextvar
48+
request_header = _extract_header()
49+
50+
return {
51+
"span_name": trace.get_current_span().name,
52+
"request_header": request_header,
53+
}
54+
4055
@staticmethod
4156
def _multithreaded_endpoint(count):
4257
def do_random_stuff():
@@ -84,6 +99,7 @@ def excluded2_endpoint():
8499
self.app.route("/hello/<int:helloid>")(self._hello_endpoint)
85100
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
86101
self.app.route("/multithreaded")(self._multithreaded_endpoint)
102+
self.app.route("/copy_context")(self._copy_context_endpoint)
87103
self.app.route("/excluded/<int:helloid>")(self._hello_endpoint)
88104
self.app.route("/excluded")(excluded_endpoint)
89105
self.app.route("/excluded2")(excluded2_endpoint)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import flask
15+
from werkzeug.test import Client
16+
from werkzeug.wrappers import Response
17+
18+
from opentelemetry.instrumentation.flask import FlaskInstrumentor
19+
from opentelemetry.test.wsgitestutil import WsgiTestBase
20+
21+
from .base_test import InstrumentationTest
22+
23+
24+
class TestCopyContext(InstrumentationTest, WsgiTestBase):
25+
def setUp(self):
26+
super().setUp()
27+
FlaskInstrumentor().instrument()
28+
self.app = flask.Flask(__name__)
29+
self._common_initialization()
30+
31+
def tearDown(self):
32+
super().tearDown()
33+
with self.disable_logging():
34+
FlaskInstrumentor().uninstrument()
35+
36+
def test_copycontext(self):
37+
"""Test that instrumentation tear down does not blow up
38+
when the request calls functions where the context has been
39+
copied via `flask.copy_current_request_context`
40+
"""
41+
self.app = flask.Flask(__name__)
42+
self.app.route("/copy_context")(self._copy_context_endpoint)
43+
client = Client(self.app, Response)
44+
resp = client.get("/copy_context", headers={"x-req": "a-header"})
45+
46+
self.assertEqual(200, resp.status_code)
47+
self.assertEqual("/copy_context", resp.json["span_name"])
48+
self.assertEqual("a-header", resp.json["request_header"])

instrumentation/opentelemetry-instrumentation-httpx/README.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ When using the instrumentor, all clients will automatically trace requests.
3030
import httpx
3131
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
3232
33-
url = "https://httpbin.org/get"
33+
url = "https://some.url/get"
3434
HTTPXClientInstrumentor().instrument()
3535
3636
with httpx.Client() as client:
@@ -51,7 +51,7 @@ use the `instrument_client` method.
5151
import httpx
5252
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
5353
54-
url = "https://httpbin.org/get"
54+
url = "https://some.url/get"
5555
5656
with httpx.Client(transport=telemetry_transport) as client:
5757
HTTPXClientInstrumentor.instrument_client(client)
@@ -96,7 +96,7 @@ If you don't want to use the instrumentor class, you can use the transport class
9696
SyncOpenTelemetryTransport,
9797
)
9898
99-
url = "https://httpbin.org/get"
99+
url = "https://some.url/get"
100100
transport = httpx.HTTPTransport()
101101
telemetry_transport = SyncOpenTelemetryTransport(transport)
102102

instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ dependencies = [
3232

3333
[project.optional-dependencies]
3434
instruments = [
35-
"httpx >= 0.18.0, <= 0.23.0",
35+
"httpx >= 0.18.0",
3636
]
3737
test = [
3838
"opentelemetry-instrumentation-httpx[instruments]",

instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import httpx
2626
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
2727
28-
url = "https://httpbin.org/get"
28+
url = "https://some.url/get"
2929
HTTPXClientInstrumentor().instrument()
3030
3131
with httpx.Client() as client:
@@ -46,7 +46,7 @@
4646
import httpx
4747
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
4848
49-
url = "https://httpbin.org/get"
49+
url = "https://some.url/get"
5050
5151
with httpx.Client(transport=telemetry_transport) as client:
5252
HTTPXClientInstrumentor.instrument_client(client)
@@ -91,7 +91,7 @@
9191
SyncOpenTelemetryTransport,
9292
)
9393
94-
url = "https://httpbin.org/get"
94+
url = "https://some.url/get"
9595
transport = httpx.HTTPTransport()
9696
telemetry_transport = SyncOpenTelemetryTransport(transport)
9797

instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def _async_call(coro: typing.Coroutine) -> asyncio.Task:
5959
def _response_hook(span, request: "RequestInfo", response: "ResponseInfo"):
6060
span.set_attribute(
6161
HTTP_RESPONSE_BODY,
62-
response[2].read(),
62+
b"".join(response[2]),
6363
)
6464

6565

@@ -68,7 +68,7 @@ async def _async_response_hook(
6868
):
6969
span.set_attribute(
7070
HTTP_RESPONSE_BODY,
71-
await response[2].aread(),
71+
b"".join([part async for part in response[2]]),
7272
)
7373

7474

@@ -97,7 +97,7 @@ class BaseTestCases:
9797
class BaseTest(TestBase, metaclass=abc.ABCMeta):
9898
# pylint: disable=no-member
9999

100-
URL = "http://httpbin.org/status/200"
100+
URL = "http://mock/status/200"
101101
response_hook = staticmethod(_response_hook)
102102
request_hook = staticmethod(_request_hook)
103103
no_update_request_hook = staticmethod(_no_update_request_hook)
@@ -165,7 +165,7 @@ def test_basic_multiple(self):
165165
self.assert_span(num_spans=2)
166166

167167
def test_not_foundbasic(self):
168-
url_404 = "http://httpbin.org/status/404"
168+
url_404 = "http://mock/status/404"
169169

170170
with respx.mock:
171171
respx.get(url_404).mock(httpx.Response(404))

0 commit comments

Comments
 (0)