Skip to content

Commit 76fda25

Browse files
authored
Add ability to exclude some routes in fastapi and starlette (#237)
1 parent b310ec1 commit 76fda25

File tree

9 files changed

+134
-9
lines changed

9 files changed

+134
-9
lines changed

instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,7 @@ def get(
6464
def collect_request_attributes(scope):
6565
"""Collects HTTP request attributes from the ASGI scope and returns a
6666
dictionary to be used as span creation attributes."""
67-
server = scope.get("server") or ["0.0.0.0", 80]
68-
port = server[1]
69-
server_host = server[0] + (":" + str(port) if port != 80 else "")
70-
full_path = scope.get("root_path", "") + scope.get("path", "")
71-
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
67+
server_host, port, http_url = get_host_port_url_tuple(scope)
7268
query_string = scope.get("query_string")
7369
if query_string and http_url:
7470
if isinstance(query_string, bytes):
@@ -105,6 +101,17 @@ def collect_request_attributes(scope):
105101
return result
106102

107103

104+
def get_host_port_url_tuple(scope):
105+
"""Returns (host, port, full_url) tuple.
106+
"""
107+
server = scope.get("server") or ["0.0.0.0", 80]
108+
port = server[1]
109+
server_host = server[0] + (":" + str(port) if port != 80 else "")
110+
full_path = scope.get("root_path", "") + scope.get("path", "")
111+
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
112+
return server_host, port, http_url
113+
114+
108115
def set_status_code(span, status_code):
109116
"""Adds HTTP response attributes to span using the status_code argument."""
110117
if not span.is_recording():
@@ -152,12 +159,13 @@ class OpenTelemetryMiddleware:
152159
Optional: Defaults to get_default_span_details.
153160
"""
154161

155-
def __init__(self, app, span_details_callback=None):
162+
def __init__(self, app, excluded_urls=None, span_details_callback=None):
156163
self.app = guarantee_single_callable(app)
157164
self.tracer = trace.get_tracer(__name__, __version__)
158165
self.span_details_callback = (
159166
span_details_callback or get_default_span_details
160167
)
168+
self.excluded_urls = excluded_urls
161169

162170
async def __call__(self, scope, receive, send):
163171
"""The ASGI application
@@ -170,6 +178,10 @@ async def __call__(self, scope, receive, send):
170178
if scope["type"] not in ("http", "websocket"):
171179
return await self.app(scope, receive, send)
172180

181+
_, _, url = get_host_port_url_tuple(scope)
182+
if self.excluded_urls and self.excluded_urls.url_disabled(url):
183+
return await self.app(scope, receive, send)
184+
173185
token = context.attach(propagators.extract(carrier_getter, scope))
174186
span_name, additional_attributes = self.span_details_callback(scope)
175187

instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Added support for excluding some routes with env var `OTEL_PYTHON_FASTAPI_EXCLUDED_URLS`
6+
([#237](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/237))
7+
58
## Version 0.11b0
69

710
Released 2020-07-28

instrumentation/opentelemetry-instrumentation-fastapi/README.rst

+15
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ Installation
1919

2020
pip install opentelemetry-instrumentation-fastapi
2121

22+
Configuration
23+
-------------
24+
25+
Exclude lists
26+
*************
27+
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.
28+
29+
For example,
30+
31+
::
32+
33+
export OTEL_PYTHON_FASTAPI_EXCLUDED_URLS="client/.*/info,healthcheck"
34+
35+
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
36+
2237

2338
Usage
2439
-----

instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
import fastapi
1717
from starlette.routing import Match
1818

19+
from opentelemetry.configuration import Configuration
1920
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
2021
from opentelemetry.instrumentation.fastapi.version import __version__ # noqa
2122
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
2223

24+
_excluded_urls = Configuration()._excluded_urls("fastapi")
25+
2326

2427
class FastAPIInstrumentor(BaseInstrumentor):
2528
"""An instrumentor for FastAPI
@@ -36,6 +39,7 @@ def instrument_app(app: fastapi.FastAPI):
3639
if not getattr(app, "is_instrumented_by_opentelemetry", False):
3740
app.add_middleware(
3841
OpenTelemetryMiddleware,
42+
excluded_urls=_excluded_urls,
3943
span_details_callback=_get_route_details,
4044
)
4145
app.is_instrumented_by_opentelemetry = True
@@ -52,7 +56,9 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
5256
def __init__(self, *args, **kwargs):
5357
super().__init__(*args, **kwargs)
5458
self.add_middleware(
55-
OpenTelemetryMiddleware, span_details_callback=_get_route_details
59+
OpenTelemetryMiddleware,
60+
excluded_urls=_excluded_urls,
61+
span_details_callback=_get_route_details,
5662
)
5763

5864

instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

+35
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
# limitations under the License.
1414

1515
import unittest
16+
from unittest.mock import patch
1617

1718
import fastapi
1819
from fastapi.testclient import TestClient
1920

2021
import opentelemetry.instrumentation.fastapi as otel_fastapi
22+
from opentelemetry.configuration import Configuration
2123
from opentelemetry.test.test_base import TestBase
2224

2325

@@ -29,10 +31,26 @@ def _create_app(self):
2931

3032
def setUp(self):
3133
super().setUp()
34+
Configuration()._reset()
35+
self.env_patch = patch.dict(
36+
"os.environ",
37+
{"OTEL_PYTHON_FASTAPI_EXCLUDED_URLS": "/exclude/123,healthzz"},
38+
)
39+
self.env_patch.start()
40+
self.exclude_patch = patch(
41+
"opentelemetry.instrumentation.fastapi._excluded_urls",
42+
Configuration()._excluded_urls("fastapi"),
43+
)
44+
self.exclude_patch.start()
3245
self._instrumentor = otel_fastapi.FastAPIInstrumentor()
3346
self._app = self._create_app()
3447
self._client = TestClient(self._app)
3548

49+
def tearDown(self):
50+
super().tearDown()
51+
self.env_patch.stop()
52+
self.exclude_patch.stop()
53+
3654
def test_basic_fastapi_call(self):
3755
self._client.get("/foobar")
3856
spans = self.memory_exporter.get_finished_spans()
@@ -54,6 +72,15 @@ def test_fastapi_route_attribute_added(self):
5472
# the asgi instrumentation is successfully feeding though.
5573
self.assertEqual(spans[-1].attributes["http.flavor"], "1.1")
5674

75+
def test_fastapi_excluded_urls(self):
76+
"""Ensure that given fastapi routes are excluded."""
77+
self._client.get("/exclude/123")
78+
spans = self.memory_exporter.get_finished_spans()
79+
self.assertEqual(len(spans), 0)
80+
self._client.get("/healthzz")
81+
spans = self.memory_exporter.get_finished_spans()
82+
self.assertEqual(len(spans), 0)
83+
5784
@staticmethod
5885
def _create_fastapi_app():
5986
app = fastapi.FastAPI()
@@ -66,6 +93,14 @@ async def _():
6693
async def _(username: str):
6794
return {"message": username}
6895

96+
@app.get("/exclude/{param}")
97+
async def _(param: str):
98+
return {"message": param}
99+
100+
@app.get("/healthzz")
101+
async def health():
102+
return {"message": "ok"}
103+
69104
return app
70105

71106

instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Changelog
22

33
## Unreleased
4+
- Added support for excluding some routes with env var `OTEL_PYTHON_STARLETTE_EXCLUDED_URLS`
5+
([#237](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/237))
46

57
## Version 0.10b0
68

instrumentation/opentelemetry-instrumentation-starlette/README.rst

+15
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ Installation
1919

2020
pip install opentelemetry-instrumentation-starlette
2121

22+
Configuration
23+
-------------
24+
25+
Exclude lists
26+
*************
27+
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.
28+
29+
For example,
30+
31+
::
32+
33+
export OTEL_PYTHON_STARLETTE_EXCLUDED_URLS="client/.*/info,healthcheck"
34+
35+
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
36+
2237

2338
Usage
2439
-----

instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
from starlette import applications
1717
from starlette.routing import Match
1818

19+
from opentelemetry.configuration import Configuration
1920
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
2021
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
2122
from opentelemetry.instrumentation.starlette.version import __version__ # noqa
2223

24+
_excluded_urls = Configuration()._excluded_urls("starlette")
25+
2326

2427
class StarletteInstrumentor(BaseInstrumentor):
2528
"""An instrumentor for starlette
@@ -36,6 +39,7 @@ def instrument_app(app: applications.Starlette):
3639
if not getattr(app, "is_instrumented_by_opentelemetry", False):
3740
app.add_middleware(
3841
OpenTelemetryMiddleware,
42+
excluded_urls=_excluded_urls,
3943
span_details_callback=_get_route_details,
4044
)
4145
app.is_instrumented_by_opentelemetry = True
@@ -52,7 +56,9 @@ class _InstrumentedStarlette(applications.Starlette):
5256
def __init__(self, *args, **kwargs):
5357
super().__init__(*args, **kwargs)
5458
self.add_middleware(
55-
OpenTelemetryMiddleware, span_details_callback=_get_route_details
59+
OpenTelemetryMiddleware,
60+
excluded_urls=_excluded_urls,
61+
span_details_callback=_get_route_details,
5662
)
5763

5864

instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
# limitations under the License.
1414

1515
import unittest
16+
from unittest.mock import patch
1617

1718
from starlette import applications
1819
from starlette.responses import PlainTextResponse
1920
from starlette.routing import Route
2021
from starlette.testclient import TestClient
2122

2223
import opentelemetry.instrumentation.starlette as otel_starlette
24+
from opentelemetry.configuration import Configuration
2325
from opentelemetry.test.test_base import TestBase
2426

2527

@@ -31,10 +33,26 @@ def _create_app(self):
3133

3234
def setUp(self):
3335
super().setUp()
36+
Configuration()._reset()
37+
self.env_patch = patch.dict(
38+
"os.environ",
39+
{"OTEL_PYTHON_STARLETTE_EXCLUDED_URLS": "/exclude/123,healthzz"},
40+
)
41+
self.env_patch.start()
42+
self.exclude_patch = patch(
43+
"opentelemetry.instrumentation.starlette._excluded_urls",
44+
Configuration()._excluded_urls("starlette"),
45+
)
46+
self.exclude_patch.start()
3447
self._instrumentor = otel_starlette.StarletteInstrumentor()
3548
self._app = self._create_app()
3649
self._client = TestClient(self._app)
3750

51+
def tearDown(self):
52+
super().tearDown()
53+
self.env_patch.stop()
54+
self.exclude_patch.stop()
55+
3856
def test_basic_starlette_call(self):
3957
self._client.get("/foobar")
4058
spans = self.memory_exporter.get_finished_spans()
@@ -56,13 +74,26 @@ def test_starlette_route_attribute_added(self):
5674
# the asgi instrumentation is successfully feeding though.
5775
self.assertEqual(spans[-1].attributes["http.flavor"], "1.1")
5876

77+
def test_starlette_excluded_urls(self):
78+
"""Ensure that givem starlette routes are excluded."""
79+
self._client.get("/healthzz")
80+
spans = self.memory_exporter.get_finished_spans()
81+
self.assertEqual(len(spans), 0)
82+
5983
@staticmethod
6084
def _create_starlette_app():
6185
def home(_):
6286
return PlainTextResponse("hi")
6387

88+
def health(_):
89+
return PlainTextResponse("ok")
90+
6491
app = applications.Starlette(
65-
routes=[Route("/foobar", home), Route("/user/{username}", home)]
92+
routes=[
93+
Route("/foobar", home),
94+
Route("/user/{username}", home),
95+
Route("/healthzz", health),
96+
]
6697
)
6798
return app
6899

0 commit comments

Comments
 (0)