Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 25a3bed

Browse files
committedJun 27, 2024·
refactor structure
1 parent 259cb9e commit 25a3bed

File tree

3 files changed

+232
-467
lines changed

3 files changed

+232
-467
lines changed
 

Diff for: ‎instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

+135-458
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import unittest
15-
from collections.abc import Mapping
1615
from timeit import default_timer
17-
from typing import Tuple
1816
from unittest.mock import patch
1917

2018
import fastapi
2119
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
2220
from fastapi.testclient import TestClient
2321

2422
import opentelemetry.instrumentation.fastapi as otel_fastapi
25-
from opentelemetry import trace
2623
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
2724
from opentelemetry.sdk.metrics.export import (
2825
HistogramDataPoint,
@@ -57,7 +54,7 @@
5754
}
5855

5956

60-
class TestFastAPIManualInstrumentation(TestBase):
57+
class BaseFastAPI:
6158
def _create_app(self):
6259
app = self._create_fastapi_app()
6360
self._instrumentor.instrument_app(
@@ -105,6 +102,137 @@ def tearDown(self):
105102
self._instrumentor.uninstrument()
106103
self._instrumentor.uninstrument_app(self._app)
107104

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):
108236
def test_instrument_app_with_instrument(self):
109237
if not isinstance(self, TestAutoInstrumentation):
110238
self._instrumentor.instrument()
@@ -314,48 +442,6 @@ def test_metric_uninstrument(self):
314442
if isinstance(point, NumberDataPoint):
315443
self.assertEqual(point.value, 0)
316444

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-
359445
@staticmethod
360446
def _create_fastapi_app():
361447
app = fastapi.FastAPI()
@@ -386,7 +472,7 @@ async def _():
386472
return app
387473

388474

389-
class TestFastAPIManualInstrumentationHooks(TestFastAPIManualInstrumentation):
475+
class TestFastAPIManualInstrumentationHooks(BaseManualFastAPI, TestBase):
390476
_server_request_hook = None
391477
_client_request_hook = None
392478
_client_response_hook = None
@@ -436,7 +522,7 @@ def client_response_hook(send_span, scope, message):
436522
)
437523

438524

439-
class TestAutoInstrumentation(TestFastAPIManualInstrumentation):
525+
class TestAutoInstrumentation(BaseAutoFastAPI, TestBase):
440526
"""Test the auto-instrumented variant
441527
442528
Extending the manual instrumentation as most test cases apply
@@ -550,7 +636,7 @@ def test_sub_app_fastapi_call(self):
550636
)
551637

552638

553-
class TestAutoInstrumentationHooks(TestFastAPIManualInstrumentationHooks):
639+
class TestAutoInstrumentationHooks(BaseAutoFastAPI, TestBase):
554640
"""
555641
Test the auto-instrumented variant for request and response hooks
556642
@@ -659,412 +745,3 @@ def test_instrumentation(self):
659745

660746
should_be_original = fastapi.FastAPI
661747
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)

Diff for: ‎instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation_custom_headers.py

+33-9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections.abc import Mapping
2+
from typing import Tuple
13
from unittest.mock import patch
24

35
import fastapi
@@ -15,6 +17,24 @@
1517
)
1618

1719

20+
class MultiMapping(Mapping):
21+
22+
def __init__(self, *items: Tuple[str, str]):
23+
self._items = items
24+
25+
def __len__(self):
26+
return len(self._items)
27+
28+
def __getitem__(self, __key):
29+
raise NotImplementedError("use .items() instead")
30+
31+
def __iter__(self):
32+
raise NotImplementedError("use .items() instead")
33+
34+
def items(self):
35+
return self._items
36+
37+
1838
@patch.dict(
1939
"os.environ",
2040
{
@@ -41,13 +61,15 @@ def _create_app():
4161

4262
@app.get("/foobar")
4363
async def _():
44-
headers = {
45-
"custom-test-header-1": "test-header-value-1",
46-
"custom-test-header-2": "test-header-value-2",
47-
"my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2",
48-
"My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4",
49-
"My-Secret-Header": "My Secret Value",
50-
}
64+
headers = MultiMapping(
65+
("custom-test-header-1", "test-header-value-1"),
66+
("custom-test-header-2", "test-header-value-2"),
67+
("my-custom-regex-header-1", "my-custom-regex-value-1"),
68+
("my-custom-regex-header-1", "my-custom-regex-value-2"),
69+
("My-Custom-Regex-Header-2", "my-custom-regex-value-3"),
70+
("My-Custom-Regex-Header-2", "my-custom-regex-value-4"),
71+
("My-Secret-Header", "My Secret Value"),
72+
)
5173
content = {"message": "hello world"}
5274
return JSONResponse(content=content, headers=headers)
5375

@@ -123,10 +145,12 @@ def test_http_custom_response_headers_in_span_attributes(self):
123145
"test-header-value-2",
124146
),
125147
"http.response.header.my_custom_regex_header_1": (
126-
"my-custom-regex-value-1,my-custom-regex-value-2",
148+
"my-custom-regex-value-1",
149+
"my-custom-regex-value-2",
127150
),
128151
"http.response.header.my_custom_regex_header_2": (
129-
"my-custom-regex-value-3,my-custom-regex-value-4",
152+
"my-custom-regex-value-3",
153+
"my-custom-regex-value-4",
130154
),
131155
"http.response.header.my_secret_header": ("[REDACTED]",),
132156
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 fastapi
15+
from starlette.testclient import TestClient
16+
17+
import opentelemetry.instrumentation.fastapi as otel_fastapi
18+
from opentelemetry import trace
19+
from opentelemetry.test.test_base import TestBase
20+
21+
22+
class TestWrappedApplication(TestBase):
23+
def setUp(self):
24+
super().setUp()
25+
26+
self.app = fastapi.FastAPI()
27+
28+
@self.app.get("/foobar")
29+
async def _():
30+
return {"message": "hello world"}
31+
32+
otel_fastapi.FastAPIInstrumentor().instrument_app(self.app)
33+
self.client = TestClient(self.app)
34+
self.tracer = self.tracer_provider.get_tracer(__name__)
35+
36+
def tearDown(self) -> None:
37+
super().tearDown()
38+
with self.disable_logging():
39+
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
40+
41+
def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
42+
with self.tracer.start_as_current_span(
43+
"test", kind=trace.SpanKind.SERVER
44+
) as parent_span:
45+
resp = self.client.get("/foobar")
46+
self.assertEqual(200, resp.status_code)
47+
48+
span_list = self.memory_exporter.get_finished_spans()
49+
for span in span_list:
50+
print(str(span.__class__) + ": " + str(span.__dict__))
51+
52+
# there should be 4 spans - single SERVER "test" and three INTERNAL "FastAPI"
53+
self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind)
54+
self.assertEqual(trace.SpanKind.INTERNAL, span_list[1].kind)
55+
# main INTERNAL span - child of test
56+
self.assertEqual(trace.SpanKind.INTERNAL, span_list[2].kind)
57+
self.assertEqual(
58+
parent_span.context.span_id, span_list[2].parent.span_id
59+
)
60+
# SERVER "test"
61+
self.assertEqual(trace.SpanKind.SERVER, span_list[3].kind)
62+
self.assertEqual(
63+
parent_span.context.span_id, span_list[3].context.span_id
64+
)

0 commit comments

Comments
 (0)
Please sign in to comment.