Add ability to exclude some routes in fastapi and starlette (#237)

This commit is contained in:
Srikanth Chekuri
2020-12-08 18:24:32 +00:00
committed by GitHub
parent b310ec1728
commit 76fda25b25
9 changed files with 134 additions and 9 deletions

View File

@ -64,11 +64,7 @@ carrier_getter = CarrierGetter()
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 @@ class OpenTelemetryMiddleware:
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)

View File

@ -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

View File

@ -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
-----

View File

@ -16,10 +16,13 @@ from typing import Optional
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 @@ class FastAPIInstrumentor(BaseInstrumentor):
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,
)

View File

@ -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 @@ class TestFastAPIManualInstrumentation(TestBase):
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 @@ class TestFastAPIManualInstrumentation(TestBase):
# 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 @@ class TestFastAPIManualInstrumentation(TestBase):
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

View File

@ -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

View File

@ -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
-----

View File

@ -16,10 +16,13 @@ from typing import Optional
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 @@ class StarletteInstrumentor(BaseInstrumentor):
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,
)

View File

@ -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.routing import Route
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 @@ class TestStarletteManualInstrumentation(TestBase):
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 @@ class TestStarletteManualInstrumentation(TestBase):
# 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