mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-28 20:52:57 +08:00
fix(opentelemetry-instrumentation-asgi): Correct http.url attribute generation (#2477)
This commit is contained in:
@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
([#2612](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2612))
|
([#2612](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2612))
|
||||||
- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+.
|
- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+.
|
||||||
([#2630](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2630))
|
([#2630](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2630))
|
||||||
|
- `opentelemetry-instrumentation-asgi` Fix generation of `http.target` and `http.url` attributes for ASGI apps
|
||||||
|
using sub apps
|
||||||
|
([#2477](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2477))
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
@ -367,7 +367,13 @@ def get_host_port_url_tuple(scope):
|
|||||||
server = scope.get("server") or ["0.0.0.0", 80]
|
server = scope.get("server") or ["0.0.0.0", 80]
|
||||||
port = server[1]
|
port = server[1]
|
||||||
server_host = server[0] + (":" + str(port) if str(port) != "80" else "")
|
server_host = server[0] + (":" + str(port) if str(port) != "80" else "")
|
||||||
full_path = scope.get("root_path", "") + scope.get("path", "")
|
# using the scope path is enough, see:
|
||||||
|
# - https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope (see: root_path and path)
|
||||||
|
# - https://asgi.readthedocs.io/en/latest/specs/www.html#wsgi-compatibility (see: PATH_INFO)
|
||||||
|
# PATH_INFO can be derived by stripping root_path from path
|
||||||
|
# -> that means that the path should contain the root_path already, so prefixing it again is not necessary
|
||||||
|
# - https://wsgi.readthedocs.io/en/latest/definitions.html#envvar-PATH_INFO
|
||||||
|
full_path = scope.get("path", "")
|
||||||
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
|
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
|
||||||
return server_host, port, http_url
|
return server_host, port, http_url
|
||||||
|
|
||||||
|
@ -12,18 +12,14 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import unittest
|
import unittest
|
||||||
from collections.abc import Mapping
|
|
||||||
from timeit import default_timer
|
from timeit import default_timer
|
||||||
from typing import Tuple
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import fastapi
|
import fastapi
|
||||||
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
|
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import opentelemetry.instrumentation.fastapi as otel_fastapi
|
import opentelemetry.instrumentation.fastapi as otel_fastapi
|
||||||
from opentelemetry import trace
|
|
||||||
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
|
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
|
||||||
from opentelemetry.sdk.metrics.export import (
|
from opentelemetry.sdk.metrics.export import (
|
||||||
HistogramDataPoint,
|
HistogramDataPoint,
|
||||||
@ -31,12 +27,8 @@ from opentelemetry.sdk.metrics.export import (
|
|||||||
)
|
)
|
||||||
from opentelemetry.sdk.resources import Resource
|
from opentelemetry.sdk.resources import Resource
|
||||||
from opentelemetry.semconv.trace import SpanAttributes
|
from opentelemetry.semconv.trace import SpanAttributes
|
||||||
from opentelemetry.test.globals_test import reset_trace_globals
|
|
||||||
from opentelemetry.test.test_base import TestBase
|
from opentelemetry.test.test_base import TestBase
|
||||||
from opentelemetry.util.http import (
|
from opentelemetry.util.http import (
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
|
|
||||||
_active_requests_count_attrs,
|
_active_requests_count_attrs,
|
||||||
_duration_attrs,
|
_duration_attrs,
|
||||||
get_excluded_urls,
|
get_excluded_urls,
|
||||||
@ -62,7 +54,7 @@ _recommended_attrs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestFastAPIManualInstrumentation(TestBase):
|
class TestBaseFastAPI(TestBase):
|
||||||
def _create_app(self):
|
def _create_app(self):
|
||||||
app = self._create_fastapi_app()
|
app = self._create_fastapi_app()
|
||||||
self._instrumentor.instrument_app(
|
self._instrumentor.instrument_app(
|
||||||
@ -85,6 +77,15 @@ class TestFastAPIManualInstrumentation(TestBase):
|
|||||||
)
|
)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls is TestBaseFastAPI:
|
||||||
|
raise unittest.SkipTest(
|
||||||
|
f"{cls.__name__} is an abstract base class"
|
||||||
|
)
|
||||||
|
|
||||||
|
super(TestBaseFastAPI, cls).setUpClass()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.env_patch = patch.dict(
|
self.env_patch = patch.dict(
|
||||||
@ -110,6 +111,155 @@ class TestFastAPIManualInstrumentation(TestBase):
|
|||||||
self._instrumentor.uninstrument()
|
self._instrumentor.uninstrument()
|
||||||
self._instrumentor.uninstrument_app(self._app)
|
self._instrumentor.uninstrument_app(self._app)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_fastapi_app():
|
||||||
|
app = fastapi.FastAPI()
|
||||||
|
sub_app = fastapi.FastAPI()
|
||||||
|
|
||||||
|
@sub_app.get("/home")
|
||||||
|
async def _():
|
||||||
|
return {"message": "sub hi"}
|
||||||
|
|
||||||
|
@app.get("/foobar")
|
||||||
|
async def _():
|
||||||
|
return {"message": "hello world"}
|
||||||
|
|
||||||
|
@app.get("/user/{username}")
|
||||||
|
async def _(username: str):
|
||||||
|
return {"message": username}
|
||||||
|
|
||||||
|
@app.get("/exclude/{param}")
|
||||||
|
async def _(param: str):
|
||||||
|
return {"message": param}
|
||||||
|
|
||||||
|
@app.get("/healthzz")
|
||||||
|
async def _():
|
||||||
|
return {"message": "ok"}
|
||||||
|
|
||||||
|
app.mount("/sub", app=sub_app)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseManualFastAPI(TestBaseFastAPI):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls is TestBaseManualFastAPI:
|
||||||
|
raise unittest.SkipTest(
|
||||||
|
f"{cls.__name__} is an abstract base class"
|
||||||
|
)
|
||||||
|
|
||||||
|
super(TestBaseManualFastAPI, cls).setUpClass()
|
||||||
|
|
||||||
|
def test_sub_app_fastapi_call(self):
|
||||||
|
"""
|
||||||
|
This test is to ensure that a span in case of a sub app targeted contains the correct server url
|
||||||
|
|
||||||
|
As this test case covers manual instrumentation, we won't see any additional spans for the sub app.
|
||||||
|
In this case all generated spans might suffice the requirements for the attributes already
|
||||||
|
(as the testcase is not setting a root_path for the outer app here)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client.get("/sub/home")
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 3)
|
||||||
|
for span in spans:
|
||||||
|
# As we are only looking to the "outer" app, we would see only the "GET /sub" spans
|
||||||
|
self.assertIn("GET /sub", span.name)
|
||||||
|
|
||||||
|
# We now want to specifically test all spans including the
|
||||||
|
# - HTTP_TARGET
|
||||||
|
# - HTTP_URL
|
||||||
|
# attributes to be populated with the expected values
|
||||||
|
spans_with_http_attributes = [
|
||||||
|
span
|
||||||
|
for span in spans
|
||||||
|
if (
|
||||||
|
SpanAttributes.HTTP_URL in span.attributes
|
||||||
|
or SpanAttributes.HTTP_TARGET in span.attributes
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# We expect only one span to have the HTTP attributes set (the SERVER span from the app itself)
|
||||||
|
# the sub app is not instrumented with manual instrumentation tests.
|
||||||
|
self.assertEqual(1, len(spans_with_http_attributes))
|
||||||
|
|
||||||
|
for span in spans_with_http_attributes:
|
||||||
|
self.assertEqual(
|
||||||
|
"/sub/home", span.attributes[SpanAttributes.HTTP_TARGET]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"https://testserver:443/sub/home",
|
||||||
|
span.attributes[SpanAttributes.HTTP_URL],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseAutoFastAPI(TestBaseFastAPI):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls is TestBaseAutoFastAPI:
|
||||||
|
raise unittest.SkipTest(
|
||||||
|
f"{cls.__name__} is an abstract base class"
|
||||||
|
)
|
||||||
|
|
||||||
|
super(TestBaseAutoFastAPI, cls).setUpClass()
|
||||||
|
|
||||||
|
def test_sub_app_fastapi_call(self):
|
||||||
|
"""
|
||||||
|
This test is to ensure that a span in case of a sub app targeted contains the correct server url
|
||||||
|
|
||||||
|
As this test case covers auto instrumentation, we will see additional spans for the sub app.
|
||||||
|
In this case all generated spans might suffice the requirements for the attributes already
|
||||||
|
(as the testcase is not setting a root_path for the outer app here)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client.get("/sub/home")
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 6)
|
||||||
|
|
||||||
|
for span in spans:
|
||||||
|
# As we are only looking to the "outer" app, we would see only the "GET /sub" spans
|
||||||
|
# -> the outer app is not aware of the sub_apps internal routes
|
||||||
|
sub_in = "GET /sub" in span.name
|
||||||
|
# The sub app spans are named GET /home as from the sub app perspective the request targets /home
|
||||||
|
# -> the sub app is technically not aware of the /sub prefix
|
||||||
|
home_in = "GET /home" in span.name
|
||||||
|
|
||||||
|
# We expect the spans to be either from the outer app or the sub app
|
||||||
|
self.assertTrue(
|
||||||
|
sub_in or home_in,
|
||||||
|
f"Span {span.name} does not have /sub or /home in its name",
|
||||||
|
)
|
||||||
|
|
||||||
|
# We now want to specifically test all spans including the
|
||||||
|
# - HTTP_TARGET
|
||||||
|
# - HTTP_URL
|
||||||
|
# attributes to be populated with the expected values
|
||||||
|
spans_with_http_attributes = [
|
||||||
|
span
|
||||||
|
for span in spans
|
||||||
|
if (
|
||||||
|
SpanAttributes.HTTP_URL in span.attributes
|
||||||
|
or SpanAttributes.HTTP_TARGET in span.attributes
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# We now expect spans with attributes from both the app and its sub app
|
||||||
|
self.assertEqual(2, len(spans_with_http_attributes))
|
||||||
|
|
||||||
|
for span in spans_with_http_attributes:
|
||||||
|
self.assertEqual(
|
||||||
|
"/sub/home", span.attributes[SpanAttributes.HTTP_TARGET]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"https://testserver:443/sub/home",
|
||||||
|
span.attributes[SpanAttributes.HTTP_URL],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
|
||||||
def test_instrument_app_with_instrument(self):
|
def test_instrument_app_with_instrument(self):
|
||||||
if not isinstance(self, TestAutoInstrumentation):
|
if not isinstance(self, TestAutoInstrumentation):
|
||||||
self._instrumentor.instrument()
|
self._instrumentor.instrument()
|
||||||
@ -322,6 +472,11 @@ class TestFastAPIManualInstrumentation(TestBase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_fastapi_app():
|
def _create_fastapi_app():
|
||||||
app = fastapi.FastAPI()
|
app = fastapi.FastAPI()
|
||||||
|
sub_app = fastapi.FastAPI()
|
||||||
|
|
||||||
|
@sub_app.get("/home")
|
||||||
|
async def _():
|
||||||
|
return {"message": "sub hi"}
|
||||||
|
|
||||||
@app.get("/foobar")
|
@app.get("/foobar")
|
||||||
async def _():
|
async def _():
|
||||||
@ -339,10 +494,12 @@ class TestFastAPIManualInstrumentation(TestBase):
|
|||||||
async def _():
|
async def _():
|
||||||
return {"message": "ok"}
|
return {"message": "ok"}
|
||||||
|
|
||||||
|
app.mount("/sub", app=sub_app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
class TestFastAPIManualInstrumentationHooks(TestFastAPIManualInstrumentation):
|
class TestFastAPIManualInstrumentationHooks(TestBaseManualFastAPI):
|
||||||
_server_request_hook = None
|
_server_request_hook = None
|
||||||
_client_request_hook = None
|
_client_request_hook = None
|
||||||
_client_response_hook = None
|
_client_response_hook = None
|
||||||
@ -392,7 +549,7 @@ class TestFastAPIManualInstrumentationHooks(TestFastAPIManualInstrumentation):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestAutoInstrumentation(TestFastAPIManualInstrumentation):
|
class TestAutoInstrumentation(TestBaseAutoFastAPI):
|
||||||
"""Test the auto-instrumented variant
|
"""Test the auto-instrumented variant
|
||||||
|
|
||||||
Extending the manual instrumentation as most test cases apply
|
Extending the manual instrumentation as most test cases apply
|
||||||
@ -453,8 +610,60 @@ class TestAutoInstrumentation(TestFastAPIManualInstrumentation):
|
|||||||
self._instrumentor.uninstrument()
|
self._instrumentor.uninstrument()
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
def test_sub_app_fastapi_call(self):
|
||||||
|
"""
|
||||||
|
!!! Attention: we need to override this testcase for the auto-instrumented variant
|
||||||
|
The reason is, that with auto instrumentation, the sub app is instrumented as well
|
||||||
|
and therefore we would see the spans for the sub app as well
|
||||||
|
|
||||||
class TestAutoInstrumentationHooks(TestFastAPIManualInstrumentationHooks):
|
This test is to ensure that a span in case of a sub app targeted contains the correct server url
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client.get("/sub/home")
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 6)
|
||||||
|
|
||||||
|
for span in spans:
|
||||||
|
# As we are only looking to the "outer" app, we would see only the "GET /sub" spans
|
||||||
|
# -> the outer app is not aware of the sub_apps internal routes
|
||||||
|
sub_in = "GET /sub" in span.name
|
||||||
|
# The sub app spans are named GET /home as from the sub app perspective the request targets /home
|
||||||
|
# -> the sub app is technically not aware of the /sub prefix
|
||||||
|
home_in = "GET /home" in span.name
|
||||||
|
|
||||||
|
# We expect the spans to be either from the outer app or the sub app
|
||||||
|
self.assertTrue(
|
||||||
|
sub_in or home_in,
|
||||||
|
f"Span {span.name} does not have /sub or /home in its name",
|
||||||
|
)
|
||||||
|
|
||||||
|
# We now want to specifically test all spans including the
|
||||||
|
# - HTTP_TARGET
|
||||||
|
# - HTTP_URL
|
||||||
|
# attributes to be populated with the expected values
|
||||||
|
spans_with_http_attributes = [
|
||||||
|
span
|
||||||
|
for span in spans
|
||||||
|
if (
|
||||||
|
SpanAttributes.HTTP_URL in span.attributes
|
||||||
|
or SpanAttributes.HTTP_TARGET in span.attributes
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# We now expect spans with attributes from both the app and its sub app
|
||||||
|
self.assertEqual(2, len(spans_with_http_attributes))
|
||||||
|
|
||||||
|
for span in spans_with_http_attributes:
|
||||||
|
self.assertEqual(
|
||||||
|
"/sub/home", span.attributes[SpanAttributes.HTTP_TARGET]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"https://testserver:443/sub/home",
|
||||||
|
span.attributes[SpanAttributes.HTTP_URL],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAutoInstrumentationHooks(TestBaseAutoFastAPI):
|
||||||
"""
|
"""
|
||||||
Test the auto-instrumented variant for request and response hooks
|
Test the auto-instrumented variant for request and response hooks
|
||||||
|
|
||||||
@ -494,6 +703,58 @@ class TestAutoInstrumentationHooks(TestFastAPIManualInstrumentationHooks):
|
|||||||
self._instrumentor.uninstrument()
|
self._instrumentor.uninstrument()
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
def test_sub_app_fastapi_call(self):
|
||||||
|
"""
|
||||||
|
!!! Attention: we need to override this testcase for the auto-instrumented variant
|
||||||
|
The reason is, that with auto instrumentation, the sub app is instrumented as well
|
||||||
|
and therefore we would see the spans for the sub app as well
|
||||||
|
|
||||||
|
This test is to ensure that a span in case of a sub app targeted contains the correct server url
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client.get("/sub/home")
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 6)
|
||||||
|
|
||||||
|
for span in spans:
|
||||||
|
# As we are only looking to the "outer" app, we would see only the "GET /sub" spans
|
||||||
|
# -> the outer app is not aware of the sub_apps internal routes
|
||||||
|
sub_in = "GET /sub" in span.name
|
||||||
|
# The sub app spans are named GET /home as from the sub app perspective the request targets /home
|
||||||
|
# -> the sub app is technically not aware of the /sub prefix
|
||||||
|
home_in = "GET /home" in span.name
|
||||||
|
|
||||||
|
# We expect the spans to be either from the outer app or the sub app
|
||||||
|
self.assertTrue(
|
||||||
|
sub_in or home_in,
|
||||||
|
f"Span {span.name} does not have /sub or /home in its name",
|
||||||
|
)
|
||||||
|
|
||||||
|
# We now want to specifically test all spans including the
|
||||||
|
# - HTTP_TARGET
|
||||||
|
# - HTTP_URL
|
||||||
|
# attributes to be populated with the expected values
|
||||||
|
spans_with_http_attributes = [
|
||||||
|
span
|
||||||
|
for span in spans
|
||||||
|
if (
|
||||||
|
SpanAttributes.HTTP_URL in span.attributes
|
||||||
|
or SpanAttributes.HTTP_TARGET in span.attributes
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# We now expect spans with attributes from both the app and its sub app
|
||||||
|
self.assertEqual(2, len(spans_with_http_attributes))
|
||||||
|
|
||||||
|
for span in spans_with_http_attributes:
|
||||||
|
self.assertEqual(
|
||||||
|
"/sub/home", span.attributes[SpanAttributes.HTTP_TARGET]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"https://testserver:443/sub/home",
|
||||||
|
span.attributes[SpanAttributes.HTTP_URL],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestAutoInstrumentationLogic(unittest.TestCase):
|
class TestAutoInstrumentationLogic(unittest.TestCase):
|
||||||
def test_instrumentation(self):
|
def test_instrumentation(self):
|
||||||
@ -511,412 +772,3 @@ class TestAutoInstrumentationLogic(unittest.TestCase):
|
|||||||
|
|
||||||
should_be_original = fastapi.FastAPI
|
should_be_original = fastapi.FastAPI
|
||||||
self.assertIs(original, should_be_original)
|
self.assertIs(original, should_be_original)
|
||||||
|
|
||||||
|
|
||||||
class TestWrappedApplication(TestBase):
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
self.app = fastapi.FastAPI()
|
|
||||||
|
|
||||||
@self.app.get("/foobar")
|
|
||||||
async def _():
|
|
||||||
return {"message": "hello world"}
|
|
||||||
|
|
||||||
otel_fastapi.FastAPIInstrumentor().instrument_app(self.app)
|
|
||||||
self.client = TestClient(self.app)
|
|
||||||
self.tracer = self.tracer_provider.get_tracer(__name__)
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
super().tearDown()
|
|
||||||
with self.disable_logging():
|
|
||||||
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
|
|
||||||
|
|
||||||
def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
|
|
||||||
with self.tracer.start_as_current_span(
|
|
||||||
"test", kind=trace.SpanKind.SERVER
|
|
||||||
) as parent_span:
|
|
||||||
resp = self.client.get("/foobar")
|
|
||||||
self.assertEqual(200, resp.status_code)
|
|
||||||
|
|
||||||
span_list = self.memory_exporter.get_finished_spans()
|
|
||||||
for span in span_list:
|
|
||||||
print(str(span.__class__) + ": " + str(span.__dict__))
|
|
||||||
|
|
||||||
# there should be 4 spans - single SERVER "test" and three INTERNAL "FastAPI"
|
|
||||||
self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind)
|
|
||||||
self.assertEqual(trace.SpanKind.INTERNAL, span_list[1].kind)
|
|
||||||
# main INTERNAL span - child of test
|
|
||||||
self.assertEqual(trace.SpanKind.INTERNAL, span_list[2].kind)
|
|
||||||
self.assertEqual(
|
|
||||||
parent_span.context.span_id, span_list[2].parent.span_id
|
|
||||||
)
|
|
||||||
# SERVER "test"
|
|
||||||
self.assertEqual(trace.SpanKind.SERVER, span_list[3].kind)
|
|
||||||
self.assertEqual(
|
|
||||||
parent_span.context.span_id, span_list[3].context.span_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MultiMapping(Mapping):
|
|
||||||
|
|
||||||
def __init__(self, *items: Tuple[str, str]):
|
|
||||||
self._items = items
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._items)
|
|
||||||
|
|
||||||
def __getitem__(self, __key):
|
|
||||||
raise NotImplementedError("use .items() instead")
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
raise NotImplementedError("use .items() instead")
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return self._items
|
|
||||||
|
|
||||||
|
|
||||||
@patch.dict(
|
|
||||||
"os.environ",
|
|
||||||
{
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
|
||||||
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.*",
|
|
||||||
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.*",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
class TestHTTPAppWithCustomHeaders(TestBase):
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.app = self._create_app()
|
|
||||||
otel_fastapi.FastAPIInstrumentor().instrument_app(self.app)
|
|
||||||
self.client = TestClient(self.app)
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
super().tearDown()
|
|
||||||
with self.disable_logging():
|
|
||||||
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_app():
|
|
||||||
app = fastapi.FastAPI()
|
|
||||||
|
|
||||||
@app.get("/foobar")
|
|
||||||
async def _():
|
|
||||||
headers = MultiMapping(
|
|
||||||
("custom-test-header-1", "test-header-value-1"),
|
|
||||||
("custom-test-header-2", "test-header-value-2"),
|
|
||||||
("my-custom-regex-header-1", "my-custom-regex-value-1"),
|
|
||||||
("my-custom-regex-header-1", "my-custom-regex-value-2"),
|
|
||||||
("My-Custom-Regex-Header-2", "my-custom-regex-value-3"),
|
|
||||||
("My-Custom-Regex-Header-2", "my-custom-regex-value-4"),
|
|
||||||
("My-Secret-Header", "My Secret Value"),
|
|
||||||
)
|
|
||||||
content = {"message": "hello world"}
|
|
||||||
return JSONResponse(content=content, headers=headers)
|
|
||||||
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
"http.request.header.regex_test_header_1": ("Regex Test Value 1",),
|
|
||||||
"http.request.header.regex_test_header_2": (
|
|
||||||
"RegexTestValue2,RegexTestValue3",
|
|
||||||
),
|
|
||||||
"http.request.header.my_secret_header": ("[REDACTED]",),
|
|
||||||
}
|
|
||||||
resp = self.client.get(
|
|
||||||
"/foobar",
|
|
||||||
headers={
|
|
||||||
"custom-test-header-1": "test-header-value-1",
|
|
||||||
"custom-test-header-2": "test-header-value-2",
|
|
||||||
"Regex-Test-Header-1": "Regex Test Value 1",
|
|
||||||
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
|
|
||||||
"My-Secret-Header": "My Secret Value",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(200, resp.status_code)
|
|
||||||
span_list = self.memory_exporter.get_finished_spans()
|
|
||||||
self.assertEqual(len(span_list), 3)
|
|
||||||
|
|
||||||
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 = {
|
|
||||||
"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",
|
|
||||||
"Regex-Test-Header-1": "Regex Test Value 1",
|
|
||||||
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
|
|
||||||
"My-Secret-Header": "My Secret Value",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(200, resp.status_code)
|
|
||||||
span_list = self.memory_exporter.get_finished_spans()
|
|
||||||
self.assertEqual(len(span_list), 3)
|
|
||||||
|
|
||||||
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_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",
|
|
||||||
),
|
|
||||||
"http.response.header.my_custom_regex_header_1": (
|
|
||||||
"my-custom-regex-value-1",
|
|
||||||
"my-custom-regex-value-2",
|
|
||||||
),
|
|
||||||
"http.response.header.my_custom_regex_header_2": (
|
|
||||||
"my-custom-regex-value-3",
|
|
||||||
"my-custom-regex-value-4",
|
|
||||||
),
|
|
||||||
"http.response.header.my_secret_header": ("[REDACTED]",),
|
|
||||||
}
|
|
||||||
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 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 = {
|
|
||||||
"http.response.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 for span in span_list if span.kind == trace.SpanKind.SERVER
|
|
||||||
][0]
|
|
||||||
|
|
||||||
for key, _ in not_expected.items():
|
|
||||||
self.assertNotIn(key, server_span.attributes)
|
|
||||||
|
|
||||||
|
|
||||||
@patch.dict(
|
|
||||||
"os.environ",
|
|
||||||
{
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
|
||||||
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.*",
|
|
||||||
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.*",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
class TestWebSocketAppWithCustomHeaders(TestBase):
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.app = self._create_app()
|
|
||||||
otel_fastapi.FastAPIInstrumentor().instrument_app(self.app)
|
|
||||||
self.client = TestClient(self.app)
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
super().tearDown()
|
|
||||||
with self.disable_logging():
|
|
||||||
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_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"),
|
|
||||||
(b"Regex-Test-Header-1", b"Regex Test Value 1"),
|
|
||||||
(
|
|
||||||
b"regex-test-header-2",
|
|
||||||
b"RegexTestValue2,RegexTestValue3",
|
|
||||||
),
|
|
||||||
(b"My-Secret-Header", b"My Secret Value"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
await websocket.send_json({"message": "hello world"})
|
|
||||||
await websocket.close()
|
|
||||||
if message.get("type") == "websocket.disconnect":
|
|
||||||
pass
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
self.assertSpanHasAttributes(server_span, expected)
|
|
||||||
|
|
||||||
@patch.dict(
|
|
||||||
"os.environ",
|
|
||||||
{
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
|
||||||
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.*",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
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_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 = {
|
|
||||||
"http.response.header.custom_test_header_3": (
|
|
||||||
"test-header-value-3",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
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_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)
|
|
||||||
|
|
||||||
|
|
||||||
@patch.dict(
|
|
||||||
"os.environ",
|
|
||||||
{
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
class TestNonRecordingSpanWithCustomHeaders(TestBase):
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
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):
|
|
||||||
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)
|
|
||||||
|
@ -0,0 +1,381 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Tuple
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import fastapi
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
import opentelemetry.instrumentation.fastapi as otel_fastapi
|
||||||
|
from opentelemetry import trace
|
||||||
|
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_SANITIZE_FIELDS,
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiMapping(Mapping):
|
||||||
|
|
||||||
|
def __init__(self, *items: Tuple[str, str]):
|
||||||
|
self._items = items
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._items)
|
||||||
|
|
||||||
|
def __getitem__(self, __key):
|
||||||
|
raise NotImplementedError("use .items() instead")
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
raise NotImplementedError("use .items() instead")
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self._items
|
||||||
|
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
"os.environ",
|
||||||
|
{
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
||||||
|
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.*",
|
||||||
|
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.*",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
class TestHTTPAppWithCustomHeaders(TestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.app = self._create_app()
|
||||||
|
otel_fastapi.FastAPIInstrumentor().instrument_app(self.app)
|
||||||
|
self.client = TestClient(self.app)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
super().tearDown()
|
||||||
|
with self.disable_logging():
|
||||||
|
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_app():
|
||||||
|
app = fastapi.FastAPI()
|
||||||
|
|
||||||
|
@app.get("/foobar")
|
||||||
|
async def _():
|
||||||
|
headers = MultiMapping(
|
||||||
|
("custom-test-header-1", "test-header-value-1"),
|
||||||
|
("custom-test-header-2", "test-header-value-2"),
|
||||||
|
("my-custom-regex-header-1", "my-custom-regex-value-1"),
|
||||||
|
("my-custom-regex-header-1", "my-custom-regex-value-2"),
|
||||||
|
("My-Custom-Regex-Header-2", "my-custom-regex-value-3"),
|
||||||
|
("My-Custom-Regex-Header-2", "my-custom-regex-value-4"),
|
||||||
|
("My-Secret-Header", "My Secret Value"),
|
||||||
|
)
|
||||||
|
content = {"message": "hello world"}
|
||||||
|
return JSONResponse(content=content, headers=headers)
|
||||||
|
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
"http.request.header.regex_test_header_1": ("Regex Test Value 1",),
|
||||||
|
"http.request.header.regex_test_header_2": (
|
||||||
|
"RegexTestValue2,RegexTestValue3",
|
||||||
|
),
|
||||||
|
"http.request.header.my_secret_header": ("[REDACTED]",),
|
||||||
|
}
|
||||||
|
resp = self.client.get(
|
||||||
|
"/foobar",
|
||||||
|
headers={
|
||||||
|
"custom-test-header-1": "test-header-value-1",
|
||||||
|
"custom-test-header-2": "test-header-value-2",
|
||||||
|
"Regex-Test-Header-1": "Regex Test Value 1",
|
||||||
|
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
|
||||||
|
"My-Secret-Header": "My Secret Value",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
span_list = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(span_list), 3)
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
"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",
|
||||||
|
"Regex-Test-Header-1": "Regex Test Value 1",
|
||||||
|
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
|
||||||
|
"My-Secret-Header": "My Secret Value",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
span_list = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(span_list), 3)
|
||||||
|
|
||||||
|
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_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",
|
||||||
|
),
|
||||||
|
"http.response.header.my_custom_regex_header_1": (
|
||||||
|
"my-custom-regex-value-1",
|
||||||
|
"my-custom-regex-value-2",
|
||||||
|
),
|
||||||
|
"http.response.header.my_custom_regex_header_2": (
|
||||||
|
"my-custom-regex-value-3",
|
||||||
|
"my-custom-regex-value-4",
|
||||||
|
),
|
||||||
|
"http.response.header.my_secret_header": ("[REDACTED]",),
|
||||||
|
}
|
||||||
|
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 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 = {
|
||||||
|
"http.response.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 for span in span_list if span.kind == trace.SpanKind.SERVER
|
||||||
|
][0]
|
||||||
|
|
||||||
|
for key, _ in not_expected.items():
|
||||||
|
self.assertNotIn(key, server_span.attributes)
|
||||||
|
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
"os.environ",
|
||||||
|
{
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
||||||
|
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.*",
|
||||||
|
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.*",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
class TestWebSocketAppWithCustomHeaders(TestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.app = self._create_app()
|
||||||
|
otel_fastapi.FastAPIInstrumentor().instrument_app(self.app)
|
||||||
|
self.client = TestClient(self.app)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
super().tearDown()
|
||||||
|
with self.disable_logging():
|
||||||
|
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_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"),
|
||||||
|
(b"Regex-Test-Header-1", b"Regex Test Value 1"),
|
||||||
|
(
|
||||||
|
b"regex-test-header-2",
|
||||||
|
b"RegexTestValue2,RegexTestValue3",
|
||||||
|
),
|
||||||
|
(b"My-Secret-Header", b"My Secret Value"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await websocket.send_json({"message": "hello world"})
|
||||||
|
await websocket.close()
|
||||||
|
if message.get("type") == "websocket.disconnect":
|
||||||
|
pass
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
self.assertSpanHasAttributes(server_span, expected)
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
"os.environ",
|
||||||
|
{
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
||||||
|
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.*",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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 = {
|
||||||
|
"http.response.header.custom_test_header_3": (
|
||||||
|
"test-header-value-3",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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)
|
||||||
|
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
"os.environ",
|
||||||
|
{
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
class TestNonRecordingSpanWithCustomHeaders(TestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
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):
|
||||||
|
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)
|
@ -0,0 +1,64 @@
|
|||||||
|
# Copyright The OpenTelemetry Authors
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
import fastapi
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
import opentelemetry.instrumentation.fastapi as otel_fastapi
|
||||||
|
from opentelemetry import trace
|
||||||
|
from opentelemetry.test.test_base import TestBase
|
||||||
|
|
||||||
|
|
||||||
|
class TestWrappedApplication(TestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.app = fastapi.FastAPI()
|
||||||
|
|
||||||
|
@self.app.get("/foobar")
|
||||||
|
async def _():
|
||||||
|
return {"message": "hello world"}
|
||||||
|
|
||||||
|
otel_fastapi.FastAPIInstrumentor().instrument_app(self.app)
|
||||||
|
self.client = TestClient(self.app)
|
||||||
|
self.tracer = self.tracer_provider.get_tracer(__name__)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
super().tearDown()
|
||||||
|
with self.disable_logging():
|
||||||
|
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
|
||||||
|
|
||||||
|
def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
|
||||||
|
with self.tracer.start_as_current_span(
|
||||||
|
"test", kind=trace.SpanKind.SERVER
|
||||||
|
) as parent_span:
|
||||||
|
resp = self.client.get("/foobar")
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
|
||||||
|
span_list = self.memory_exporter.get_finished_spans()
|
||||||
|
for span in span_list:
|
||||||
|
print(str(span.__class__) + ": " + str(span.__dict__))
|
||||||
|
|
||||||
|
# there should be 4 spans - single SERVER "test" and three INTERNAL "FastAPI"
|
||||||
|
self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind)
|
||||||
|
self.assertEqual(trace.SpanKind.INTERNAL, span_list[1].kind)
|
||||||
|
# main INTERNAL span - child of test
|
||||||
|
self.assertEqual(trace.SpanKind.INTERNAL, span_list[2].kind)
|
||||||
|
self.assertEqual(
|
||||||
|
parent_span.context.span_id, span_list[2].parent.span_id
|
||||||
|
)
|
||||||
|
# SERVER "test"
|
||||||
|
self.assertEqual(trace.SpanKind.SERVER, span_list[3].kind)
|
||||||
|
self.assertEqual(
|
||||||
|
parent_span.context.span_id, span_list[3].context.span_id
|
||||||
|
)
|
@ -18,7 +18,7 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from starlette import applications
|
from starlette import applications
|
||||||
from starlette.responses import PlainTextResponse
|
from starlette.responses import PlainTextResponse
|
||||||
from starlette.routing import Route
|
from starlette.routing import Mount, Route
|
||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
from starlette.websockets import WebSocket
|
from starlette.websockets import WebSocket
|
||||||
|
|
||||||
@ -103,6 +103,43 @@ class TestStarletteManualInstrumentation(TestBase):
|
|||||||
"opentelemetry.instrumentation.starlette",
|
"opentelemetry.instrumentation.starlette",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_sub_app_starlette_call(self):
|
||||||
|
"""
|
||||||
|
This test is to ensure that a span in case of a sub app targeted contains the correct server url
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client.get("/sub/home")
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 3)
|
||||||
|
for span in spans:
|
||||||
|
# As we are only looking to the "outer" app, we would see only the "GET /sub" spans
|
||||||
|
self.assertIn("GET /sub", span.name)
|
||||||
|
|
||||||
|
# We now want to specifically test all spans including the
|
||||||
|
# - HTTP_TARGET
|
||||||
|
# - HTTP_URL
|
||||||
|
# attributes to be populated with the expected values
|
||||||
|
spans_with_http_attributes = [
|
||||||
|
span
|
||||||
|
for span in spans
|
||||||
|
if (
|
||||||
|
SpanAttributes.HTTP_URL in span.attributes
|
||||||
|
or SpanAttributes.HTTP_TARGET in span.attributes
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# expect only one span to have the attributes
|
||||||
|
self.assertEqual(1, len(spans_with_http_attributes))
|
||||||
|
|
||||||
|
for span in spans_with_http_attributes:
|
||||||
|
self.assertEqual(
|
||||||
|
"/sub/home", span.attributes[SpanAttributes.HTTP_TARGET]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"http://testserver/sub/home",
|
||||||
|
span.attributes[SpanAttributes.HTTP_URL],
|
||||||
|
)
|
||||||
|
|
||||||
def test_starlette_route_attribute_added(self):
|
def test_starlette_route_attribute_added(self):
|
||||||
"""Ensure that starlette routes are used as the span name."""
|
"""Ensure that starlette routes are used as the span name."""
|
||||||
self._client.get("/user/123")
|
self._client.get("/user/123")
|
||||||
@ -250,13 +287,20 @@ class TestStarletteManualInstrumentation(TestBase):
|
|||||||
def health(_):
|
def health(_):
|
||||||
return PlainTextResponse("ok")
|
return PlainTextResponse("ok")
|
||||||
|
|
||||||
|
def sub_home(_):
|
||||||
|
return PlainTextResponse("sub hi")
|
||||||
|
|
||||||
|
sub_app = applications.Starlette(routes=[Route("/home", sub_home)])
|
||||||
|
|
||||||
app = applications.Starlette(
|
app = applications.Starlette(
|
||||||
routes=[
|
routes=[
|
||||||
Route("/foobar", home),
|
Route("/foobar", home),
|
||||||
Route("/user/{username}", home),
|
Route("/user/{username}", home),
|
||||||
Route("/healthzz", health),
|
Route("/healthzz", health),
|
||||||
]
|
Mount("/sub", app=sub_app),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
@ -354,6 +398,72 @@ class TestAutoInstrumentation(TestStarletteManualInstrumentation):
|
|||||||
spans = self.memory_exporter.get_finished_spans()
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
self.assertEqual(len(spans), 0)
|
self.assertEqual(len(spans), 0)
|
||||||
|
|
||||||
|
def test_sub_app_starlette_call(self):
|
||||||
|
"""
|
||||||
|
!!! Attention: we need to override this testcase for the auto-instrumented variant
|
||||||
|
The reason is, that with auto instrumentation, the sub app is instrumented as well
|
||||||
|
and therefore we would see the spans for the sub app as well
|
||||||
|
|
||||||
|
This test is to ensure that a span in case of a sub app targeted contains the correct server url
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client.get("/sub/home")
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 6)
|
||||||
|
|
||||||
|
for span in spans:
|
||||||
|
# As we are only looking to the "outer" app, we would see only the "GET /sub" spans
|
||||||
|
# -> the outer app is not aware of the sub_apps internal routes
|
||||||
|
sub_in = "GET /sub" in span.name
|
||||||
|
# The sub app spans are named GET /home as from the sub app perspective the request targets /home
|
||||||
|
# -> the sub app is technically not aware of the /sub prefix
|
||||||
|
home_in = "GET /home" in span.name
|
||||||
|
|
||||||
|
# We expect the spans to be either from the outer app or the sub app
|
||||||
|
self.assertTrue(
|
||||||
|
sub_in or home_in,
|
||||||
|
f"Span {span.name} does not have /sub or /home in its name",
|
||||||
|
)
|
||||||
|
|
||||||
|
# We now want to specifically test all spans including the
|
||||||
|
# - HTTP_TARGET
|
||||||
|
# - HTTP_URL
|
||||||
|
# attributes to be populated with the expected values
|
||||||
|
spans_with_http_attributes = [
|
||||||
|
span
|
||||||
|
for span in spans
|
||||||
|
if (
|
||||||
|
SpanAttributes.HTTP_URL in span.attributes
|
||||||
|
or SpanAttributes.HTTP_TARGET in span.attributes
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# We now expect spans with attributes from both the app and its sub app
|
||||||
|
self.assertEqual(2, len(spans_with_http_attributes))
|
||||||
|
|
||||||
|
# Due to a potential bug in starlettes handling of sub app mounts, we can
|
||||||
|
# check only the server kind spans for the correct attributes
|
||||||
|
# The internal one generated by the sub app is not yet producing the correct attributes
|
||||||
|
server_span = next(
|
||||||
|
(
|
||||||
|
span
|
||||||
|
for span in spans_with_http_attributes
|
||||||
|
if span.kind == SpanKind.SERVER
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(server_span)
|
||||||
|
# As soon as the bug is fixed for starlette, we can iterate over spans_with_http_attributes here
|
||||||
|
# to verify the correctness of the attributes for the internal span as well
|
||||||
|
self.assertEqual(
|
||||||
|
"/sub/home", server_span.attributes[SpanAttributes.HTTP_TARGET]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"http://testserver/sub/home",
|
||||||
|
server_span.attributes[SpanAttributes.HTTP_URL],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestAutoInstrumentationHooks(TestStarletteManualInstrumentationHooks):
|
class TestAutoInstrumentationHooks(TestStarletteManualInstrumentationHooks):
|
||||||
"""
|
"""
|
||||||
@ -374,6 +484,72 @@ class TestAutoInstrumentationHooks(TestStarletteManualInstrumentationHooks):
|
|||||||
self._instrumentor.uninstrument()
|
self._instrumentor.uninstrument()
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
def test_sub_app_starlette_call(self):
|
||||||
|
"""
|
||||||
|
!!! Attention: we need to override this testcase for the auto-instrumented variant
|
||||||
|
The reason is, that with auto instrumentation, the sub app is instrumented as well
|
||||||
|
and therefore we would see the spans for the sub app as well
|
||||||
|
|
||||||
|
This test is to ensure that a span in case of a sub app targeted contains the correct server url
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client.get("/sub/home")
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 6)
|
||||||
|
|
||||||
|
for span in spans:
|
||||||
|
# As we are only looking to the "outer" app, we would see only the "GET /sub" spans
|
||||||
|
# -> the outer app is not aware of the sub_apps internal routes
|
||||||
|
sub_in = "GET /sub" in span.name
|
||||||
|
# The sub app spans are named GET /home as from the sub app perspective the request targets /home
|
||||||
|
# -> the sub app is technically not aware of the /sub prefix
|
||||||
|
home_in = "GET /home" in span.name
|
||||||
|
|
||||||
|
# We expect the spans to be either from the outer app or the sub app
|
||||||
|
self.assertTrue(
|
||||||
|
sub_in or home_in,
|
||||||
|
f"Span {span.name} does not have /sub or /home in its name",
|
||||||
|
)
|
||||||
|
|
||||||
|
# We now want to specifically test all spans including the
|
||||||
|
# - HTTP_TARGET
|
||||||
|
# - HTTP_URL
|
||||||
|
# attributes to be populated with the expected values
|
||||||
|
spans_with_http_attributes = [
|
||||||
|
span
|
||||||
|
for span in spans
|
||||||
|
if (
|
||||||
|
SpanAttributes.HTTP_URL in span.attributes
|
||||||
|
or SpanAttributes.HTTP_TARGET in span.attributes
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# We now expect spans with attributes from both the app and its sub app
|
||||||
|
self.assertEqual(2, len(spans_with_http_attributes))
|
||||||
|
|
||||||
|
# Due to a potential bug in starlettes handling of sub app mounts, we can
|
||||||
|
# check only the server kind spans for the correct attributes
|
||||||
|
# The internal one generated by the sub app is not yet producing the correct attributes
|
||||||
|
server_span = next(
|
||||||
|
(
|
||||||
|
span
|
||||||
|
for span in spans_with_http_attributes
|
||||||
|
if span.kind == SpanKind.SERVER
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(server_span)
|
||||||
|
# As soon as the bug is fixed for starlette, we can iterate over spans_with_http_attributes here
|
||||||
|
# to verify the correctness of the attributes for the internal span as well
|
||||||
|
self.assertEqual(
|
||||||
|
"/sub/home", server_span.attributes[SpanAttributes.HTTP_TARGET]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"http://testserver/sub/home",
|
||||||
|
server_span.attributes[SpanAttributes.HTTP_URL],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestAutoInstrumentationLogic(unittest.TestCase):
|
class TestAutoInstrumentationLogic(unittest.TestCase):
|
||||||
def test_instrumentation(self):
|
def test_instrumentation(self):
|
||||||
|
Reference in New Issue
Block a user