Skip to content

Commit 865837f

Browse files
authored
Ensure clean http url (#538)
1 parent e347fa7 commit 865837f

File tree

21 files changed

+155
-9
lines changed

21 files changed

+155
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
([#530](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/530))
1515
- Fix weak reference error for pyodbc cursor in SQLAlchemy instrumentation.
1616
([#469](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/469))
17+
- Implemented specification that HTTP span attributes must not contain username and password.
18+
([#538](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/538))
1719

1820
### Added
1921
- `opentelemetry-instrumentation-httpx` Add `httpx` instrumentation

instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ install_requires =
4141
opentelemetry-api == 1.4.0.dev0
4242
opentelemetry-semantic-conventions == 0.23.dev0
4343
opentelemetry-instrumentation == 0.23.dev0
44+
opentelemetry-util-http == 0.23.dev0
4445
wrapt >= 1.0.0, < 2.0.0
4546

4647
[options.packages.find]

instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def strip_query_params(url: yarl.URL) -> str:
8282
from opentelemetry.semconv.trace import SpanAttributes
8383
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
8484
from opentelemetry.trace.status import Status, StatusCode
85+
from opentelemetry.util.http import remove_url_credentials
8586

8687
_UrlFilterT = typing.Optional[typing.Callable[[str], str]]
8788
_SpanNameT = typing.Optional[
@@ -173,11 +174,11 @@ async def on_request_start(
173174
if trace_config_ctx.span.is_recording():
174175
attributes = {
175176
SpanAttributes.HTTP_METHOD: http_method,
176-
SpanAttributes.HTTP_URL: trace_config_ctx.url_filter(
177-
params.url
177+
SpanAttributes.HTTP_URL: remove_url_credentials(
178+
trace_config_ctx.url_filter(params.url)
178179
)
179180
if callable(trace_config_ctx.url_filter)
180-
else str(params.url),
181+
else remove_url_credentials(str(params.url)),
181182
}
182183
for key, value in attributes.items():
183184
trace_config_ctx.span.set_attribute(key, value)

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,37 @@ async def request_handler(request):
321321
]
322322
)
323323

324+
def test_credential_removal(self):
325+
trace_configs = [aiohttp_client.create_trace_config()]
326+
327+
url = "http://username:[email protected]/status/200"
328+
with self.subTest(url=url):
329+
330+
async def do_request(url):
331+
async with aiohttp.ClientSession(
332+
trace_configs=trace_configs,
333+
) as session:
334+
async with session.get(url):
335+
pass
336+
337+
loop = asyncio.get_event_loop()
338+
loop.run_until_complete(do_request(url))
339+
340+
self.assert_spans(
341+
[
342+
(
343+
"HTTP GET",
344+
(StatusCode.UNSET, None),
345+
{
346+
SpanAttributes.HTTP_METHOD: "GET",
347+
SpanAttributes.HTTP_URL: "http://httpbin.org/status/200",
348+
SpanAttributes.HTTP_STATUS_CODE: int(HTTPStatus.OK),
349+
},
350+
)
351+
]
352+
)
353+
self.memory_exporter.clear()
354+
324355

325356
class TestAioHttpClientInstrumentor(TestBase):
326357
URL = "/test-path"

instrumentation/opentelemetry-instrumentation-asgi/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ install_requires =
4141
opentelemetry-api == 1.4.0.dev0
4242
opentelemetry-semantic-conventions == 0.23.dev0
4343
opentelemetry-instrumentation == 0.23.dev0
44+
opentelemetry-util-http == 0.23.dev0
4445

4546
[options.extras_require]
4647
test =

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from opentelemetry.propagators.textmap import Getter
3333
from opentelemetry.semconv.trace import SpanAttributes
3434
from opentelemetry.trace.status import Status, StatusCode
35+
from opentelemetry.util.http import remove_url_credentials
3536

3637

3738
class ASGIGetter(Getter):
@@ -86,7 +87,7 @@ def collect_request_attributes(scope):
8687
SpanAttributes.NET_HOST_PORT: port,
8788
SpanAttributes.HTTP_FLAVOR: scope.get("http_version"),
8889
SpanAttributes.HTTP_TARGET: scope.get("path"),
89-
SpanAttributes.HTTP_URL: http_url,
90+
SpanAttributes.HTTP_URL: remove_url_credentials(http_url),
9091
}
9192
http_method = scope.get("method")
9293
if http_method:

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,14 @@ def test_response_attributes_invalid_status_code(self):
430430
otel_asgi.set_status_code(self.span, "Invalid Status Code")
431431
self.assertEqual(self.span.set_status.call_count, 1)
432432

433+
def test_credential_removal(self):
434+
self.scope["server"] = ("username:[email protected]", 80)
435+
self.scope["path"] = "/status/200"
436+
attrs = otel_asgi.collect_request_attributes(self.scope)
437+
self.assertEqual(
438+
attrs[SpanAttributes.HTTP_URL], "http://httpbin.org/status/200"
439+
)
440+
433441

434442
if __name__ == "__main__":
435443
unittest.main()

instrumentation/opentelemetry-instrumentation-requests/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ install_requires =
4141
opentelemetry-api == 1.4.0.dev0
4242
opentelemetry-semantic-conventions == 0.23.dev0
4343
opentelemetry-instrumentation == 0.23.dev0
44+
opentelemetry-util-http == 0.23.dev0
4445

4546
[options.extras_require]
4647
test =

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from opentelemetry.semconv.trace import SpanAttributes
5151
from opentelemetry.trace import SpanKind, get_tracer
5252
from opentelemetry.trace.status import Status
53+
from opentelemetry.util.http import remove_url_credentials
5354

5455
# A key to a context variable to avoid creating duplicate spans when instrumenting
5556
# both, Session.request and Session.send, since Session.request calls into Session.send
@@ -124,6 +125,8 @@ def _instrumented_requests_call(
124125
if not span_name or not isinstance(span_name, str):
125126
span_name = get_default_span_name(method)
126127

128+
url = remove_url_credentials(url)
129+
127130
labels = {}
128131
labels[SpanAttributes.HTTP_METHOD] = method
129132
labels[SpanAttributes.HTTP_URL] = url

instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,13 @@ def test_invalid_url(self):
357357
)
358358
self.assertEqual(span.status.status_code, StatusCode.ERROR)
359359

360+
def test_credential_removal(self):
361+
new_url = "http://username:[email protected]/status/200"
362+
self.perform_request(new_url)
363+
span = self.assert_span()
364+
365+
self.assertEqual(span.attributes[SpanAttributes.HTTP_URL], self.URL)
366+
360367
def test_if_headers_equals_none(self):
361368
result = requests.get(self.URL, headers=None)
362369
self.assertEqual(result.text, "Hello!")

instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from opentelemetry.semconv.trace import SpanAttributes
2323
from opentelemetry.trace.status import Status
2424
from opentelemetry.util._time import _time_ns
25+
from opentelemetry.util.http import remove_url_credentials
2526

2627

2728
def _normalize_request(args, kwargs):
@@ -61,7 +62,7 @@ def fetch_async(tracer, request_hook, response_hook, func, _, args, kwargs):
6162

6263
if span.is_recording():
6364
attributes = {
64-
SpanAttributes.HTTP_URL: request.url,
65+
SpanAttributes.HTTP_URL: remove_url_credentials(request.url),
6566
SpanAttributes.HTTP_METHOD: request.method,
6667
}
6768
for key, value in attributes.items():

instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,29 @@ def test_response_headers(self):
455455
self.memory_exporter.clear()
456456
set_global_response_propagator(orig)
457457

458+
def test_credential_removal(self):
459+
response = self.fetch(
460+
"http://username:[email protected]/status/200"
461+
)
462+
self.assertEqual(response.code, 200)
463+
464+
spans = self.sorted_spans(self.memory_exporter.get_finished_spans())
465+
self.assertEqual(len(spans), 1)
466+
client = spans[0]
467+
468+
self.assertEqual(client.name, "GET")
469+
self.assertEqual(client.kind, SpanKind.CLIENT)
470+
self.assert_span_has_attributes(
471+
client,
472+
{
473+
SpanAttributes.HTTP_URL: "http://httpbin.org/status/200",
474+
SpanAttributes.HTTP_METHOD: "GET",
475+
SpanAttributes.HTTP_STATUS_CODE: 200,
476+
},
477+
)
478+
479+
self.memory_exporter.clear()
480+
458481

459482
class TornadoHookTest(TornadoTest):
460483
_client_request_hook = None

instrumentation/opentelemetry-instrumentation-urllib/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ install_requires =
4141
opentelemetry-api == 1.4.0.dev0
4242
opentelemetry-semantic-conventions == 0.23.dev0
4343
opentelemetry-instrumentation == 0.23.dev0
44+
opentelemetry-util-http == 0.23.dev0
4445

4546
[options.extras_require]
4647
test =

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from opentelemetry.semconv.trace import SpanAttributes
5454
from opentelemetry.trace import SpanKind, get_tracer
5555
from opentelemetry.trace.status import Status
56+
from opentelemetry.util.http import remove_url_credentials
5657

5758
# A key to a context variable to avoid creating duplicate spans when instrumenting
5859
_SUPPRESS_HTTP_INSTRUMENTATION_KEY = "suppress_http_instrumentation"
@@ -142,6 +143,8 @@ def _instrumented_open_call(
142143
if not span_name or not isinstance(span_name, str):
143144
span_name = get_default_span_name(method)
144145

146+
url = remove_url_credentials(url)
147+
145148
labels = {
146149
SpanAttributes.HTTP_METHOD: method,
147150
SpanAttributes.HTTP_URL: url,

instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
from opentelemetry.test.test_base import TestBase
3636
from opentelemetry.trace import StatusCode
3737

38+
# pylint: disable=too-many-public-methods
39+
3840

3941
class RequestsIntegrationTestBase(abc.ABC):
4042
# pylint: disable=no-member
@@ -318,6 +320,15 @@ def test_requests_timeout_exception(self, *_, **__):
318320
span = self.assert_span()
319321
self.assertEqual(span.status.status_code, StatusCode.ERROR)
320322

323+
def test_credential_removal(self):
324+
url = "http://username:[email protected]/status/200"
325+
326+
with self.assertRaises(Exception):
327+
self.perform_request(url)
328+
329+
span = self.assert_span()
330+
self.assertEqual(span.attributes[SpanAttributes.HTTP_URL], self.URL)
331+
321332

322333
class TestRequestsIntegration(RequestsIntegrationTestBase, TestBase):
323334
@staticmethod

instrumentation/opentelemetry-instrumentation-urllib3/tests/test_urllib3_integration.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,9 @@ def url_filter(url):
287287

288288
response = self.perform_request(self.HTTP_URL + "?e=mcc")
289289
self.assert_success_span(response, self.HTTP_URL)
290+
291+
def test_credential_removal(self):
292+
url = "http://username:[email protected]/status/200"
293+
294+
response = self.perform_request(url)
295+
self.assert_success_span(response, self.HTTP_URL)

instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ install_requires =
4141
opentelemetry-api == 1.4.0.dev0
4242
opentelemetry-semantic-conventions == 0.23.dev0
4343
opentelemetry-instrumentation == 0.23.dev0
44+
opentelemetry-util-http == 0.23.dev0
4445

4546
[options.extras_require]
4647
test =

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def hello():
6565
from opentelemetry.propagators.textmap import Getter
6666
from opentelemetry.semconv.trace import SpanAttributes
6767
from opentelemetry.trace.status import Status, StatusCode
68+
from opentelemetry.util.http import remove_url_credentials
6869

6970
_HTTP_VERSION_PREFIX = "HTTP/"
7071
_CARRIER_KEY_PREFIX = "HTTP_"
@@ -128,7 +129,9 @@ def collect_request_attributes(environ):
128129
if target is not None:
129130
result[SpanAttributes.HTTP_TARGET] = target
130131
else:
131-
result[SpanAttributes.HTTP_URL] = wsgiref_util.request_uri(environ)
132+
result[SpanAttributes.HTTP_URL] = remove_url_credentials(
133+
wsgiref_util.request_uri(environ)
134+
)
132135

133136
remote_addr = environ.get("REMOTE_ADDR")
134137
if remote_addr:

instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,18 @@ def test_response_attributes(self):
364364
self.assertEqual(self.span.set_attribute.call_count, len(expected))
365365
self.span.set_attribute.assert_has_calls(expected, any_order=True)
366366

367+
def test_credential_removal(self):
368+
self.environ["HTTP_HOST"] = "username:[email protected]"
369+
self.environ["PATH_INFO"] = "/status/200"
370+
expected = {
371+
SpanAttributes.HTTP_URL: "http://httpbin.com/status/200",
372+
SpanAttributes.NET_HOST_PORT: 80,
373+
}
374+
self.assertGreaterEqual(
375+
otel_wsgi.collect_request_attributes(self.environ).items(),
376+
expected.items(),
377+
)
378+
367379

368380
class TestWsgiMiddlewareWithTracerProvider(WsgiTestBase):
369381
def validate_response(

tox.ini

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ commands_pre =
237237

238238
grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test]
239239

240-
falcon,flask,django,pyramid,tornado,starlette,fastapi: pip install {toxinidir}/util/opentelemetry-util-http[test]
240+
falcon,flask,django,pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
241241
wsgi,falcon,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
242242
asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
243243

@@ -413,6 +413,7 @@ deps =
413413
protobuf>=3.13.0
414414
requests==2.25.0
415415
pyodbc~=4.0.30
416+
416417
changedir =
417418
tests/opentelemetry-docker-tests/tests
418419

@@ -435,17 +436,17 @@ commands_pre =
435436
-e {toxinidir}/opentelemetry-python-core/exporter/opentelemetry-exporter-opencensus
436437
docker-compose up -d
437438
python check_availability.py
439+
438440
commands =
439441
pytest {posargs}
440442

441443
commands_post =
442444
docker-compose down -v
443445

444-
445446
[testenv:generate]
446447
deps =
447448
-r {toxinidir}/gen-requirements.txt
448449

449450
commands =
450451
{toxinidir}/scripts/generate_setup.py
451-
{toxinidir}/scripts/generate_instrumentation_bootstrap.py
452+
{toxinidir}/scripts/generate_instrumentation_bootstrap.py

util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from os import environ
1616
from re import compile as re_compile
1717
from re import search
18+
from urllib.parse import urlparse, urlunparse
1819

1920

2021
class ExcludeList:
@@ -57,3 +58,30 @@ def get_excluded_urls(instrumentation):
5758
]
5859

5960
return ExcludeList(excluded_urls)
61+
62+
63+
def remove_url_credentials(url: str) -> str:
64+
"""Given a string url, remove the username and password only if it is a valid url"""
65+
66+
try:
67+
parsed = urlparse(url)
68+
if all([parsed.scheme, parsed.netloc]): # checks for valid url
69+
parsed_url = urlparse(url)
70+
netloc = (
71+
(":".join(((parsed_url.hostname or ""), str(parsed_url.port))))
72+
if parsed_url.port
73+
else (parsed_url.hostname or "")
74+
)
75+
return urlunparse(
76+
(
77+
parsed_url.scheme,
78+
netloc,
79+
parsed_url.path,
80+
parsed_url.params,
81+
parsed_url.query,
82+
parsed_url.fragment,
83+
)
84+
)
85+
except ValueError: # an unparseable url was passed
86+
pass
87+
return url

0 commit comments

Comments
 (0)