From b2b62c01b14fd552df7627f2cbe998a998a6d6b1 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Wed, 6 Apr 2022 12:03:57 +0530 Subject: [PATCH 01/10] FastAPI: capture custom request response headers in span attributes --- .../instrumentation/asgi/__init__.py | 51 ++++ .../instrumentation/fastapi/__init__.py | 52 ++++ .../tests/test_fastapi_instrumentation.py | 271 +++++++++++++++++- 3 files changed, 373 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index d8932da996..66891744e5 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -91,6 +91,57 @@ def client_response_hook(span: Span, message: dict): OpenTelemetryMiddleware().(application, server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook) +Capture HTTP request and response headers +***************************************** +You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. + +Request headers +*************** +To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` +to a comma-separated list of HTTP header names. + +For example, + +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" + +will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. + +It is recommended that you should give the correct names of the headers to be captured in the environment variable. +Request header names in ASGI are case insensitive. So, giving header name as ``CUStom-Header`` in environment variable will be able capture header with name ``custom-header``. + +The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). +The value of the attribute will be single item list containing all the header values. + +Example of the added span attribute, +``http.request.header.custom_request_header = [","]`` + +Response headers +**************** +To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` +to a comma-separated list of HTTP header names. + +For example, + +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" + +will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. + +It is recommended that you should give the correct names of the headers to be captured in the environment variable. +Response header names captured in ASGI are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. + +The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). +The value of the attribute will be single item list containing all the header values. + +Example of the added span attribute, +``http.response.header.custom_response_header = [","]`` + +Note: + Environment variable names to caputre http headers are still experimental, and thus are subject to change. + API --- """ diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index 52e1c0682c..a5528fc41f 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -74,6 +74,58 @@ def client_response_hook(span: Span, message: dict): FastAPIInstrumentor().instrument(server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook) + +Capture HTTP request and response headers +***************************************** +You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. + +Request headers +*************** +To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` +to a comma-separated list of HTTP header names. + +For example, + +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" + +will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. + +It is recommended that you should give the correct names of the headers to be captured in the environment variable. +Request header names in FastAPI are case insensitive. So, giving header name as ``CUStom-Header`` in environment variable will be able capture header with name ``custom-header``. + +The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). +The value of the attribute will be single item list containing all the header values. + +Example of the added span attribute, +``http.request.header.custom_request_header = [","]`` + +Response headers +**************** +To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` +to a comma-separated list of HTTP header names. + +For example, + +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" + +will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. + +It is recommended that you should give the correct names of the headers to be captured in the environment variable. +Response header names captured in FastAPI are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. + +The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). +The value of the attribute will be single item list containing all the header values. + +Example of the added span attribute, +``http.response.header.custom_response_header = [","]`` + +Note: + Environment variable names to caputre http headers are still experimental, and thus are subject to change. + API --- """ diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index ae963e4f87..20d0a7d87e 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -16,6 +16,7 @@ from unittest.mock import patch import fastapi +from fastapi.responses import JSONResponse from fastapi.testclient import TestClient import opentelemetry.instrumentation.fastapi as otel_fastapi @@ -24,7 +25,11 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase -from opentelemetry.util.http import get_excluded_urls +from opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, + get_excluded_urls, +) class TestFastAPIManualInstrumentation(TestBase): @@ -375,3 +380,267 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self): self.assertEqual( parent_span.context.span_id, span_list[3].context.span_id ) + + +class TestCustomHeaders(TestFastAPIManualInstrumentation): + def setUp(self): + super().setUp() + self.env_patch = patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + }, + ) + self.env_patch.start() + self.app = self._create_http_app() + + otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) + self.client = TestClient(self.app) + + def tearDown(self) -> None: + super().tearDown() + self.env_patch.stop() + + @staticmethod + def _create_http_app(): + app = fastapi.FastAPI() + + @app.get("/foobar") + async def _(): + headers = { + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + } + content = {"message": "hello world"} + return JSONResponse(content=content, headers=headers) + + return app + + @staticmethod + def _create_websocket_app(): + app = fastapi.FastAPI() + + # @app.websocket("/foobar_web") + # async def _(websocket: fastapi.WebSocket): + # websocket.scope["headers"] + # await websocket.accept() + # await websocket.send_json({"message":"hello world"}) + # await websocket.close() + + @app.websocket("/foobar_web") + async def _(websocket: fastapi.WebSocket): + message = await websocket.receive() + if message.get("type") == "websocket.connect": + await websocket.send( + { + "type": "websocket.accept", + "headers": [ + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-2", b"test-header-value-2"), + ], + } + ) + await websocket.send_json({"message": "hello world"}) + await websocket.close() + if message.get("type") == "websocket.disconnect": + pass + + return app + + def test_http_custom_request_headers_in_span_attributes(self): + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + resp = self._client.get( + "/foobar", + headers={ + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + }, + ) + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + for span in span_list: + if span.kind == trace.SpanKind.SERVER: + self.assertSpanHasAttributes(span, expected) + + def test_http_custom_request_headers_not_in_span_attributes(self): + not_expected = { + "http.request.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + resp = self._client.get( + "/foobar", + headers={ + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + }, + ) + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + server_span = span_list[-1] + + if server_span.kind == trace.SpanKind.SERVER: + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + + def test_http_custom_response_headers_in_span_attributes(self): + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + resp = self.client.get("/foobar") + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + for span in span_list: + if span.kind == trace.SpanKind.SERVER: + self.assertSpanHasAttributes(span, expected) + + def test_http_custom_response_headers_not_in_span_attributes(self): + not_expected = { + "http.reponse.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + resp = self._client.get("/foobar") + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + server_span = span_list[-1] + + if server_span.kind == trace.SpanKind.SERVER: + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + + def test_web_socket_custom_request_headers_in_span_attributes(self): + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + self.app = self._create_websocket_app() + otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) + self._client = TestClient(self.app) + + with self._client.websocket_connect( + "/foobar_web", + headers={ + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + }, + ) as websocket: + + data = websocket.receive_json() + self.assertEqual(data, {"message": "hello world"}) + + span_list = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(span_list), 5) + for span in span_list: + if span.kind == trace.SpanKind.SERVER: + self.assertSpanHasAttributes(span, expected) + + def test_web_socket_custom_request_headers_not_in_span_attributes(self): + not_expected = { + "http.request.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + self.app = self._create_websocket_app() + otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) + self._client = TestClient(self.app) + + with self._client.websocket_connect( + "/foobar_web", + headers={ + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + }, + ) as websocket: + + data = websocket.receive_json() + self.assertEqual(data, {"message": "hello world"}) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) + + server_span = [ + span + for span in span_list + if span.kind == trace.SpanKind.SERVER + ][0] + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + + def test_web_socket_custom_response_headers_in_span_attributes(self): + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + self.app = self._create_websocket_app() + otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) + self._client = TestClient(self.app) + + with self._client.websocket_connect("/foobar_web") as websocket: + data = websocket.receive_json() + self.assertEqual(data, {"message": "hello world"}) + + span_list = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(span_list), 5) + for span in span_list: + if span.kind == trace.SpanKind.SERVER: + self.assertSpanHasAttributes(span, expected) + + def test_web_socket_custom_response_headers_not_in_span_attributes(self): + not_expected = { + "http.reponse.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + self.app = self._create_websocket_app() + otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) + self._client = TestClient(self.app) + + with self._client.websocket_connect("/foobar_web") as websocket: + data = websocket.receive_json() + self.assertEqual(data, {"message": "hello world"}) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) + + server_spans = [ + span + for span in span_list + if span.kind == trace.SpanKind.SERVER + ] + self.assertEqual(len(server_spans), 1) + server_span = server_spans[0] + + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) From dc1dd2957a320f4f03ccbd62abd9f0ba76d7e0a0 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Wed, 6 Apr 2022 12:13:33 +0530 Subject: [PATCH 02/10] added entry in changelog.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b10bcdda47..8680bb746f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.10.0-0.29b0...HEAD) ### Added +- `opentelemetry-instrumentation-fastapi` Capture custom request/response headers in span attributes + ([#1032])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1032) - `opentelemetry-instrumentation-django` Capture custom request/response headers in span attributes ([#1024])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1024) - `opentelemetry-instrumentation-asgi` Capture custom request/response headers in span attributes From d198cd3c7c416cc977803a7dbf1f97a287fde65e Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Thu, 7 Apr 2022 20:49:06 +0530 Subject: [PATCH 03/10] Resolved span no error for pypy3. Created separate test class to test websocket app with custom request/response headers --- .../tests/test_fastapi_instrumentation.py | 185 +++++++++++------- 1 file changed, 109 insertions(+), 76 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 20d0a7d87e..0539419db0 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -382,7 +382,7 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self): ) -class TestCustomHeaders(TestFastAPIManualInstrumentation): +class TestHTTPAppWithCustomHeaders(TestBase): def setUp(self): super().setUp() self.env_patch = patch.dict( @@ -394,7 +394,6 @@ def setUp(self): ) self.env_patch.start() self.app = self._create_http_app() - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) @@ -417,17 +416,9 @@ async def _(): return app - @staticmethod - def _create_websocket_app(): + app = fastapi.FastAPI() - # @app.websocket("/foobar_web") - # async def _(websocket: fastapi.WebSocket): - # websocket.scope["headers"] - # await websocket.accept() - # await websocket.send_json({"message":"hello world"}) - # await websocket.close() - @app.websocket("/foobar_web") async def _(websocket: fastapi.WebSocket): message = await websocket.receive() @@ -457,7 +448,7 @@ def test_http_custom_request_headers_in_span_attributes(self): "test-header-value-2", ), } - resp = self._client.get( + resp = self.client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", @@ -468,9 +459,12 @@ def test_http_custom_request_headers_in_span_attributes(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) - for span in span_list: - if span.kind == trace.SpanKind.SERVER: - self.assertSpanHasAttributes(span, expected) + server_span = [ + span for span in span_list + if span.kind == trace.SpanKind.SERVER + ][0] + + self.assertSpanHasAttributes(server_span, expected) def test_http_custom_request_headers_not_in_span_attributes(self): not_expected = { @@ -478,7 +472,7 @@ def test_http_custom_request_headers_not_in_span_attributes(self): "test-header-value-3", ), } - resp = self._client.get( + resp = self.client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", @@ -489,11 +483,13 @@ def test_http_custom_request_headers_not_in_span_attributes(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) - server_span = span_list[-1] + server_span = [ + span for span in span_list + if span.kind == trace.SpanKind.SERVER + ][0] - if server_span.kind == trace.SpanKind.SERVER: - for key, _ in not_expected.items(): - self.assertNotIn(key, server_span.attributes) + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) def test_http_custom_response_headers_in_span_attributes(self): expected = { @@ -509,9 +505,11 @@ def test_http_custom_response_headers_in_span_attributes(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) - for span in span_list: - if span.kind == trace.SpanKind.SERVER: - self.assertSpanHasAttributes(span, expected) + server_span = [ + span for span in span_list + if span.kind == trace.SpanKind.SERVER + ][0] + self.assertSpanHasAttributes(server_span, expected) def test_http_custom_response_headers_not_in_span_attributes(self): not_expected = { @@ -519,17 +517,63 @@ def test_http_custom_response_headers_not_in_span_attributes(self): "test-header-value-3", ), } - resp = self._client.get("/foobar") + resp = self.client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) - server_span = span_list[-1] + server_span = [ + span for span in span_list + if span.kind == trace.SpanKind.SERVER + ][0] + + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + + + +class TestWebSocketAppWithCustomHeaders(TestBase): + def setUp(self): + super().setUp() + self.env_patch = patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + }, + ) + self.env_patch.start() + self.app = self._create_app() + otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) + self.client = TestClient(self.app) + + def _create_app(self): + app = fastapi.FastAPI() + + @app.websocket("/foobar_web") + async def _(websocket: fastapi.WebSocket): + message = await websocket.receive() + if message.get("type") == "websocket.connect": + await websocket.send( + { + "type": "websocket.accept", + "headers": [ + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-2", b"test-header-value-2"), + ], + } + ) + await websocket.send_json({"message": "hello world"}) + await websocket.close() + if message.get("type") == "websocket.disconnect": + pass - if server_span.kind == trace.SpanKind.SERVER: - for key, _ in not_expected.items(): - self.assertNotIn(key, server_span.attributes) + return app + def tearDown(self) -> None: + super().tearDown() + self.env_patch.stop() + def test_web_socket_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ( @@ -539,27 +583,26 @@ def test_web_socket_custom_request_headers_in_span_attributes(self): "test-header-value-2", ), } - self.app = self._create_websocket_app() - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) - self._client = TestClient(self.app) - with self._client.websocket_connect( + with self.client.websocket_connect( "/foobar_web", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", }, ) as websocket: - data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) - span_list = self.memory_exporter.get_finished_spans() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) + + server_span = [ + span for span in span_list + if span.kind == trace.SpanKind.SERVER + ][0] - self.assertEqual(len(span_list), 5) - for span in span_list: - if span.kind == trace.SpanKind.SERVER: - self.assertSpanHasAttributes(span, expected) + self.assertSpanHasAttributes(server_span, expected) def test_web_socket_custom_request_headers_not_in_span_attributes(self): not_expected = { @@ -567,31 +610,27 @@ def test_web_socket_custom_request_headers_not_in_span_attributes(self): "test-header-value-3", ), } - self.app = self._create_websocket_app() - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) - self._client = TestClient(self.app) - with self._client.websocket_connect( + with self.client.websocket_connect( "/foobar_web", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", }, ) as websocket: - data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 5) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) + + server_span = [ + span for span in span_list + if span.kind == trace.SpanKind.SERVER + ][0] - server_span = [ - span - for span in span_list - if span.kind == trace.SpanKind.SERVER - ][0] - for key, _ in not_expected.items(): - self.assertNotIn(key, server_span.attributes) + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) def test_web_socket_custom_response_headers_in_span_attributes(self): expected = { @@ -602,20 +641,20 @@ def test_web_socket_custom_response_headers_in_span_attributes(self): "test-header-value-2", ), } - self.app = self._create_websocket_app() - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) - self._client = TestClient(self.app) - with self._client.websocket_connect("/foobar_web") as websocket: + with self.client.websocket_connect("/foobar_web") as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) - span_list = self.memory_exporter.get_finished_spans() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) - self.assertEqual(len(span_list), 5) - for span in span_list: - if span.kind == trace.SpanKind.SERVER: - self.assertSpanHasAttributes(span, expected) + server_span = [ + span for span in span_list + if span.kind == trace.SpanKind.SERVER + ][0] + + self.assertSpanHasAttributes(server_span, expected) def test_web_socket_custom_response_headers_not_in_span_attributes(self): not_expected = { @@ -623,24 +662,18 @@ def test_web_socket_custom_response_headers_not_in_span_attributes(self): "test-header-value-3", ), } - self.app = self._create_websocket_app() - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) - self._client = TestClient(self.app) - with self._client.websocket_connect("/foobar_web") as websocket: + with self.client.websocket_connect("/foobar_web") as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 5) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) - server_spans = [ - span - for span in span_list - if span.kind == trace.SpanKind.SERVER - ] - self.assertEqual(len(server_spans), 1) - server_span = server_spans[0] + server_span = [ + span for span in span_list + if span.kind == trace.SpanKind.SERVER + ][0] - for key, _ in not_expected.items(): - self.assertNotIn(key, server_span.attributes) + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) From 6fb339c4a22261b4310d6f6314f0e21d9f100395 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Fri, 8 Apr 2022 00:07:40 +0530 Subject: [PATCH 04/10] resolving syntax and linting errors --- .../tests/test_fastapi_instrumentation.py | 59 +++++-------------- 1 file changed, 14 insertions(+), 45 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 0539419db0..54cfa4a2f5 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -393,7 +393,7 @@ def setUp(self): }, ) self.env_patch.start() - self.app = self._create_http_app() + self.app = self._create_app() otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) @@ -402,7 +402,7 @@ def tearDown(self) -> None: self.env_patch.stop() @staticmethod - def _create_http_app(): + def _create_app(): app = fastapi.FastAPI() @app.get("/foobar") @@ -416,29 +416,6 @@ async def _(): return app - - app = fastapi.FastAPI() - - @app.websocket("/foobar_web") - async def _(websocket: fastapi.WebSocket): - message = await websocket.receive() - if message.get("type") == "websocket.connect": - await websocket.send( - { - "type": "websocket.accept", - "headers": [ - (b"custom-test-header-1", b"test-header-value-1"), - (b"custom-test-header-2", b"test-header-value-2"), - ], - } - ) - await websocket.send_json({"message": "hello world"}) - await websocket.close() - if message.get("type") == "websocket.disconnect": - pass - - return app - def test_http_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ( @@ -460,10 +437,9 @@ def test_http_custom_request_headers_in_span_attributes(self): self.assertEqual(len(span_list), 3) server_span = [ - span for span in span_list - if span.kind == trace.SpanKind.SERVER + span for span in span_list if span.kind == trace.SpanKind.SERVER ][0] - + self.assertSpanHasAttributes(server_span, expected) def test_http_custom_request_headers_not_in_span_attributes(self): @@ -484,8 +460,7 @@ def test_http_custom_request_headers_not_in_span_attributes(self): self.assertEqual(len(span_list), 3) server_span = [ - span for span in span_list - if span.kind == trace.SpanKind.SERVER + span for span in span_list if span.kind == trace.SpanKind.SERVER ][0] for key, _ in not_expected.items(): @@ -506,8 +481,7 @@ def test_http_custom_response_headers_in_span_attributes(self): self.assertEqual(len(span_list), 3) server_span = [ - span for span in span_list - if span.kind == trace.SpanKind.SERVER + span for span in span_list if span.kind == trace.SpanKind.SERVER ][0] self.assertSpanHasAttributes(server_span, expected) @@ -523,14 +497,12 @@ def test_http_custom_response_headers_not_in_span_attributes(self): self.assertEqual(len(span_list), 3) server_span = [ - span for span in span_list - if span.kind == trace.SpanKind.SERVER + span for span in span_list if span.kind == trace.SpanKind.SERVER ][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) - class TestWebSocketAppWithCustomHeaders(TestBase): def setUp(self): @@ -547,7 +519,8 @@ def setUp(self): otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) - def _create_app(self): + @staticmethod + def _create_app(): app = fastapi.FastAPI() @app.websocket("/foobar_web") @@ -573,7 +546,7 @@ async def _(websocket: fastapi.WebSocket): def tearDown(self) -> None: super().tearDown() self.env_patch.stop() - + def test_web_socket_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ( @@ -598,8 +571,7 @@ def test_web_socket_custom_request_headers_in_span_attributes(self): self.assertEqual(len(span_list), 5) server_span = [ - span for span in span_list - if span.kind == trace.SpanKind.SERVER + span for span in span_list if span.kind == trace.SpanKind.SERVER ][0] self.assertSpanHasAttributes(server_span, expected) @@ -625,8 +597,7 @@ def test_web_socket_custom_request_headers_not_in_span_attributes(self): self.assertEqual(len(span_list), 5) server_span = [ - span for span in span_list - if span.kind == trace.SpanKind.SERVER + span for span in span_list if span.kind == trace.SpanKind.SERVER ][0] for key, _ in not_expected.items(): @@ -650,8 +621,7 @@ def test_web_socket_custom_response_headers_in_span_attributes(self): self.assertEqual(len(span_list), 5) server_span = [ - span for span in span_list - if span.kind == trace.SpanKind.SERVER + span for span in span_list if span.kind == trace.SpanKind.SERVER ][0] self.assertSpanHasAttributes(server_span, expected) @@ -671,8 +641,7 @@ def test_web_socket_custom_response_headers_not_in_span_attributes(self): self.assertEqual(len(span_list), 5) server_span = [ - span for span in span_list - if span.kind == trace.SpanKind.SERVER + span for span in span_list if span.kind == trace.SpanKind.SERVER ][0] for key, _ in not_expected.items(): From 1c6688c42f9e69a1ce35ac247b2659ec5378a98e Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Fri, 8 Apr 2022 16:52:17 +0530 Subject: [PATCH 05/10] uninstrumenting app in teardown function in test classes --- .../tests/test_fastapi_instrumentation.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 54cfa4a2f5..172abb03f0 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -400,6 +400,8 @@ def setUp(self): def tearDown(self) -> None: super().tearDown() self.env_patch.stop() + with self.disable_logging(): + otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) @staticmethod def _create_app(): @@ -519,6 +521,12 @@ def setUp(self): otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) + def tearDown(self) -> None: + super().tearDown() + self.env_patch.stop() + with self.disable_logging(): + otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) + @staticmethod def _create_app(): app = fastapi.FastAPI() @@ -543,10 +551,6 @@ async def _(websocket: fastapi.WebSocket): return app - def tearDown(self) -> None: - super().tearDown() - self.env_patch.stop() - def test_web_socket_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ( From aa2e7fcb55e8d20d3e124dbfc3e676486d4ec25a Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Fri, 8 Apr 2022 19:57:00 +0530 Subject: [PATCH 06/10] adding test case for no op tracer provider --- .../tests/test_fastapi_instrumentation.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 172abb03f0..e154617af4 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -25,6 +25,7 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase +from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, @@ -650,3 +651,47 @@ def test_web_socket_custom_response_headers_not_in_span_attributes(self): for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) + + +class TestNonRecordingSpanWithCustomHeaders(TestBase): + def setUp(self): + super().setUp() + self.env_patch = patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + }, + ) + self.env_patch.start() + self.app = fastapi.FastAPI() + + @self.app.get("/foobar") + async def _(): + return {"message": "hello world"} + + reset_trace_globals() + tracer_provider = trace.NoOpTracerProvider() + trace.set_tracer_provider(tracer_provider=tracer_provider) + + self._instrumentor = otel_fastapi.FastAPIInstrumentor() + self._instrumentor.instrument_app(self.app) + self.client = TestClient(self.app) + + def tearDown(self) -> None: + super().tearDown() + with self.disable_logging(): + self._instrumentor.uninstrument_app(self.app) + + def test_custom_header_not_present_in_non_recording_span(self): + try: + resp = self.client.get( + "/foobar", + headers={ + "custom-test-header-1": "test-header-value-1", + }, + ) + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + except Exception as e: + self.fail(f"Exception raised in Non recording span: {str(e)}") From 1f96d495a0c73f4a5dbf588315ae706a2cf52422 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Fri, 8 Apr 2022 20:01:32 +0530 Subject: [PATCH 07/10] resolving generate command errors --- .../tests/test_fastapi_instrumentation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index e154617af4..baf5ccd05e 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -24,8 +24,8 @@ from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.sdk.resources import Resource from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.test.test_base import TestBase from opentelemetry.test.globals_test import reset_trace_globals +from opentelemetry.test.test_base import TestBase from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, @@ -668,7 +668,7 @@ def setUp(self): @self.app.get("/foobar") async def _(): return {"message": "hello world"} - + reset_trace_globals() tracer_provider = trace.NoOpTracerProvider() trace.set_tracer_provider(tracer_provider=tracer_provider) @@ -681,7 +681,7 @@ def tearDown(self) -> None: super().tearDown() with self.disable_logging(): self._instrumentor.uninstrument_app(self.app) - + def test_custom_header_not_present_in_non_recording_span(self): try: resp = self.client.get( From ca952e33b50723e464879eafe5d31a69f261fbb2 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Fri, 8 Apr 2022 21:05:39 +0530 Subject: [PATCH 08/10] resolving linting errors --- .../tests/test_fastapi_instrumentation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index baf5ccd05e..ad129aaba7 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -693,5 +693,5 @@ def test_custom_header_not_present_in_non_recording_span(self): self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) - except Exception as e: - self.fail(f"Exception raised in Non recording span: {str(e)}") + except Exception as exc: + self.fail(f"Exception raised in Non recording span: {str(exc)}") From 88273b834323a5c8057092f4e78b2e81b696ac6d Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Fri, 8 Apr 2022 22:11:31 +0530 Subject: [PATCH 09/10] resolving linting errors --- .../tests/test_fastapi_instrumentation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index ad129aaba7..9f6ffec848 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -683,7 +683,6 @@ def tearDown(self) -> None: self._instrumentor.uninstrument_app(self.app) def test_custom_header_not_present_in_non_recording_span(self): - try: resp = self.client.get( "/foobar", headers={ @@ -693,5 +692,3 @@ def test_custom_header_not_present_in_non_recording_span(self): self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) - except Exception as exc: - self.fail(f"Exception raised in Non recording span: {str(exc)}") From 7d020832ab2e5f40966e7a9d4bfc8a0e9d8736aa Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Fri, 8 Apr 2022 22:18:04 +0530 Subject: [PATCH 10/10] resolving linting errors --- .../tests/test_fastapi_instrumentation.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 9f6ffec848..e4a0960a26 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -683,12 +683,12 @@ def tearDown(self) -> None: self._instrumentor.uninstrument_app(self.app) def test_custom_header_not_present_in_non_recording_span(self): - resp = self.client.get( - "/foobar", - headers={ - "custom-test-header-1": "test-header-value-1", - }, - ) - self.assertEqual(200, resp.status_code) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) + resp = self.client.get( + "/foobar", + headers={ + "custom-test-header-1": "test-header-value-1", + }, + ) + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0)