|
12 | 12 | # See the License for the specific language governing permissions and
|
13 | 13 | # limitations under the License.
|
14 | 14 | import unittest
|
15 |
| -from collections.abc import Mapping |
16 | 15 | from timeit import default_timer
|
17 |
| -from typing import Tuple |
18 | 16 | from unittest.mock import patch
|
19 | 17 |
|
20 | 18 | import fastapi
|
21 | 19 | from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
|
22 | 20 | from fastapi.testclient import TestClient
|
23 | 21 |
|
24 | 22 | import opentelemetry.instrumentation.fastapi as otel_fastapi
|
25 |
| -from opentelemetry import trace |
26 | 23 | from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
|
27 | 24 | from opentelemetry.sdk.metrics.export import (
|
28 | 25 | HistogramDataPoint,
|
|
57 | 54 | }
|
58 | 55 |
|
59 | 56 |
|
60 |
| -class TestFastAPIManualInstrumentation(TestBase): |
| 57 | +class BaseFastAPI: |
61 | 58 | def _create_app(self):
|
62 | 59 | app = self._create_fastapi_app()
|
63 | 60 | self._instrumentor.instrument_app(
|
@@ -105,6 +102,137 @@ def tearDown(self):
|
105 | 102 | self._instrumentor.uninstrument()
|
106 | 103 | self._instrumentor.uninstrument_app(self._app)
|
107 | 104 |
|
| 105 | + @staticmethod |
| 106 | + def _create_fastapi_app(): |
| 107 | + app = fastapi.FastAPI() |
| 108 | + sub_app = fastapi.FastAPI() |
| 109 | + |
| 110 | + @sub_app.get("/home") |
| 111 | + async def _(): |
| 112 | + return {"message": "sub hi"} |
| 113 | + |
| 114 | + @app.get("/foobar") |
| 115 | + async def _(): |
| 116 | + return {"message": "hello world"} |
| 117 | + |
| 118 | + @app.get("/user/{username}") |
| 119 | + async def _(username: str): |
| 120 | + return {"message": username} |
| 121 | + |
| 122 | + @app.get("/exclude/{param}") |
| 123 | + async def _(param: str): |
| 124 | + return {"message": param} |
| 125 | + |
| 126 | + @app.get("/healthzz") |
| 127 | + async def _(): |
| 128 | + return {"message": "ok"} |
| 129 | + |
| 130 | + app.mount("/sub", app=sub_app) |
| 131 | + |
| 132 | + return app |
| 133 | + |
| 134 | + |
| 135 | +class BaseManualFastAPI(BaseFastAPI): |
| 136 | + |
| 137 | + def test_sub_app_fastapi_call(self): |
| 138 | + """ |
| 139 | + This test is to ensure that a span in case of a sub app targeted contains the correct server url |
| 140 | +
|
| 141 | + As this test case covers manual instrumentation, we won't see any additional spans for the sub app. |
| 142 | + In this case all generated spans might suffice the requirements for the attributes already |
| 143 | + (as the testcase is not setting a root_path for the outer app here) |
| 144 | + """ |
| 145 | + |
| 146 | + self._client.get("/sub/home") |
| 147 | + spans = self.memory_exporter.get_finished_spans() |
| 148 | + self.assertEqual(len(spans), 3) |
| 149 | + for span in spans: |
| 150 | + # As we are only looking to the "outer" app, we would see only the "GET /sub" spans |
| 151 | + self.assertIn("GET /sub", span.name) |
| 152 | + |
| 153 | + # We now want to specifically test all spans including the |
| 154 | + # - HTTP_TARGET |
| 155 | + # - HTTP_URL |
| 156 | + # attributes to be populated with the expected values |
| 157 | + spans_with_http_attributes = [ |
| 158 | + span |
| 159 | + for span in spans |
| 160 | + if ( |
| 161 | + SpanAttributes.HTTP_URL in span.attributes |
| 162 | + or SpanAttributes.HTTP_TARGET in span.attributes |
| 163 | + ) |
| 164 | + ] |
| 165 | + |
| 166 | + # We expect only one span to have the HTTP attributes set (the SERVER span from the app itself) |
| 167 | + # the sub app is not instrumented with manual instrumentation tests. |
| 168 | + self.assertEqual(1, len(spans_with_http_attributes)) |
| 169 | + |
| 170 | + for span in spans_with_http_attributes: |
| 171 | + self.assertEqual( |
| 172 | + "/sub/home", span.attributes[SpanAttributes.HTTP_TARGET] |
| 173 | + ) |
| 174 | + self.assertEqual( |
| 175 | + "https://testserver:443/sub/home", |
| 176 | + span.attributes[SpanAttributes.HTTP_URL], |
| 177 | + ) |
| 178 | + |
| 179 | + |
| 180 | +class BaseAutoFastAPI(BaseFastAPI): |
| 181 | + |
| 182 | + def test_sub_app_fastapi_call(self): |
| 183 | + """ |
| 184 | + This test is to ensure that a span in case of a sub app targeted contains the correct server url |
| 185 | +
|
| 186 | + As this test case covers auto instrumentation, we will see additional spans for the sub app. |
| 187 | + In this case all generated spans might suffice the requirements for the attributes already |
| 188 | + (as the testcase is not setting a root_path for the outer app here) |
| 189 | + """ |
| 190 | + |
| 191 | + self._client.get("/sub/home") |
| 192 | + spans = self.memory_exporter.get_finished_spans() |
| 193 | + self.assertEqual(len(spans), 6) |
| 194 | + |
| 195 | + for span in spans: |
| 196 | + # As we are only looking to the "outer" app, we would see only the "GET /sub" spans |
| 197 | + # -> the outer app is not aware of the sub_apps internal routes |
| 198 | + sub_in = "GET /sub" in span.name |
| 199 | + # The sub app spans are named GET /home as from the sub app perspective the request targets /home |
| 200 | + # -> the sub app is technically not aware of the /sub prefix |
| 201 | + home_in = "GET /home" in span.name |
| 202 | + |
| 203 | + # We expect the spans to be either from the outer app or the sub app |
| 204 | + self.assertTrue( |
| 205 | + sub_in or home_in, |
| 206 | + f"Span {span.name} does not have /sub or /home in its name", |
| 207 | + ) |
| 208 | + |
| 209 | + # We now want to specifically test all spans including the |
| 210 | + # - HTTP_TARGET |
| 211 | + # - HTTP_URL |
| 212 | + # attributes to be populated with the expected values |
| 213 | + spans_with_http_attributes = [ |
| 214 | + span |
| 215 | + for span in spans |
| 216 | + if ( |
| 217 | + SpanAttributes.HTTP_URL in span.attributes |
| 218 | + or SpanAttributes.HTTP_TARGET in span.attributes |
| 219 | + ) |
| 220 | + ] |
| 221 | + |
| 222 | + # We now expect spans with attributes from both the app and its sub app |
| 223 | + self.assertEqual(2, len(spans_with_http_attributes)) |
| 224 | + |
| 225 | + for span in spans_with_http_attributes: |
| 226 | + self.assertEqual( |
| 227 | + "/sub/home", span.attributes[SpanAttributes.HTTP_TARGET] |
| 228 | + ) |
| 229 | + self.assertEqual( |
| 230 | + "https://testserver:443/sub/home", |
| 231 | + span.attributes[SpanAttributes.HTTP_URL], |
| 232 | + ) |
| 233 | + |
| 234 | + |
| 235 | +class TestFastAPIManualInstrumentation(BaseManualFastAPI, TestBase): |
108 | 236 | def test_instrument_app_with_instrument(self):
|
109 | 237 | if not isinstance(self, TestAutoInstrumentation):
|
110 | 238 | self._instrumentor.instrument()
|
@@ -314,48 +442,6 @@ def test_metric_uninstrument(self):
|
314 | 442 | if isinstance(point, NumberDataPoint):
|
315 | 443 | self.assertEqual(point.value, 0)
|
316 | 444 |
|
317 |
| - def test_sub_app_fastapi_call(self): |
318 |
| - """ |
319 |
| - This test is to ensure that a span in case of a sub app targeted contains the correct server url |
320 |
| -
|
321 |
| - As this test case covers manual instrumentation, we won't see any additional spans for the sub app. |
322 |
| - In this case all generated spans might suffice the requirements for the attributes already |
323 |
| - (as the testcase is not setting a root_path for the outer app here) |
324 |
| - """ |
325 |
| - |
326 |
| - self._client.get("/sub/home") |
327 |
| - spans = self.memory_exporter.get_finished_spans() |
328 |
| - self.assertEqual(len(spans), 3) |
329 |
| - for span in spans: |
330 |
| - # As we are only looking to the "outer" app, we would see only the "GET /sub" spans |
331 |
| - self.assertIn("GET /sub", span.name) |
332 |
| - |
333 |
| - # We now want to specifically test all spans including the |
334 |
| - # - HTTP_TARGET |
335 |
| - # - HTTP_URL |
336 |
| - # attributes to be populated with the expected values |
337 |
| - spans_with_http_attributes = [ |
338 |
| - span |
339 |
| - for span in spans |
340 |
| - if ( |
341 |
| - SpanAttributes.HTTP_URL in span.attributes |
342 |
| - or SpanAttributes.HTTP_TARGET in span.attributes |
343 |
| - ) |
344 |
| - ] |
345 |
| - |
346 |
| - # We expect only one span to have the HTTP attributes set (the SERVER span from the app itself) |
347 |
| - # the sub app is not instrumented with manual instrumentation tests. |
348 |
| - self.assertEqual(1, len(spans_with_http_attributes)) |
349 |
| - |
350 |
| - for span in spans_with_http_attributes: |
351 |
| - self.assertEqual( |
352 |
| - "/sub/home", span.attributes[SpanAttributes.HTTP_TARGET] |
353 |
| - ) |
354 |
| - self.assertEqual( |
355 |
| - "https://testserver:443/sub/home", |
356 |
| - span.attributes[SpanAttributes.HTTP_URL], |
357 |
| - ) |
358 |
| - |
359 | 445 | @staticmethod
|
360 | 446 | def _create_fastapi_app():
|
361 | 447 | app = fastapi.FastAPI()
|
@@ -386,7 +472,7 @@ async def _():
|
386 | 472 | return app
|
387 | 473 |
|
388 | 474 |
|
389 |
| -class TestFastAPIManualInstrumentationHooks(TestFastAPIManualInstrumentation): |
| 475 | +class TestFastAPIManualInstrumentationHooks(BaseManualFastAPI, TestBase): |
390 | 476 | _server_request_hook = None
|
391 | 477 | _client_request_hook = None
|
392 | 478 | _client_response_hook = None
|
@@ -436,7 +522,7 @@ def client_response_hook(send_span, scope, message):
|
436 | 522 | )
|
437 | 523 |
|
438 | 524 |
|
439 |
| -class TestAutoInstrumentation(TestFastAPIManualInstrumentation): |
| 525 | +class TestAutoInstrumentation(BaseAutoFastAPI, TestBase): |
440 | 526 | """Test the auto-instrumented variant
|
441 | 527 |
|
442 | 528 | Extending the manual instrumentation as most test cases apply
|
@@ -550,7 +636,7 @@ def test_sub_app_fastapi_call(self):
|
550 | 636 | )
|
551 | 637 |
|
552 | 638 |
|
553 |
| -class TestAutoInstrumentationHooks(TestFastAPIManualInstrumentationHooks): |
| 639 | +class TestAutoInstrumentationHooks(BaseAutoFastAPI, TestBase): |
554 | 640 | """
|
555 | 641 | Test the auto-instrumented variant for request and response hooks
|
556 | 642 |
|
@@ -659,412 +745,3 @@ def test_instrumentation(self):
|
659 | 745 |
|
660 | 746 | should_be_original = fastapi.FastAPI
|
661 | 747 | self.assertIs(original, should_be_original)
|
662 |
| - |
663 |
| - |
664 |
| -class TestWrappedApplication(TestBase): |
665 |
| - def setUp(self): |
666 |
| - super().setUp() |
667 |
| - |
668 |
| - self.app = fastapi.FastAPI() |
669 |
| - |
670 |
| - @self.app.get("/foobar") |
671 |
| - async def _(): |
672 |
| - return {"message": "hello world"} |
673 |
| - |
674 |
| - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) |
675 |
| - self.client = TestClient(self.app) |
676 |
| - self.tracer = self.tracer_provider.get_tracer(__name__) |
677 |
| - |
678 |
| - def tearDown(self) -> None: |
679 |
| - super().tearDown() |
680 |
| - with self.disable_logging(): |
681 |
| - otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) |
682 |
| - |
683 |
| - def test_mark_span_internal_in_presence_of_span_from_other_framework(self): |
684 |
| - with self.tracer.start_as_current_span( |
685 |
| - "test", kind=trace.SpanKind.SERVER |
686 |
| - ) as parent_span: |
687 |
| - resp = self.client.get("/foobar") |
688 |
| - self.assertEqual(200, resp.status_code) |
689 |
| - |
690 |
| - span_list = self.memory_exporter.get_finished_spans() |
691 |
| - for span in span_list: |
692 |
| - print(str(span.__class__) + ": " + str(span.__dict__)) |
693 |
| - |
694 |
| - # there should be 4 spans - single SERVER "test" and three INTERNAL "FastAPI" |
695 |
| - self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) |
696 |
| - self.assertEqual(trace.SpanKind.INTERNAL, span_list[1].kind) |
697 |
| - # main INTERNAL span - child of test |
698 |
| - self.assertEqual(trace.SpanKind.INTERNAL, span_list[2].kind) |
699 |
| - self.assertEqual( |
700 |
| - parent_span.context.span_id, span_list[2].parent.span_id |
701 |
| - ) |
702 |
| - # SERVER "test" |
703 |
| - self.assertEqual(trace.SpanKind.SERVER, span_list[3].kind) |
704 |
| - self.assertEqual( |
705 |
| - parent_span.context.span_id, span_list[3].context.span_id |
706 |
| - ) |
707 |
| - |
708 |
| - |
709 |
| -class MultiMapping(Mapping): |
710 |
| - |
711 |
| - def __init__(self, *items: Tuple[str, str]): |
712 |
| - self._items = items |
713 |
| - |
714 |
| - def __len__(self): |
715 |
| - return len(self._items) |
716 |
| - |
717 |
| - def __getitem__(self, __key): |
718 |
| - raise NotImplementedError("use .items() instead") |
719 |
| - |
720 |
| - def __iter__(self): |
721 |
| - raise NotImplementedError("use .items() instead") |
722 |
| - |
723 |
| - def items(self): |
724 |
| - return self._items |
725 |
| - |
726 |
| - |
727 |
| -@patch.dict( |
728 |
| - "os.environ", |
729 |
| - { |
730 |
| - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", |
731 |
| - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", |
732 |
| - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", |
733 |
| - }, |
734 |
| -) |
735 |
| -class TestHTTPAppWithCustomHeaders(TestBase): |
736 |
| - def setUp(self): |
737 |
| - super().setUp() |
738 |
| - self.app = self._create_app() |
739 |
| - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) |
740 |
| - self.client = TestClient(self.app) |
741 |
| - |
742 |
| - def tearDown(self) -> None: |
743 |
| - super().tearDown() |
744 |
| - with self.disable_logging(): |
745 |
| - otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) |
746 |
| - |
747 |
| - @staticmethod |
748 |
| - def _create_app(): |
749 |
| - app = fastapi.FastAPI() |
750 |
| - |
751 |
| - @app.get("/foobar") |
752 |
| - async def _(): |
753 |
| - headers = MultiMapping( |
754 |
| - ("custom-test-header-1", "test-header-value-1"), |
755 |
| - ("custom-test-header-2", "test-header-value-2"), |
756 |
| - ("my-custom-regex-header-1", "my-custom-regex-value-1"), |
757 |
| - ("my-custom-regex-header-1", "my-custom-regex-value-2"), |
758 |
| - ("My-Custom-Regex-Header-2", "my-custom-regex-value-3"), |
759 |
| - ("My-Custom-Regex-Header-2", "my-custom-regex-value-4"), |
760 |
| - ("My-Secret-Header", "My Secret Value"), |
761 |
| - ) |
762 |
| - content = {"message": "hello world"} |
763 |
| - return JSONResponse(content=content, headers=headers) |
764 |
| - |
765 |
| - return app |
766 |
| - |
767 |
| - def test_http_custom_request_headers_in_span_attributes(self): |
768 |
| - expected = { |
769 |
| - "http.request.header.custom_test_header_1": ( |
770 |
| - "test-header-value-1", |
771 |
| - ), |
772 |
| - "http.request.header.custom_test_header_2": ( |
773 |
| - "test-header-value-2", |
774 |
| - ), |
775 |
| - "http.request.header.regex_test_header_1": ("Regex Test Value 1",), |
776 |
| - "http.request.header.regex_test_header_2": ( |
777 |
| - "RegexTestValue2,RegexTestValue3", |
778 |
| - ), |
779 |
| - "http.request.header.my_secret_header": ("[REDACTED]",), |
780 |
| - } |
781 |
| - resp = self.client.get( |
782 |
| - "/foobar", |
783 |
| - headers={ |
784 |
| - "custom-test-header-1": "test-header-value-1", |
785 |
| - "custom-test-header-2": "test-header-value-2", |
786 |
| - "Regex-Test-Header-1": "Regex Test Value 1", |
787 |
| - "regex-test-header-2": "RegexTestValue2,RegexTestValue3", |
788 |
| - "My-Secret-Header": "My Secret Value", |
789 |
| - }, |
790 |
| - ) |
791 |
| - self.assertEqual(200, resp.status_code) |
792 |
| - span_list = self.memory_exporter.get_finished_spans() |
793 |
| - self.assertEqual(len(span_list), 3) |
794 |
| - |
795 |
| - server_span = [ |
796 |
| - span for span in span_list if span.kind == trace.SpanKind.SERVER |
797 |
| - ][0] |
798 |
| - |
799 |
| - self.assertSpanHasAttributes(server_span, expected) |
800 |
| - |
801 |
| - def test_http_custom_request_headers_not_in_span_attributes(self): |
802 |
| - not_expected = { |
803 |
| - "http.request.header.custom_test_header_3": ( |
804 |
| - "test-header-value-3", |
805 |
| - ), |
806 |
| - } |
807 |
| - resp = self.client.get( |
808 |
| - "/foobar", |
809 |
| - headers={ |
810 |
| - "custom-test-header-1": "test-header-value-1", |
811 |
| - "custom-test-header-2": "test-header-value-2", |
812 |
| - "Regex-Test-Header-1": "Regex Test Value 1", |
813 |
| - "regex-test-header-2": "RegexTestValue2,RegexTestValue3", |
814 |
| - "My-Secret-Header": "My Secret Value", |
815 |
| - }, |
816 |
| - ) |
817 |
| - self.assertEqual(200, resp.status_code) |
818 |
| - span_list = self.memory_exporter.get_finished_spans() |
819 |
| - self.assertEqual(len(span_list), 3) |
820 |
| - |
821 |
| - server_span = [ |
822 |
| - span for span in span_list if span.kind == trace.SpanKind.SERVER |
823 |
| - ][0] |
824 |
| - |
825 |
| - for key, _ in not_expected.items(): |
826 |
| - self.assertNotIn(key, server_span.attributes) |
827 |
| - |
828 |
| - def test_http_custom_response_headers_in_span_attributes(self): |
829 |
| - expected = { |
830 |
| - "http.response.header.custom_test_header_1": ( |
831 |
| - "test-header-value-1", |
832 |
| - ), |
833 |
| - "http.response.header.custom_test_header_2": ( |
834 |
| - "test-header-value-2", |
835 |
| - ), |
836 |
| - "http.response.header.my_custom_regex_header_1": ( |
837 |
| - "my-custom-regex-value-1", |
838 |
| - "my-custom-regex-value-2", |
839 |
| - ), |
840 |
| - "http.response.header.my_custom_regex_header_2": ( |
841 |
| - "my-custom-regex-value-3", |
842 |
| - "my-custom-regex-value-4", |
843 |
| - ), |
844 |
| - "http.response.header.my_secret_header": ("[REDACTED]",), |
845 |
| - } |
846 |
| - resp = self.client.get("/foobar") |
847 |
| - self.assertEqual(200, resp.status_code) |
848 |
| - span_list = self.memory_exporter.get_finished_spans() |
849 |
| - self.assertEqual(len(span_list), 3) |
850 |
| - |
851 |
| - server_span = [ |
852 |
| - span for span in span_list if span.kind == trace.SpanKind.SERVER |
853 |
| - ][0] |
854 |
| - self.assertSpanHasAttributes(server_span, expected) |
855 |
| - |
856 |
| - def test_http_custom_response_headers_not_in_span_attributes(self): |
857 |
| - not_expected = { |
858 |
| - "http.response.header.custom_test_header_3": ( |
859 |
| - "test-header-value-3", |
860 |
| - ), |
861 |
| - } |
862 |
| - resp = self.client.get("/foobar") |
863 |
| - self.assertEqual(200, resp.status_code) |
864 |
| - span_list = self.memory_exporter.get_finished_spans() |
865 |
| - self.assertEqual(len(span_list), 3) |
866 |
| - |
867 |
| - server_span = [ |
868 |
| - span for span in span_list if span.kind == trace.SpanKind.SERVER |
869 |
| - ][0] |
870 |
| - |
871 |
| - for key, _ in not_expected.items(): |
872 |
| - self.assertNotIn(key, server_span.attributes) |
873 |
| - |
874 |
| - |
875 |
| -@patch.dict( |
876 |
| - "os.environ", |
877 |
| - { |
878 |
| - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", |
879 |
| - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", |
880 |
| - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", |
881 |
| - }, |
882 |
| -) |
883 |
| -class TestWebSocketAppWithCustomHeaders(TestBase): |
884 |
| - def setUp(self): |
885 |
| - super().setUp() |
886 |
| - self.app = self._create_app() |
887 |
| - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) |
888 |
| - self.client = TestClient(self.app) |
889 |
| - |
890 |
| - def tearDown(self) -> None: |
891 |
| - super().tearDown() |
892 |
| - with self.disable_logging(): |
893 |
| - otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) |
894 |
| - |
895 |
| - @staticmethod |
896 |
| - def _create_app(): |
897 |
| - app = fastapi.FastAPI() |
898 |
| - |
899 |
| - @app.websocket("/foobar_web") |
900 |
| - async def _(websocket: fastapi.WebSocket): |
901 |
| - message = await websocket.receive() |
902 |
| - if message.get("type") == "websocket.connect": |
903 |
| - await websocket.send( |
904 |
| - { |
905 |
| - "type": "websocket.accept", |
906 |
| - "headers": [ |
907 |
| - (b"custom-test-header-1", b"test-header-value-1"), |
908 |
| - (b"custom-test-header-2", b"test-header-value-2"), |
909 |
| - (b"Regex-Test-Header-1", b"Regex Test Value 1"), |
910 |
| - ( |
911 |
| - b"regex-test-header-2", |
912 |
| - b"RegexTestValue2,RegexTestValue3", |
913 |
| - ), |
914 |
| - (b"My-Secret-Header", b"My Secret Value"), |
915 |
| - ], |
916 |
| - } |
917 |
| - ) |
918 |
| - await websocket.send_json({"message": "hello world"}) |
919 |
| - await websocket.close() |
920 |
| - if message.get("type") == "websocket.disconnect": |
921 |
| - pass |
922 |
| - |
923 |
| - return app |
924 |
| - |
925 |
| - def test_web_socket_custom_request_headers_in_span_attributes(self): |
926 |
| - expected = { |
927 |
| - "http.request.header.custom_test_header_1": ( |
928 |
| - "test-header-value-1", |
929 |
| - ), |
930 |
| - "http.request.header.custom_test_header_2": ( |
931 |
| - "test-header-value-2", |
932 |
| - ), |
933 |
| - } |
934 |
| - |
935 |
| - with self.client.websocket_connect( |
936 |
| - "/foobar_web", |
937 |
| - headers={ |
938 |
| - "custom-test-header-1": "test-header-value-1", |
939 |
| - "custom-test-header-2": "test-header-value-2", |
940 |
| - }, |
941 |
| - ) as websocket: |
942 |
| - data = websocket.receive_json() |
943 |
| - self.assertEqual(data, {"message": "hello world"}) |
944 |
| - |
945 |
| - span_list = self.memory_exporter.get_finished_spans() |
946 |
| - self.assertEqual(len(span_list), 5) |
947 |
| - |
948 |
| - server_span = [ |
949 |
| - span for span in span_list if span.kind == trace.SpanKind.SERVER |
950 |
| - ][0] |
951 |
| - |
952 |
| - self.assertSpanHasAttributes(server_span, expected) |
953 |
| - |
954 |
| - @patch.dict( |
955 |
| - "os.environ", |
956 |
| - { |
957 |
| - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", |
958 |
| - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", |
959 |
| - }, |
960 |
| - ) |
961 |
| - def test_web_socket_custom_request_headers_not_in_span_attributes(self): |
962 |
| - not_expected = { |
963 |
| - "http.request.header.custom_test_header_3": ( |
964 |
| - "test-header-value-3", |
965 |
| - ), |
966 |
| - } |
967 |
| - |
968 |
| - with self.client.websocket_connect( |
969 |
| - "/foobar_web", |
970 |
| - headers={ |
971 |
| - "custom-test-header-1": "test-header-value-1", |
972 |
| - "custom-test-header-2": "test-header-value-2", |
973 |
| - }, |
974 |
| - ) as websocket: |
975 |
| - data = websocket.receive_json() |
976 |
| - self.assertEqual(data, {"message": "hello world"}) |
977 |
| - |
978 |
| - span_list = self.memory_exporter.get_finished_spans() |
979 |
| - self.assertEqual(len(span_list), 5) |
980 |
| - |
981 |
| - server_span = [ |
982 |
| - span for span in span_list if span.kind == trace.SpanKind.SERVER |
983 |
| - ][0] |
984 |
| - |
985 |
| - for key, _ in not_expected.items(): |
986 |
| - self.assertNotIn(key, server_span.attributes) |
987 |
| - |
988 |
| - def test_web_socket_custom_response_headers_in_span_attributes(self): |
989 |
| - expected = { |
990 |
| - "http.response.header.custom_test_header_1": ( |
991 |
| - "test-header-value-1", |
992 |
| - ), |
993 |
| - "http.response.header.custom_test_header_2": ( |
994 |
| - "test-header-value-2", |
995 |
| - ), |
996 |
| - } |
997 |
| - |
998 |
| - with self.client.websocket_connect("/foobar_web") as websocket: |
999 |
| - data = websocket.receive_json() |
1000 |
| - self.assertEqual(data, {"message": "hello world"}) |
1001 |
| - |
1002 |
| - span_list = self.memory_exporter.get_finished_spans() |
1003 |
| - self.assertEqual(len(span_list), 5) |
1004 |
| - |
1005 |
| - server_span = [ |
1006 |
| - span for span in span_list if span.kind == trace.SpanKind.SERVER |
1007 |
| - ][0] |
1008 |
| - |
1009 |
| - self.assertSpanHasAttributes(server_span, expected) |
1010 |
| - |
1011 |
| - def test_web_socket_custom_response_headers_not_in_span_attributes(self): |
1012 |
| - not_expected = { |
1013 |
| - "http.response.header.custom_test_header_3": ( |
1014 |
| - "test-header-value-3", |
1015 |
| - ), |
1016 |
| - } |
1017 |
| - |
1018 |
| - with self.client.websocket_connect("/foobar_web") as websocket: |
1019 |
| - data = websocket.receive_json() |
1020 |
| - self.assertEqual(data, {"message": "hello world"}) |
1021 |
| - |
1022 |
| - span_list = self.memory_exporter.get_finished_spans() |
1023 |
| - self.assertEqual(len(span_list), 5) |
1024 |
| - |
1025 |
| - server_span = [ |
1026 |
| - span for span in span_list if span.kind == trace.SpanKind.SERVER |
1027 |
| - ][0] |
1028 |
| - |
1029 |
| - for key, _ in not_expected.items(): |
1030 |
| - self.assertNotIn(key, server_span.attributes) |
1031 |
| - |
1032 |
| - |
1033 |
| -@patch.dict( |
1034 |
| - "os.environ", |
1035 |
| - { |
1036 |
| - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", |
1037 |
| - }, |
1038 |
| -) |
1039 |
| -class TestNonRecordingSpanWithCustomHeaders(TestBase): |
1040 |
| - def setUp(self): |
1041 |
| - super().setUp() |
1042 |
| - self.app = fastapi.FastAPI() |
1043 |
| - |
1044 |
| - @self.app.get("/foobar") |
1045 |
| - async def _(): |
1046 |
| - return {"message": "hello world"} |
1047 |
| - |
1048 |
| - reset_trace_globals() |
1049 |
| - tracer_provider = trace.NoOpTracerProvider() |
1050 |
| - trace.set_tracer_provider(tracer_provider=tracer_provider) |
1051 |
| - |
1052 |
| - self._instrumentor = otel_fastapi.FastAPIInstrumentor() |
1053 |
| - self._instrumentor.instrument_app(self.app) |
1054 |
| - self.client = TestClient(self.app) |
1055 |
| - |
1056 |
| - def tearDown(self) -> None: |
1057 |
| - super().tearDown() |
1058 |
| - with self.disable_logging(): |
1059 |
| - self._instrumentor.uninstrument_app(self.app) |
1060 |
| - |
1061 |
| - def test_custom_header_not_present_in_non_recording_span(self): |
1062 |
| - resp = self.client.get( |
1063 |
| - "/foobar", |
1064 |
| - headers={ |
1065 |
| - "custom-test-header-1": "test-header-value-1", |
1066 |
| - }, |
1067 |
| - ) |
1068 |
| - self.assertEqual(200, resp.status_code) |
1069 |
| - span_list = self.memory_exporter.get_finished_spans() |
1070 |
| - self.assertEqual(len(span_list), 0) |
0 commit comments