diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 6391636035..44f07586aa 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -64,11 +64,7 @@ def get( def collect_request_attributes(scope): """Collects HTTP request attributes from the ASGI scope and returns a dictionary to be used as span creation attributes.""" - server = scope.get("server") or ["0.0.0.0", 80] - port = server[1] - server_host = server[0] + (":" + str(port) if port != 80 else "") - full_path = scope.get("root_path", "") + scope.get("path", "") - http_url = scope.get("scheme", "http") + "://" + server_host + full_path + server_host, port, http_url = get_host_port_url_tuple(scope) query_string = scope.get("query_string") if query_string and http_url: if isinstance(query_string, bytes): @@ -105,6 +101,17 @@ def collect_request_attributes(scope): return result +def get_host_port_url_tuple(scope): + """Returns (host, port, full_url) tuple. + """ + server = scope.get("server") or ["0.0.0.0", 80] + port = server[1] + server_host = server[0] + (":" + str(port) if port != 80 else "") + full_path = scope.get("root_path", "") + scope.get("path", "") + http_url = scope.get("scheme", "http") + "://" + server_host + full_path + return server_host, port, http_url + + def set_status_code(span, status_code): """Adds HTTP response attributes to span using the status_code argument.""" if not span.is_recording(): @@ -152,12 +159,13 @@ class OpenTelemetryMiddleware: Optional: Defaults to get_default_span_details. """ - def __init__(self, app, span_details_callback=None): + def __init__(self, app, excluded_urls=None, span_details_callback=None): self.app = guarantee_single_callable(app) self.tracer = trace.get_tracer(__name__, __version__) self.span_details_callback = ( span_details_callback or get_default_span_details ) + self.excluded_urls = excluded_urls async def __call__(self, scope, receive, send): """The ASGI application @@ -170,6 +178,10 @@ async def __call__(self, scope, receive, send): if scope["type"] not in ("http", "websocket"): return await self.app(scope, receive, send) + _, _, url = get_host_port_url_tuple(scope) + if self.excluded_urls and self.excluded_urls.url_disabled(url): + return await self.app(scope, receive, send) + token = context.attach(propagators.extract(carrier_getter, scope)) span_name, additional_attributes = self.span_details_callback(scope) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md index c8c5cea0d3..824e355ec5 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Added support for excluding some routes with env var `OTEL_PYTHON_FASTAPI_EXCLUDED_URLS` + ([#237](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/237)) + ## Version 0.11b0 Released 2020-07-28 diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/README.rst b/instrumentation/opentelemetry-instrumentation-fastapi/README.rst index 4cc612da76..0594bb78e5 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/README.rst +++ b/instrumentation/opentelemetry-instrumentation-fastapi/README.rst @@ -19,6 +19,21 @@ Installation pip install opentelemetry-instrumentation-fastapi +Configuration +------------- + +Exclude lists +************* +To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FASTAPI_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. + +For example, + +:: + + export OTEL_PYTHON_FASTAPI_EXCLUDED_URLS="client/.*/info,healthcheck" + +will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. + Usage ----- diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index 57c9a5bfc7..b938464065 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -16,10 +16,13 @@ import fastapi from starlette.routing import Match +from opentelemetry.configuration import Configuration from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.fastapi.version import __version__ # noqa from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +_excluded_urls = Configuration()._excluded_urls("fastapi") + class FastAPIInstrumentor(BaseInstrumentor): """An instrumentor for FastAPI @@ -36,6 +39,7 @@ def instrument_app(app: fastapi.FastAPI): if not getattr(app, "is_instrumented_by_opentelemetry", False): app.add_middleware( OpenTelemetryMiddleware, + excluded_urls=_excluded_urls, span_details_callback=_get_route_details, ) app.is_instrumented_by_opentelemetry = True @@ -52,7 +56,9 @@ class _InstrumentedFastAPI(fastapi.FastAPI): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.add_middleware( - OpenTelemetryMiddleware, span_details_callback=_get_route_details + OpenTelemetryMiddleware, + excluded_urls=_excluded_urls, + span_details_callback=_get_route_details, ) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 47617d4e95..2cabb0f0c1 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -13,11 +13,13 @@ # limitations under the License. import unittest +from unittest.mock import patch import fastapi from fastapi.testclient import TestClient import opentelemetry.instrumentation.fastapi as otel_fastapi +from opentelemetry.configuration import Configuration from opentelemetry.test.test_base import TestBase @@ -29,10 +31,26 @@ def _create_app(self): def setUp(self): super().setUp() + Configuration()._reset() + self.env_patch = patch.dict( + "os.environ", + {"OTEL_PYTHON_FASTAPI_EXCLUDED_URLS": "/exclude/123,healthzz"}, + ) + self.env_patch.start() + self.exclude_patch = patch( + "opentelemetry.instrumentation.fastapi._excluded_urls", + Configuration()._excluded_urls("fastapi"), + ) + self.exclude_patch.start() self._instrumentor = otel_fastapi.FastAPIInstrumentor() self._app = self._create_app() self._client = TestClient(self._app) + def tearDown(self): + super().tearDown() + self.env_patch.stop() + self.exclude_patch.stop() + def test_basic_fastapi_call(self): self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() @@ -54,6 +72,15 @@ def test_fastapi_route_attribute_added(self): # the asgi instrumentation is successfully feeding though. self.assertEqual(spans[-1].attributes["http.flavor"], "1.1") + def test_fastapi_excluded_urls(self): + """Ensure that given fastapi routes are excluded.""" + self._client.get("/exclude/123") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + self._client.get("/healthzz") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + @staticmethod def _create_fastapi_app(): app = fastapi.FastAPI() @@ -66,6 +93,14 @@ async def _(): async def _(username: str): return {"message": username} + @app.get("/exclude/{param}") + async def _(param: str): + return {"message": param} + + @app.get("/healthzz") + async def health(): + return {"message": "ok"} + return app diff --git a/instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md index 1991025f6c..c022ef67e7 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## Unreleased +- Added support for excluding some routes with env var `OTEL_PYTHON_STARLETTE_EXCLUDED_URLS` + ([#237](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/237)) ## Version 0.10b0 diff --git a/instrumentation/opentelemetry-instrumentation-starlette/README.rst b/instrumentation/opentelemetry-instrumentation-starlette/README.rst index 1d05c0b717..85aef860c3 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/README.rst +++ b/instrumentation/opentelemetry-instrumentation-starlette/README.rst @@ -19,6 +19,21 @@ Installation pip install opentelemetry-instrumentation-starlette +Configuration +------------- + +Exclude lists +************* +To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_STARLETTE_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. + +For example, + +:: + + export OTEL_PYTHON_STARLETTE_EXCLUDED_URLS="client/.*/info,healthcheck" + +will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. + Usage ----- diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py index f469447f3a..1725123b6d 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -16,10 +16,13 @@ from starlette import applications from starlette.routing import Match +from opentelemetry.configuration import Configuration from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.starlette.version import __version__ # noqa +_excluded_urls = Configuration()._excluded_urls("starlette") + class StarletteInstrumentor(BaseInstrumentor): """An instrumentor for starlette @@ -36,6 +39,7 @@ def instrument_app(app: applications.Starlette): if not getattr(app, "is_instrumented_by_opentelemetry", False): app.add_middleware( OpenTelemetryMiddleware, + excluded_urls=_excluded_urls, span_details_callback=_get_route_details, ) app.is_instrumented_by_opentelemetry = True @@ -52,7 +56,9 @@ class _InstrumentedStarlette(applications.Starlette): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.add_middleware( - OpenTelemetryMiddleware, span_details_callback=_get_route_details + OpenTelemetryMiddleware, + excluded_urls=_excluded_urls, + span_details_callback=_get_route_details, ) diff --git a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py index a49db07c9a..1729741363 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from unittest.mock import patch from starlette import applications from starlette.responses import PlainTextResponse @@ -20,6 +21,7 @@ from starlette.testclient import TestClient import opentelemetry.instrumentation.starlette as otel_starlette +from opentelemetry.configuration import Configuration from opentelemetry.test.test_base import TestBase @@ -31,10 +33,26 @@ def _create_app(self): def setUp(self): super().setUp() + Configuration()._reset() + self.env_patch = patch.dict( + "os.environ", + {"OTEL_PYTHON_STARLETTE_EXCLUDED_URLS": "/exclude/123,healthzz"}, + ) + self.env_patch.start() + self.exclude_patch = patch( + "opentelemetry.instrumentation.starlette._excluded_urls", + Configuration()._excluded_urls("starlette"), + ) + self.exclude_patch.start() self._instrumentor = otel_starlette.StarletteInstrumentor() self._app = self._create_app() self._client = TestClient(self._app) + def tearDown(self): + super().tearDown() + self.env_patch.stop() + self.exclude_patch.stop() + def test_basic_starlette_call(self): self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() @@ -56,13 +74,26 @@ def test_starlette_route_attribute_added(self): # the asgi instrumentation is successfully feeding though. self.assertEqual(spans[-1].attributes["http.flavor"], "1.1") + def test_starlette_excluded_urls(self): + """Ensure that givem starlette routes are excluded.""" + self._client.get("/healthzz") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + @staticmethod def _create_starlette_app(): def home(_): return PlainTextResponse("hi") + def health(_): + return PlainTextResponse("ok") + app = applications.Starlette( - routes=[Route("/foobar", home), Route("/user/{username}", home)] + routes=[ + Route("/foobar", home), + Route("/user/{username}", home), + Route("/healthzz", health), + ] ) return app